Bond

The Bond Language

When Bond was created, the goal was not to invent yet another language; it was more of an endeavour to create a minimalistic runtime environment for a bytecode interpreted language. So, where possible, Bond borrowed its syntax and semantics from C. This document begins by covering some notable differences between Bond and C that exist in the spirit of keeping things clean and simple. Then the document moves on to cover the Bond language in detail.

Notable Differences With C

No preprocessor

There is no preprocessor, so there are no macros or other mechanisms to manipulate the source code before it is scanned by the compiler.

No header files or forward declarations

All code is placed in .bond files with no need for separate header files. Header files are not needed since type, function and constant declarations do not need to occur before they are referenced in source code; the compiler will resolve those references during semantic analysis after all files have been parsed. This important difference with C also means that structs do not need to be forward declared and functions do not need separate declarations and definitions.

Include directives

Despite the lack of header files, the Bond language supports include directives that tell the compiler which other files to scan and parse. In contrast to C's include directive, Bond's is a compiler directive, not a preprocessor directive, so the text of the included file is not substituted into the including file. A file that is included multiple times, or circularly, is only scanned once, so include guards are not needed. See the section on Compiler Directives for further details.

Ambiguities in the grammar

C's grammar is ambiguous and Bond inherited those ambiguities. Some statements in C can be parsed in more than one way, such as the following:

A * B;

This line of code can be parsed as the declarative statement: B is a pointer to type A. Or it can be parsed as the expression statement: the value of variable A times the value of variable B. C compilers resolve the ambiguity using forward declarations and by giving the parser access to the symbol table so that it can look up whether A is a type. This approach introduces a burden on the programmer to add forward declarations to the source code and pushes elements of semantic analysis (e.g. maintenance of the symbol table) into the parser.

Fortunately, such ambiguities result in statements with no effect when parsed as expression statements, so it is not necessary to support such statements. Bond's approach to resolving the ambiguity is to favour parsing statements as declarative statements whenever possible. If it turns out that A is not a type, the semantic analyzer will raise an error to that effect during a later stage of compilation.

In hindsight, it may have been worth considering eliminating such ambiguities by modifying the syntax for declarative statements.

For clarity and to avoid other possible ambiguities, the syntax for casting and the sizeof operator were both slightly modified from C. The syntax for casting is:

cast<type>(expression)

The sizeof operator comes in two forms: one for types and one for expressions. The notation is the following:

sizeof<type>
sizeof expression

The expression in the second form can optionally be wrapped in parentheses as is the case in C.

Type declarations

To simplify reading and parsing type declarations, all elements pertaining to type declarations (e.g. type names, [], *, const) appear prior to variable names. The most obvious effect of this change is with array declarations, but there are some other subtleties as well.

int[3][5] array; // C equivalent is: int array[3][5];
int[3] a, b;     // Both a and b are of type int[3].
int* a, b;       // Both a and b are of type int*. In C, a is int* and b is int.

Primitive type names

All primitive type names consist of a single reserved keyword. Where C has the type unsigned int, Bond has uint.

Numeric types have a fixed width

The types char, short, int and long (as well as their unsigned counterparts) have 8, 16, 32, and 64 bits respectively. The types float and double have 32 and 64 bits respectively. See the section on Primitive Types for further details.

Conditions must be Boolean

The conditions of if, while, do and for statements and of the ternary operator, as well as the operands of the !, && and || operators, must be of type bool. Integers and pointers are not implicitly converted to bool, and casts between bool and numeric types are not allowed. Instead, compare the value explicitly.

if (ptr)          // Error: ptr is not a bool.
if (ptr != null)  // OK.
while (count)     // Error: count is not a bool.
while (count > 0) // OK.

Unreachable code is an error

A statement that can never be executed, such as one that follows a return, break or continue statement in the same block, is reported as a compile error rather than being silently ignored.

No function pointers

Indeed, the Bond language makes no provisions for function pointers.

No variadic functions

Adding support for variadic functions involves work that has not been tackled. Bond may have variadic functions someday, but for the time being, that work is not planned.

Namespaces

Bond has namespaces that serve the same purpose as C++ namespaces, which is to prevent name collisions. All declarations of the Bond standard library are in the Bond namespace.

namespace Foo
{
    const int BAR = 42;
}

// Identifiers within namespaces are qualified with ::
const int BAZ = Foo::BAR;

See the section on Namespaces for further details.

Structs can have member functions

Another aspect borrowed from C++ is struct member functions. See the section on Structure Types for further details.

No global or static variables

Global constants are permitted, however, global variables are not. (TODO: see the documentation on the Virtual Machine Architecture for the justification.)

const int[2][3] GLOBAL_CONSTANT = {{1, 2, 3}, {4, 5, 6}}; // Permitted
int[2][3] GLOBAL_VARIABLE = {{1, 2, 3}, {4, 5, 6}};       // Error

There are also no static variables, so all local variables have a lifetime that does not surpass that of the scope in which they are declared.

Scope-based resource management

C has explicit resource management, meaning that the acquisition of a resource must be explicitly paired with the release of that resource in code. For example, memory allocated with malloc() must be deallocated with free() and files opened with fopen() must be closed with fclose(). Bond supports explicit resource management as well, but it also has a form of scope-based resource management that allows resources to be automatically released when a function returns. See the section on Scope-Based Resource Management for further details.

Native blocks

Declarations for functions and structs implemented in C++ can be placed in native blocks, allowing those functions to be called and those types to be manipulated from Bond code with full type checking and semantic analysis. See the section on Native Blocks for further details.

Language Reference

Basics of Bond

Structure of a Program

For a quick overview of several of the fundamental concepts of Bond, let us take a quick look at the ubiquitous Hello World program. The source code for it follows.

1
2
3
4
5
6
7
8
// This is a Hello World program written in Bond.
include "io.bond";

int main()
{
    Bond::StdOut()->PrintStr("Hello World!");
    return 0;
}

When this program runs, it prints the following output to the screen.

Hello World!

Here is a line-by-line breakdown of the program.

Line 1: // This is a Hello World program written in Bond.
Two consecutive forward slashes indicate the beginning of a comment. Comments are useful for documenting code, but do not influence the program. They are ignored by the compiler. See the Comments section for details.
Line 2: include "io.bond";
An include directive tells the compiler that the programmer wishes to reference code written in another file. In this case, the referenced file is io.bond. The file io.bond is part of the Standard Bond Library and defines input and output operations.
Line 3: A blank line.
Blank lines have no effect on the program, but do help to organize code and improve readability.
Line 4: int main()
This line begins the definition of a function whose name is main. A function encapsulates the code that performs a specific task or operation. Functions are covered in greater detail in the Functions section later.

A function definition specifies a return type (i.e. what kind of result the function produces; in this case int), a name (main), and a parameter list enclosed in parentheses (i.e. the operands of the operation performed by the function). This example does not require any parameters.

The main function is a special function for standalone Bond programs. It is the entry point of the program (i.e. where execution of the program begins). When Bond is embedded in a larger application written in C++, any function may be an entry point.

Lines 5 and 8: { and }
A function has an opening brace and a closing brace to enclose its body, the code that performs the function's task.
Line 6: Bond::StdOut()->PrintStr("Hello World!");
This line of code is a statement. Statements perform actions. This particular line of code is quite loaded, since it calls two functions.

The first function is called Bond::StdOut and it is defined in io.bond, which was included on line 2. The function is actually just called StdOut and it is defined in the namespace called Bond. We will see more about namespaces in the Namespaces section. The pair of parentheses that follow indicate that we are calling the function. The function returns a pointer to an OutputStream, which allows the program to print some output.

The second function is called PrintStr. The -> symbol indicates that the function belongs to the OutputStream pointed to by the value returned by the first function. Again the pair of parentheses indicate that we are calling the function. However, this time the parentheses enclose the string "Hello World!". That string is given to the PrintStr function so that the function may print it to the screen.

Line 7: return 0;
This line of code is a statement that specifies what value is returned by the main function. As we saw on line 4, the main function returns an int. In Bond, as with other programming languages, such as C and C++, the return value of the main function indicates the exit status of the program. By convention, a return value of 0 indicates that the program terminated normally, whereas any other value indicates an application specific error code.

Comments

Comments are text that is ignored by the compiler. Bond supports both styles of comments found in C. A line comment begins with // and extends to the end of the line. A block comment begins with /* and ends with the next */, and may span several lines. Block comments do not nest.

// This is a line comment.
int x = 7; // A line comment can follow code.

/* This is a block comment
   that spans multiple lines. */
int y = /* even in the middle of a line */ 8;

Compiler Directives

Bond currently has one compiler directive: include. Unlike preprocessor directives in C, Bond compiler directives do not perform textual substitution. Instead, they provide instructions to the compiler about what additional source files should be scanned and parsed.

The syntax for an include directive is the reserved keyword include, followed by a string literal path and a semicolon.

include "io.bond";
include "memory.bond";

Include directives may only appear at file scope, where they can be interleaved with other declarations. They cannot appear inside namespaces or functions. If the same file is included more than once (directly or indirectly through other includes), the compiler scans that file only once.

// app.bond
include "io.bond";

int main()
{
    Bond::StdOut()->PrintStr("Compiler directives are simple and explicit.\n");
    return 0;
}

Identifiers

Identifiers are names for symbols such as variables, functions and types defined in code. Identifiers consist of any sequence of letters, digits and underscores ('_'), with the exception that the first character cannot be a digit. Also, keywords are reserved and cannot be used as identifiers. Identifiers are case sensitive.

Keywords

Keywords are reserved by the language and have special meaning for the compiler. The following is a list of all Bond keywords.

alignof bool break case cast char const continue default do double else enum false float for if include int long namespace native null return short sizeof struct switch this true uchar uint ulong ushort using void while

Primitive Types

Bond is a statically typed language, so every variable and expression has a type known at compile time. The type of a value describes how much space it occupies in memory, how that memory is interpreted and what operations and range of values are valid. At the core of the type system are the primitive types. Primitive types are predefined in the language and are named by their reserved keyword. The type system is actually much more vast and the remaining types are covered in the Data Types section.

Boolean Type

There is one type to represent Boolean values named bool. It has two values: true and false. The bool type is implemented in the Bond Runtime Library using the C++ uint8_t type.

Integer Types

The integer data types are used for storing values which are whole numbers. There are several types that vary in size and range of values that they can represent. Note that the char and uchar types are used to store characters.

The following table provides the details for all of the integer types, including the underlying C++ types used to implement them in the Bond Runtime Library.

Type C++ Type Storage Size Value Range
char int8_t 8 bits -128 to 127
uchar uint8_t 8 bits 0 to 255
short int16_t 16 bits -32,768 to 32,767
ushort uint16_t 16 bits 0 to 65,535
int int32_t 32 bits -2,147,483,648 to 2,147,483,647
uint uint32_t 32 bits 0 to 4,294,967,295
long int64_t 64 bits -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
ulong uint64_t 64 bits 0 to 18,446,744,073,709,551,615
Floating-Point Types

Bond supports two floating-point types which are used for storing values that approximate real numbers: float and double. These two types are implemented in the Bond Runtime Library using the C++ float and double types, which are assumed to conform to the IEEE 754 binary32 and binary64 formats respectively.

Literal Constants

Literal constants represent immutable values in source code. They can be booleans, numbers, characters, strings of characters or the null pointer.

Boolean Constants

The type bool has two values represented by the keywords true and false.

Integer Constants

Integer constants represent integral values (i.e. whole numbers). They can be expressed in decimal (base 10), octal (base 8) or hexadecimal (base 16).

Hexadecimal integers consist of 0x (a lowercase x is required) and a sequence of hexadecimal digits which are the digits 0 through 9 and the letters a through f and A through F. For example, 0x2f represents decimal value 47.

Octal integers consist of 0 and a sequence of octal digits which are the digits 0 through 7. For example, 063 represents the decimal value 51.

Decimal integers consist of a sequence of the digits 0 through 9 and cannot begin with a 0.

Without a suffix, an integer constant is of type int. An optional suffix u or U can be appended to force the value to be of type uint and an optional suffix l or L can be appended to force the value to be of type long. The two can be combined in either order to create values of type ulong. For example, 27u is of type uint, and 27ul and 27lu are of type ulong.

Unlike C, the type of an integer constant is determined by its suffix alone, not by the magnitude of its value. A constant without an l or L suffix is truncated to 32 bits, even if its value does not fit. For example, 4294967296 is an int whose value is 0, whereas 4294967296L is a long with the intended value.

Floating-Point Constants

Floating-point constants represent an approximation of real numbers. They are composed of an integral part, a decimal point, a fractional part and an exponent part. The integral part or the fractional part may be omitted, but not both and the exponent may always be omitted. The integral and fractional parts consist of a sequence of digits, while the exponent consists of e or E followed by an optional sign and a sequence of digits.

An optional suffix f or F can be appended to force the value to be of type float, otherwise the value will be of type double.

A sequence of digits is a floating-point constant if it contains a decimal point, an exponent or the f suffix; otherwise it is an integer constant. The following are examples of valid floating-point constants.

3.          // Integral part only. Without the '.' the value would be an int.
.5          // Fractional part only.
3.14159     // Both integral and fractional parts.
314159e-5   // Same as above expressed with an exponent.
3.14159f    // This value is of type float.
3f          // Also a float, since the suffix makes it a floating-point constant.
Character Constants

Character constants represent values of type char and are typically expressed as a single character between single quotes. For example, 'a' represents the letter a.

Some values cannot be expressed with a single character and are expressed as an escape sequence between single quotes. Escape sequences begin with a backslash and are depicted in the table below. They can also be used in string literals.

Escape Sequence Meaning
\aAlert (bell)
\bBackspace
\fForm feed
\nNewline
\rCarriage return
\tHorizontal tab
\vVertical tab
\'Single quote
\"Double quote
\\Backslash
\?Question mark
\oooCharacter with the octal value ooo
\xhhCharacter with the hexadecimal value hh

Octal and hexadecimal escape sequences consume as many octal or hexadecimal digits as follow the backslash, and the resulting value must not exceed 255. For example, '\0' is the null character and '\x41' is the letter A.

String Literals

String literals represent a sequence of characters and are of type const char *. They consist of a sequence of characters and escape sequences enclosed in double quotes and are implicitly terminated with a null character (\0).

const char *greeting = "Hello\tWorld!\n";

A string literal can also be used to initialize an array of char, in which case the characters, including the terminating null character, are copied into the array. See the section on Array Types for details.

Null Pointer

The null pointer is represented by the keyword null. It is a special pointer value that points to no valid object. The null pointer does not have a specific pointer type; it can be assigned to or compared with any pointer type without a cast.

Null pointers are commonly used to initialize pointer variables that are assigned later, to represent optional references and to indicate failure in APIs that return pointers. Pointers, including examples that use null, are covered in the Pointer Types section.

Variables

A variable is a named storage area for a value of a particular type. A variable can be used to store the result of a computation, and recalling it later. The syntax to declare a variable is a type name followed by a name (an identifier). The following snippet shows the declaration of a variable named x that stores an int.

int x;

Several variables of the same type can be declared at once by using a comma separated list of names.

int x, y, z;

However, it is often easier to read and understand code that uses separate declarations.

int x;
int y;
int z;

A variable can be initialized when it is declared by following its name with an equal sign and an expression that produces the value that is assigned to the variable.

int x = 7;

The value of a variable can be accessed by placing its name in an expression.

int x = 6;
int y = 8;

// Add the value of variables x and y.
int sum = x + y;

A variable can be assigned a new value over the course of a program's execution by placing it on the left hand side of an equal sign.

// Initialize x to 0.
int x = 0;

// Add 1 to the current value of x and assign the result to x.
x = x + 1;

// The value of x is now 1.

The value assigned to a variable must be of an appropriate type, meaning that it has exactly the same type as the variable or that converting a numerical value from one type to another would not lead to a loss of data. For example, assigning a value of type double to a variable of type int would truncate the fractional part of the value, a value of type int cannot fit into a short since the latter's range is smaller, and an int would lose its sign when assigned to a uint. Conversely, assigning an int to a double and a short to an int are both acceptable since they are lossless operations. Assigning a uint to an int remains unsafe since both types can represent values that cannot be represented by the other. Less obviously, assigning an int to a float or a long to a double is also considered lossy, since the floating-point type does not have enough precision to represent every value of the integer type exactly. A conversion that could lose data requires an explicit cast; otherwise the compiler reports an error.

When the value being assigned is a constant that is known at compile time, the compiler checks the value itself rather than its type. The assignment is allowed as long as the value fits in the destination type. This is why constants of type int can be used to initialize variables of smaller or unsigned types.

short s = 5;      // OK: 5 fits in a short, even though 5 is an int.
uint u = 7;       // OK: 7 fits in a uint.
ulong i = 0;      // OK: 0 fits in a ulong.
uchar c = 300;    // Error: 300 does not fit in a uchar.
uint v = -1;      // Error: -1 does not fit in a uint.

int x = 7;
short t = x;      // Error: the value of x is not known at compile time
                  // and an int does not always fit in a short.
short w = cast<short>(x); // OK: the cast makes the conversion explicit.

A local variable must be declared before it is referenced, and it only exists until the end of the block in which it is declared (see Compound Statements). In contrast, constants, functions and types declared at file or namespace scope can be referenced anywhere, regardless of the order of their declarations.

A variable declared with the keyword const cannot have its value changed after it has been initialized.

const double PI = 3.14159;
PI = 4; // Error. Cannot assign a value to a const variable.

There are at least two benefits to using constant variables. The first is that it enables the compiler to perform optimizations. If a constant variable is initialized with a constant expression that can be evaluated at compile time, writing and reading the variable's value to and from memory becomes a superfluous activity. The compiler can instead substitute the variable's value, which it already knows, directly into the generated code wherever that variable is referenced.

The second benefit to constant variables is that they improve the readability of code. Even if the value of a constant variable is not known at compile time, it provides the reader with a guarantee that there is no code that alters the value of the variable after it has been initialized.

const int result = CalculateResult();

// Skip many lines of complicated code.

// Without understanding all of the above code, we are certain
// that the value of result has been determined on the first line.
int resultPlusOne = result + 1;

Operators

Assignment Operator

As we have seen in the Variables section, the assignment operator, represented by an equal sign =, assigns a value to a variable. Even though the assignment operator is represented by an equal sign, it is not used for determining equality.

The expression producing the value that is assigned is on the right side of the assignment operator, and the variable receiving the value is on the left. In the following example, the value of the variable y is assigned to the variable x.

x = y;

As with all other expressions, the assignment expression evaluates to a value, which is the value assigned. The value of the assignment expression can then be used in another expression. This approach should be used carefully, since it can lead to code which is difficult to understand. In the following example, the assignment of the value 7 to the variable y produces the value 7, which is then added to the value 8 and assigned to the variable x.

x = 8 + (y = 7); // x is 15 and y is 7.
Arithmetic Operators

The five binary arithmetic operators are +, -, *, / and %, and they perform addition, subtraction, multiplication, division and modulo division respectively. The syntax for using these operators is straightforward and resembles the notation for basic arithmetic (i.e. the two operands appear on either side of the operator). The operands can be any expression that evaluates to a numerical type, including constants, variables, other arithmetic expressions, function calls and so forth.

double a = 2.0;
double b = a + 1.0;      // Addition: b is 3.0.
double c = b - 7.0;      // Subtraction: c is -4.0.
double d = b * c;        // Multiplication: d is -12.0.
double e = d / a;        // Division: e is -6.0.
int remainder = 23 % 10; // Modulo division: remainder is 3.

Modulo division, which is applicable only to integers, produces the remainder of a division.

The + and - operators can also be used as unary operators and appear before their lone operand. The - operator negates its operand, meaning that it is equivalent to multiplying the operand by -1. The + operator is equivalent to multiplying its operand by 1, which leaves it intact. It is not particularly useful and is only included in the language for completeness.

double a = 3.0;
double b = -a; // b is -3.0.
double c = -b; // c is 3.0.
double d = +a; // d is 3.0.
double e = +b; // e is -3.0.

Putting it all together:

double a = -2.0 - -((3.0 * 12.0) / (3.0 + 6.0)); // a is 2.0.
Increment and Decrement Operators

Since adding and subtracting 1 from variables is such a common thing to do when programming, special shorthand operators exist specifically for that task denoted with ++ and --. The former adds 1 to its operand and the latter subtracts 1.

The following two lines of code are equivalent methods of adding 1 to the variable a.

++a;
a = a + 1;

Likewise, the following two lines of code are equivalent methods of subtracting 1 from the variable a.

--a;
a = a - 1;

Furthermore, the increment and decrement operators can both be used in prefix or suffix forms. Both forms perform the same operation, however they differ in their order of evaluation. In the prefix form, the addition or subtraction is performed first, then the value of the expression is evaluated. In the suffix form, the value of expression is evaluated first, then the addition or subtraction is performed. The following examples show the difference.

int a = 7;
int b = ++a;
// a is 8 and b is also 8. a is incremented before its value is captured.

a = 7;
b = a++;
// a is 8, but b is 7. The value of a is captured before it is incremented.
Relational and Comparison Operators

The six relational operators are ==, !=, <, >, <= and >=, and they are used to compare the value of two expressions. The result of a relational operator is a bool (i.e. either the value true or false) indicating whether the comparison was successful.

The == and != operators test whether two values are equal or not equal respectively.

int a = 7;
int b = 7;
int c = 9;
a == b;    // Result is true since both operands are 7.
a == c;    // Result is false since one operand is 7 and one is 9.
a != b;    // Result is false since both operands are 7.
a != c;    // Result is true since one operand is 7 and one is 9.

The <, >, <= and >= operators test whether the first operand is less than, greater than, less than or equal to and greater than or equal to the second operand respectively.

int a = 7;
int b = 7;
int c = 9;
a < b;     // Result is false since 7 is not less than 7.
a < c;     // Result is true since 7 is less than 9.
c < a;     // Result is false since 9 is not less than 7.
a > b;     // Result is false since 7 is not greater than 7.
a > c;     // Result is false since 7 is not greater than 9.
c > a;     // Result is true since 9 is greater than 7.
a <= b;    // Result is true since 7 is equal to 7.
a <= c;    // Result is true since 7 is less than 9.
c <= a;    // Result is false since 9 is not less than or equal to 7.
a >= b;    // Result is true since 7 is equal to 7.
a >= c;    // Result is false since 7 is not greater than or equal to 9.
c >= a;    // Result is true since 9 is greater than 7.
Logical Operators

The two binary logical operators && and || are used for combining the truth values of two Boolean expressions. The operands of all of the logical operators must be of type bool; unlike C, integers and pointers are not implicitly treated as truth values (see Conditions must be Boolean).

The conjunction operator && evaluates to true only if its operands are both true, otherwise it evaluates to false. If the first operand is false, the second operand is not evaluated at all, since the expression will evaluate to false regardless of the second operand's value.

The disjunction operator || evaluates to false only if its operands are both false, otherwise it evaluates to true. If the first operand is true, the second operand is not evaluated at all, since the expression will evaluate to true regardless of the second operand's value.

bool a = true;
bool b = true;
bool c = false;
bool d = false;
a && b;    // Result is true since both operands are true.
a && c;    // Result is false since second operand is false.
c && a;    // Result is false since first operand is false.
c && d;    // Result is false since both operands are false.
a || b;    // Result is true since both operands are true.
a || c;    // Result is true since first operand is true.
c || a;    // Result is true since second operand is true.
c || d;    // Result is false since both operands are false.

The third logical operator is the unary not operator !. This operator negates the value of its Boolean operand, such that false becomes true and vice-versa.

We can combine all of the operators that we have learned so far to formulate more complex conditions. The following snippet of code checks whether a variable named value falls in the range [5 - 10] if the range check is enabled. If the range check is not enabled, the condition always succeeds.

bool conditionValid = !rangeCheckEnabled || ((value >= 5) && (value <= 10));
Bitwise Logical Operators

There are a handful of operators for directly manipulating the bits of integer values. First are the bitwise logical operators, which are analogous to the Boolean logical operators seen above. With each of the binary bitwise logical operators, each bit of the left operand is paired with the corresponding bit of the right operand and the operation is performed between each pair of bits. A bit value of 0 corresponds to a truth value of false, whereas a bit value of 1 corresponds to a truth value of true.

The & operator performs a bitwise conjunction, while the | operator performs a bitwise disjunction. A third operator, ^, performs an exclusive disjunction. In the case of the exclusive disjunction operator, the result is true if only one, but not both of the operands are true.

int a = 12;
int b = 10;
a & b; // Result is  8, since 1100 & 1010 is 1000.
a | b; // Result is 14, since 1100 | 1010 is 1110.
a ^ b; // Result is  6, since 1100 ^ 1010 is 0110.

The ~ operator is analogous to the unary Boolean not operator. It flips each bit of its operand such that 0 becomes 1 and 1 becomes 0. Note that all bits including leading zeroes and the sign bit of signed integers are flipped.

int a = 12;
~a; // Result is -13, since ~1100 is 11111111111111111111111111110011.
Bit Shifting Operators

Another set of operators for manipulating the bits of integers are the bit shifting operators. The left shift operator << shifts the bits of the left operand to the left. The right operand specifies the number of positions by which the bits are shifted. Bits shifted out of the left side are discarded whereas bits inserted to the right are zero.

int a = 123; // a is 00000000000000000000000001111011.
a << 3;      // Result is 00000000000000000000001111011000.

Similarly, the >> operator shifts the bits of the left operand to the right by the number of positions specified by the right operand. Bits shifted out of the right side are discarded and bits inserted into the left side follow slightly more complicated rules. Inserted bits are usually zero, however, if the left operand is a signed integer, then the inserted bits are the same as the contents of the leftmost bit (i.e. the sign bit). This behaviour has the interesting property of preserving the sign of the left operand.

int a = 123; // a is 00000000000000000000000001111011.
a >> 3;      // Result is 00000000000000000000000000001111, zeros inserted on the left.
a = -123;    // a is 11111111111111111111111110000101.
a >> 3;      // Result is 11111111111111111111111111110000, ones inserted on the left.
Compound Assignment Operators

There are ten compound assignment operators that, in addition to performing an operation, assign the result to the variable or memory location (see Arrays and Pointers) described by the left operand. These operators do not introduce any new operations, they just provide convenient shorthand notation. See the table below for an overview of all of the compound assignment operators.

Compound Operator Equivalent Expression
a += b; a = a + b;
a -= b; a = a - b;
a *= b; a = a * b;
a /= b; a = a / b;
a %= b; a = a % b;
a &= b; a = a & b;
a |= b; a = a | b;
a ^= b; a = a ^ b;
a <<= b; a = a << b;
a >>= b; a = a >> b;

The equivalence is not exact in two respects. First, the left operand of a compound assignment is evaluated only once, which matters when evaluating it has side effects (e.g. array[i++] += 1;). Second, the result of the operation is converted back to the type of the left operand without the check for loss of data that applies to the plain assignment operator.

short s = 5;
s += 1;     // OK: the result is converted back to short.
s = s + 1;  // Error: s + 1 is an int, which does not always fit in a short.

The += and -= operators can also be applied to pointers to advance them by a number of elements (see Pointer Types).

Ternary Operator

The ternary operator, also known as the conditional operator, allows for an expression to evaluate to one of two results based on the evaluation of a boolean condition. The notation for the ternary operator is as follows:

condition ? expression1 : expression2

The condition expression is evaluated first, which must evaluate to a value of type bool. If the result of the condition is true, then expression1 is evaluated and the result of the ternary operator is the value of expression1. Otherwise, if the condition is false, then expression2 is evaluated and the result of the ternary operator is the value of expression2. Note that only one of expression1 or expression2 is evaluated. The two expressions must either both be of numeric types, both be pointers, both be of type bool or both be of the same struct type. When both are numeric, the result has the same type that an arithmetic operation on the two expressions would produce (e.g. if one is an int and the other is a double, the result is a double).

bool a = true;
bool b = false;
int c = 6;
int d = 8;
a ? c : d; // Result is 6 since a is true so the value of c is evaluated.
b ? c : d; // Result is 8 since b is false so the value of d is evaluated.
((c >= 1) && (c <= 10)) ? (d - 1) : (d + 1); // Result is 7 (d - 1) since c is in the range [1 - 10].

// Multiple possible outcomes based on the result of function calls.
Condition1() ? (Condition2() ? Result1() : Result2()) : (Condition3() ? Result3() : Result4());
Comma Operator

The comma operator is used to concatenate several expressions where only one expression is expected. All of the subexpressions are evaluated in order, however, the result of the enclosing expression is that of the last subexpression only.

int a = 0;
int b = ((a = 7), a * 2);  // a is assigned the value 7, then b is assigned the value 14 (i.e. 7 * 2).

The example above would be much easier to understand if it had been written in two statements: one that assigns a value to a and one that assigns a value to b. This lack of clarity is almost always present when using the comma operator, so it is best to avoid it when possible. There are cases, as we will see in the for Statements section, where the comma operator is an appropriate tool.

Type Casting Operator

The type casting operator allows a value of one type to be converted to another. It is often used to convert between numeric types, such as from double to int, but it can also be used to convert pointers from one type to another. Pointers are covered in the Pointer Types section. The notation for a type cast is as follows:

cast<Type>(value)

The result of the expression is value converted to Type. The following code snippet shows an example.

double a = 1.5;
double b = 2.6;

int c = cast<int>(a + b) + 5; // cast<int>(1.5 + 2.6) + 5
                              // cast<int>(4.1) + 5
                              // 4 + 5
                              // 9

A cast can convert between any two numeric types and between any two pointer types. A struct value can only be cast to its own type. Casts between pointers and integers and between bool and numeric types are not allowed. To obtain a bool from a number, compare it instead (e.g. x != 0).

sizeof and alignof Operators

The sizeof operator is used to evaluate the size in bytes of a value of a given type. It can be expressed in two forms as follows:

sizeof<type>
sizeof expression

In the first form, the type whose size is evaluated is explicitly named. In the second form, the size of the type of an expression is evaluated. Note that the expression itself is not evaluated; it is only used to specify a type. The expression can also optionally be wrapped in parentheses.

The notation for the alignof operator is the same, but evaluates instead to the memory alignment of the specified type.

uint sizeofDouble = sizeof<double>; // doubles occupy 8 bytes of memory.
sizeofDouble = sizeof(4.0 + 2.0); // equivalent to sizeof<double>.

uint alignofDouble = alignof<double>; // doubles align on 8 byte memory address boundaries.
alignofDouble = alignof(4.0 + 2.0); // equivalent to alignof<double>.
Operator Precedence

Since expressions can contain several operators, there are rules that determine the order in which operators are evaluated. These rules are analogous to the way that multiplication takes precedence over addition in arithmetic. Likewise, parentheses can be used to make the evaluation order explicit. The following table enumerates the precedence and associativity of Bond operators. Note that it includes some operators that will be covered in later sections of this document.

Precedence Operator Description Associativity
1 ++ -- Postfix increment and decrement Left-to-right
() Function call
[] Array subscript
. -> Member access
2 ++ -- Prefix increment and decrement Right-to-left
+ - Unary plus and minus
! ~ Logical and bitwise not
cast<> Type cast
* Pointer dereference
& Address of
sizeof alignof Size and alignment of type
3 * / % Multiplication, division and modulo division Left-to-right
4 + - Addition and subtraction Left-to-right
5 << >> Bit shifting operators Left-to-right
6 < <= > >= Relational operators Left-to-right
7 == != Equality and inequality Left-to-right
8 & Bitwise conjunction Left-to-right
9 ^ Bitwise exclusive disjunction Left-to-right
10 | Bitwise disjunction Left-to-right
11 && Conjunction Left-to-right
12 || Disjunction Left-to-right
13 ?: Ternary operator Right-to-left
14 = Assignment Right-to-left
*= /= %= += -=
<<= >>= &= ^= |=
Compound assignment
15 , Comma Left-to-right

Statements

Expression Statements

Many of the code examples presented so far in this document have been examples of expression statements. An expression statement consists of a single expression terminated with a semicolon. Expression statements only do meaningful work if they have some kind of side effect, such as assigning a value to a variable, or calling a function that in turn has side effects.

a + b;     // Calculates a result which is discarded. Does no meaningful work.
a = b + c; // Performs an addition and assigns the result to the variable a.

Declarative Statements

A declarative statement is used to declare and optionally initialize a variable. In fact, we have already covered declarative statements in some detail in the Variables section, and have seen many examples in the sample code. The only aspect of declarative statements that we have yet to cover is the initialization of aggregate types, however, that will be deferred to the sections on Array Types and Structure Types.

Compound Statements

A compound statement, also known as a block, provides a means to collect several statements together that can be placed wherever a single statement is expected. A block consists of a list of zero or more statements enclosed in curly braces: {}. The statements contained within a block are all executed in order and can be any type of statement, including nested compound statements. Furthermore, any variables declared within a block are local to that block and cease to exist when program execution leaves the scope of the block.

int a = 10;
int b = 20;

// The following is a compound statement that contains two statements.
{
    int c = 30; // Declare a new variable that only exists between {}.
    b = a + c;  // Assign a new value (40) to b declared above.
}

int sum = a + b; // a and b are still 10 and 40 respectively.

// The following would be an error because c no longer exists.
// sum += c;

if Statements

An if statement allows part of a program to be executed conditionally based on the evaluation of a boolean expression. The basic syntax for an if statement is the following, where if and else are reserved keywords:

if (condition_expression)
    true_statement
else
    false_statement

The condition_expression, which must be of type bool, is evaluated and if its result is true, the true_statement is then executed, otherwise the false_statement is executed. Both the true_statement and false_statement are single statements, but they can be compound statements to allow for an arbitrary amount of code to be conditionally executed.

if (x > 10)
    Bond::StdOut()->PrintStr("X is greater than 10.");
else
    Bond::StdOut()->PrintStr("X is less than or equal to 10.");

The else clause, which includes the false_statement, is optional. This example also demonstrates the use of a compound statement.

if (x > 10)
{
    Bond::StdOut()->PrintStr("This is a compound statement.\n");
    Bond::StdOut()->PrintStr("X is greater than 10.");
}
Bond::StdOut()->PrintStr("This statement executes regardless.");

Multiple if statements can be daisy-chained to evaluate several conditions. This is known as a cascaded if statement.

if (x < 10)
    Bond::StdOut()->PrintStr("X is less than 10.");
else if (x < 20)
    Bond::StdOut()->PrintStr("X is greater than or equal to 10 but less than 20.");
else if (x < 30)
    Bond::StdOut()->PrintStr("X is greater than or equal to 20 but less than 30.");
else
    Bond::StdOut()->PrintStr("X is greater than or equal to 30.");

switch Statements

Another type of statement, called the switch statement, can be used to conditionally execute pieces of code. It selects a block of code to execute based on the value of an integral expression. The syntax is as follows, where switch, case, default and break are all reserved keywords:

switch (control_expression)
{
    case value_1:
        statements_1
        break;
    case value_2:
        statements_2
        break;

    ...

    case value_n:
        statements_n
        break;
    default:
        default_statements
        break;
}

To put the switch statement into perspective, it is worth noting that the above switch statement is roughly equivalent in functionality to the following cascaded if statement.

if (control_expression == value_1)
{
    statements_1
}
else if (control_expression == value_2)
{
    statements_2
}

...

else if (control_expression == value_n)
{
    statements_n
}
else
{
    default_statements
}

The switch statement works by evaluating the control_expression once, which must be of an integer type of 32 bits or less (i.e. char, uchar, short, ushort, int, uint or an enumerated type), then comparing the result against the values next to each case label looking for an exact match. The values (value_1, value_2 and so forth) must be integer constant expressions that can be evaluated at compile time, such as literals, constant variables, enumerators or arithmetic on those. Each value may only appear once, and there may be at most one default label. When a match is found, the statements following the matching case label are executed. When a break statement is encountered, execution resumes after the switch statement. If no exact value is matched, the statements following the default label are executed. If no exact match is found and there is no default label, then none of the statements contained within the switch statement are executed and execution resumes after the switch statement.

Unlike C, execution cannot fall through from one group of statements to the next. Each group must end with a jump statement: a break, a return, or a continue if the switch statement is inside a loop. A statement that always jumps, such as an if statement whose two branches both end with a jump statement, is also acceptable. To execute the same statements for several values, place several labels before them.

switch (c)
{
    case 'y':
    case 'Y':
        Bond::StdOut()->PrintStr("Yes.");
        break;
    default:
        Bond::StdOut()->PrintStr("No.");
        break;
}
const int FEET = 1;
const int METERS = 2;

switch (units)
{
    case FEET:
        Bond::StdOut()->PrintStr("Units are in feet.");
        break;
    case METERS:
        Bond::StdOut()->PrintStr("Units are in meters.");
        break;
    default:
        Bond::StdOut()->PrintStr("Units are not recognized.");
        break;
}

So why is there a need for a switch statement when a cascaded if statement is just as capable? The fundamental difference between the two is one of efficiency; the cascaded if statement does a sequential search for a match, whereas the internal machinery of a switch statement can perform either a table lookup or a binary search. The cascaded if statement also requires the Bond compiler to generate a longer stream of instructions.

while Statements

A while statement, also known as a while loop, is a statement similar in structure to an if statement, but rather than conditionally executing a statement once, it repeatedly executes a statement as long as the condition remains true. The syntax is:

while (condition_expression)
    body_statement

The while loop first evaluates the condition_expression, which must be of type bool, and if its result is true, the body_statement is executed. In contrast to the if statement, execution then returns to the top of the loop and the condition is then re-evaluated, and the cycle repeats until the condition_expression is false. For the loop to terminate, the body of the loop should typically have a side effect (e.g. increment a counter) to cause the condition to become false after some number of iterations.

int i = 0;
while (i < 3)
{
    Bond::StdOut()->PrintStr("i is: ")->PrintI(i)->PrintStr("\n");
    ++i;
}
Bond::StdOut()->PrintStr("After the loop, i is: ")->PrintI(i)->PrintStr("\n");

The output from the above snippet of code would be:

i is: 0
i is: 1
i is: 2
After the loop, i is: 3

Another form of the while loop is the do-while loop. It exchanges the order of evaluation of the condition_expression and the body_statement, so that the body is always executed at least once. This form of loop is less common, but useful in cases where something required to evaluate the condition is determined within the body of the loop. The notation for the do-while loop is:

do
    body_statement
while (condition_expression);

for Statements

Another form of iteration statement is the for statement, also called the for loop. The for loop provides a bit of additional syntax to manage the iteration, which is as follows:

for (init_statement; condition_expression; increment_expression)
    body_statement

The above for loop syntax is roughly equivalent to the following while loop:

{
    init_statement;
    while (condition_expression)
    {
        body_statement
        increment_expression;
    }
}

The equivalence is not exact: a continue statement in the body of a for loop still causes the increment_expression to be evaluated before the condition is re-evaluated, whereas in the while loop above it would skip it (see Jump Statements).

All three parts between the parentheses following the for keyword are optional. If the condition_expression is omitted, the loop repeats until it is exited by a jump statement. The init_statement executes first and once only. It can be an expression or a variable declaration. A variable declared there only exists within the for loop. Then, as with the while loop, the condition_expression is evaluated. If it evaluates to true, the body_statement executes. Subsequently, the increment_expression is evaluated; it is expected to have a side effect (e.g. increment the variable declared in the init_statement), since the result of the expression is not used otherwise. Finally, execution returns to the top of the loop and the condition_expression is evaluated again. Once the condition evaluates to false, execution proceeds to the statement following the body of the loop.

The example presented in the while Statements section can be written more concisely as:

for (int i = 0; i < 3; ++i)
{
    Bond::StdOut()->PrintStr("i is: ")->PrintI(i)->PrintStr("\n");
}

Jump Statements

Jump statements transfer execution to another point in the program. There are three of them: break, continue and return. The return statement exits a function and is covered in the Function Definitions section. The break and continue statements can be nested in the body of a loop and allow for greater control over the execution of the loop. The break statement is also used to exit a switch statement.

The break statement allows a loop to be terminated before its exit condition is satisfied. It can be placed anywhere within the body of a loop and is usually contained within an if statement that tests for special conditions.

int a = 0;
int b = 0;
for (int i = 0; i < 10; ++i)
{
    ++a;
    if (i == 7)
    {
        // Exit loop immediately.
        break;
    }
    ++b;
}
Bond::StdOut()->PrintStr("a is: ")->PrintI(a)->PrintStr("\n");
Bond::StdOut()->PrintStr("b is: ")->PrintI(b)->PrintStr("\n");

The output from the above snippet of code would be:

a is: 8
b is: 7

The continue statement allows the remainder of the body of a loop to be skipped. The loop continues to iterate as though execution had reached the end of the body.

int a = 0;
int b = 0;
for (int i = 0; i < 10; ++i)
{
    ++a;
    if ((i % 3) == 0)
    {
        // Skip numbers divisible by 3.
        continue;
    }
    ++b;
}
Bond::StdOut()->PrintStr("a is: ")->PrintI(a)->PrintStr("\n");
Bond::StdOut()->PrintStr("b is: ")->PrintI(b)->PrintStr("\n");

Note that this code hits the continue statement four times when i is equal to 0, 3, 6 and 9, so b is not incremented on those four executions of the loop. The output is:

a is: 10
b is: 6

If a continue or break statement appears within nested loops, its effect applies only to the innermost loop. It is an error to use a continue statement outside the body of a loop or to use a break statement outside the body of a loop or a switch statement.

Since execution never proceeds past a jump statement to the statement that follows it in the same block, any such statement could never be executed. Bond reports unreachable code like this as an error.

for (int i = 0; i < 10; ++i)
{
    break;
    ++a; // Error: unreachable code.
}

Functions

A function is a sequence of statements that perform a specific task or operation, which is defined once but can be called from multiple locations in a program. Functions therefore promote code reuse by not requiring the operation to be reimplemented in all of the locations where it needs to be performed. Functions also help to provide abstraction and to keep code concise. One does not need to be exposed to a function's implementation details when calling it, only its name, inputs and what type of result it outputs need to be known. In Bond, all executable code is encapsulated in functions.

Function Definitions

Thus far we have only seen one example of a function definition in the Hello World program. Now we will take a detailed look at the general form of a function definition:

return_type function_name ( param_type1 param_name1, param_type2 param_name2 … )
{
    statements
}

The first element of a function definition is its return type. When a function performs work and returns a result, the return type specifies the type of value that is returned. If the function does not return a value, then the return type should be specified using the void keyword.

The next element of a function definition is its name. As with variable names, the name must be a valid identifier that is not a reserved keyword. A function's name is used to call it when it is needed to perform work.

Following the name is a parameter list. The parameter list is a comma separated list of variable declarations enclosed in parentheses. Each parameter holds a value passed to the function as one of its inputs. The parameter list may be empty if the function requires no inputs, however, the parentheses are not optional.

The final part of a function definition is the body. The body is a code block enclosed in curly braces, as we saw in the Compound Statements section, that contains all of the statements that perform the function's work.

A return statement consists of the return keyword followed by an expression whose value is returned to the caller. As with assigning a value to a variable, the value must be convertible to the function's return type without a loss of data. In a function that returns a value, every path of execution through the function must end with a return statement, otherwise the compiler reports an error. A function whose return type is void can use a return statement with no expression (return;) to exit early, or simply reach the end of its body.

// A trivial example of a function with no parameters.
double GetThreePointNine()
{
    return 3.9;
}

// A function with a single parameter.
double Square(double x)
{
    return x * x;
}

// A function with two parameters.
double Multiply(double x, double y)
{
    return x * y;
}

// A function with multiple return paths.
double Max(double x, double y)
{
    if (x > y)
    {
        return x;
    }
    return y;
}

Function Calls

While a function definition specifies what a function does, a function call is an expression that invokes the function to perform work. When a function is called, program execution transfers from the call site to the top of the function and continues until the bottom of the function or a return statement is reached. When finished, program execution returns to the call site and the function's return value becomes the value of the function call expression.

In code, a function call consists of the function's name followed by an argument list in parentheses. The argument list is a list of comma separated values that are assigned to the parameters in the function's parameter list. As with assigning a value to variable, the type of a function argument must be compatible with the type of the parameter to which it is assigned. Even if the called function has no parameters, the parentheses are not optional.

double value = GetThreePointNine();
double valueTimesTwo = Multiply(value, 2.0);

Since the values of function arguments are copied into their respective parameters, which are variables local to a function, a function cannot modify the arguments, only its local copies of them. This applies to struct arguments too: the entire struct is copied. Likewise, a struct returned by a function is copied to the caller.

void AssignToParameter(int value)
{
    // This only modifies our local copy of the argument assigned to the parameter named value.
    value = 3;
}

void TryToModifyAFunctionArgument()
{
    int a = 1;
    AssignToParameter(a);

    // b is assigned the value 1 since a was not modified by the function call.
    int b = a;
}

To give functions the ability to modify data, pointers can be passed as function arguments. This topic is covered in the Pointer Types section.

Function calls can appear anywhere that an expression is expected. Functions can call other functions, and can also call themselves in a pattern known as recursion. To end the recursion, recursive functions must test a base case.

int Factorial(int n)
{
    // Test for the base case to end the recursion.
    if (n <= 1)
    {
        return 1;
    }
    // Otherwise, recurse.
    return n * Factorial(n - 1);
}

Data Types

Bond is a statically typed language, so every variable and expression has a type known at compile time. Bond is also strongly typed, meaning that values of one type will not be implicitly converted to another type (e.g. during a variable assignment or when passing an argument to a function) if it could lead to a loss of data. Such conversions require an explicit cast. The rules are described in the Variables section.

We have already seen the primitive types for storing simple values, however, Bond's type system supports several others.

Enumerated Types

An enumeration is a set of named integer constants. Values of an enumeration are represented using the type int, and behave as the int type in every respect (e.g. array indexing, arithmetic operations, relational operations, etc.).

An enumeration is declared using the enum keyword, followed by an identifier to name the type, an open brace, an enumerator list, a closing brace and a semicolon.

enum AnimalSound
{
    Moo,        // Value is 0 since this is the first enumerator.
    Oink,       // Value is 1 since Moo + 1 = 1.
    Baa = 42,   // Value is explicitly specified as 42.
    Quack,      // Value is 43 since Baa + 1 = 43.
    Meow = -1,  // Values can be negative.
    Woof        // Values can be duplicated. This one is also 0.
};

The enumerator list consists of a comma separated list of enumerators. A comma following the last enumerator is allowed, but not required.

An enumerator can be a single identifier. In that case its value is 0 if it is the first enumerator, or it takes the value of the previous enumerator + 1. Alternatively an enumerator can be an identifier followed by an equal sign and a constant expression. In that case it takes on the value of the constant expression, which must be an integer of 32 bits or less that can be evaluated at compile time. The expression may refer to other enumerators and constants.

Enumerated types can be declared at file scope or within a namespace. As in C, the enumerators are not scoped by the name of the enumeration; they are placed directly in the enclosing scope (the file or namespace in which the enumeration is declared). They are therefore referred to as Moo, not AnimalSound::Moo, and their names must not clash with other names in that scope.

We can declare and initialize a variable of an enumerated type as we would a variable of any other type. In this case, we declare the type as AnimalSound, and use the name of any one of the enumerators in the initializer. We could also have used any other expression, such as a function call, that resolves to an AnimalSound.

AnimalSound sound = Moo;

Array Types

An array is a collection of elements of the same type stored contiguously in memory. As noted in the Type Declarations section, one notable difference between Bond and C is that all parts of a type descriptor, including array dimensions, appear before the variable name.

An array type is written by placing the length in square brackets after the element type. The length must be an integer constant expression that can be evaluated at compile time, and it cannot be zero.

int[5] numbers;
float[10] samples;
char[32] buffer;

Multiple variables of the same array type can be declared at once, and each variable will have the same type. In contrast to C, there is no ambiguity here.

int[3] a, b; // Both a and b are of type int[3].

An array can be initialized with a brace-enclosed initializer list. If an initializer list is provided, the array length can be omitted from the type and the compiler will infer the length from the number of elements.

int[5] data = {10, 20, 30, 40, 50};
int[] values = {1, 2, 3}; // Length is inferred as 3.

If the initializer list has fewer elements than the array, the remaining elements are initialized to zero. It is an error for it to have more elements than the array.

int[5] partial = {1, 2}; // Elements are 1, 2, 0, 0, 0.

An array of char can also be initialized with a string literal. The characters of the string, including the terminating null character, are copied into the array, so the array must be long enough to hold them.

char[] name = "Bond";   // Length is inferred as 5, including the null character.
char[16] buffer = "Hi"; // The first 3 characters are 'H', 'i' and '\0'.

An array of structs can also be initialized with an initializer list. Each element is itself a brace-enclosed list or an expression that evaluates to the struct type.

struct Vector2
{
    int x;
    int y;
};

Vector2[3] points = {{1, 2}, {3, 4}, {5, 6}};

Individual elements of an array can be accessed and modified using the index operator []. Array indices start at 0. Thus, for an array of length n, the valid indices are 0 through n - 1. As in C, indices are not checked at runtime, so accessing an element outside of that range leads to undefined behaviour.

int[4] data = {10, 20, 30, 40};
int first = data[0];  // first is 10.
int third = data[2];  // third is 30.
data[1] = 25;         // Modify the second element.

Unlike individual elements, an array as a whole cannot be assigned to. To copy an array, copy its elements one at a time.

int[3] a = {1, 2, 3};
int[3] b;
b = a; // Error: arrays cannot be assigned.

Arrays work naturally with loops for iterating over their elements.

int[5] scores = {85, 92, 78, 95, 88};
int sum = 0;
for (int i = 0; i < 5; ++i)
{
    sum += scores[i];
}

In most expressions, an array is converted to a pointer to its first element. This is what allows an array to be passed to a function that expects a pointer, or assigned to a pointer variable (see Pointer Types). When an array type appears as a parameter type, the compiler converts it to a pointer to the element type. This means that arrays are effectively passed by reference and that the function does not know the length of the array.

int SumArray(int[] values, int count)
{
    int total = 0;
    for (int i = 0; i < count; ++i)
    {
        total += values[i];
    }
    return total;
}

A function cannot return an array type.

Multidimensional Arrays

Multidimensional arrays are created by specifying multiple dimensions in the type descriptor. As with single dimensional arrays, all dimensions appear before the variable name. Only the first (outermost) dimension may be omitted when an initializer list is provided; all subsequent dimensions must have explicit lengths.

int[2][3] matrix = {{1, 2, 3}, {4, 5, 6}};

This declares a 2-by-3 array. The first dimension has 2 elements, each of which is an array of 3 integers. Note that the syntax int[2][3] is equivalent to C's int matrix[2][3].

Elements of a multidimensional array are accessed using successive applications of the index operator.

int[2][3] m = {{1, 2, 3}, {4, 5, 6}};
int val = m[1][2]; // val is 6.
m[0][1] = 20;      // Modify element at row 0, column 1.

Array types with three or more dimensions follow the same pattern.

int[2][3][4] cube;
cube[0][1][2] = 42;

Pointer Types

Pointers are used to store the memory addresses of values rather than values themselves. Pointers are useful for allowing functions to modify values, sharing data without having to copy it, and working with dynamically allocated memory.

Pointer types are written by placing * in the type declaration. For example, int * is a pointer to an int. Two operators are commonly used for working with pointers. The address-of operator, denoted with &, produces the address of a variable. Meanwhile, the dereference operator, denoted with *, allows the data sitting at the memory address to be inspected or modified.

int val = 42;

// Declare a pointer variable and initialize it with a pointer to val.
int *ptr = &val;

// Inspect the value at the address held by ptr.
Bond::StdOut()->PrintStr("*ptr is: ")->PrintI(*ptr)->PrintStr("\n");

// Modify the value the pointer points to.
*ptr = 32;

// Observe that both *ptr and val now have the new value.
Bond::StdOut()->PrintStr("*ptr is: ")->PrintI(*ptr)->PrintStr("\n");
Bond::StdOut()->PrintStr("val is: ")->PrintI(val)->PrintStr("\n");

The output of the above code would be:

*ptr is: 42
*ptr is: 32
val is: 32

Pointers can point to multiple values stored contiguously in memory. An array is converted to a pointer to its first element when assigned to a pointer, as in the example below. Like with arrays, accessing the elements is done with the index operator [], but take care because pointers do not know how many elements they point to. Additionally, integer arithmetic can be performed on pointers. Adding an integer to or subtracting an integer from a pointer with the +, -, += and -= operators moves it by that number of elements, and the ++ and -- operators move it by one element. Subtracting two pointers gives the number of elements between them. The result of that subtraction is a long when Bond runs with 64-bit pointers and an int when it runs with 32-bit pointers, which is why the example below casts it to int.

int[5] data = {10, 20, 30, 40, 50};
int *ptr = data;

Bond::StdOut()->PrintStr("ptr[0] is: ")->PrintI(ptr[0])->PrintStr("\n");
Bond::StdOut()->PrintStr("ptr[2] is: ")->PrintI(ptr[2])->PrintStr("\n");

ptr[2] = 31;

Bond::StdOut()->PrintStr("ptr[2] is: ")->PrintI(ptr[2])->PrintStr("\n");

// Use pointer arithmetic to assign a new value. Equivalent to: ptr[2] = 32;
*(ptr + 2) = 32;
Bond::StdOut()->PrintStr("ptr[2] is: ")->PrintI(ptr[2])->PrintStr("\n");

// Calculate the distance (i.e. number of elements) between two pointers.
int distance = cast<int>(&data[4] - &data[0]);
Bond::StdOut()->PrintStr("distance is: ")->PrintI(distance)->PrintStr("\n");

The code above outputs:

ptr[0] is: 10
ptr[2] is: 30
ptr[2] is: 31
ptr[2] is: 32
distance is: 4

As we saw in the Function Calls section, functions receive copies of their arguments, therefore they cannot modify the original values. This limitation can be overcome by instead passing pointers.

void AssignToParameter(int value)
{
    // This only modifies our local copy of the argument assigned to the parameter named value.
    value = 3;
}

void AssignToPointer(int *ptr)
{
    // This modifies the value pointed to by ptr.
    *ptr = 3;
}

void TryToModifyAFunctionArgument()
{
    int a = 1;
    AssignToParameter(a);

    // Observe that a has not changed. Outputs 1.
    Bond::StdOut()->PrintStr("a is: ")->PrintI(a)->PrintStr("\n");

    AssignToPointer(&a);

    // Observe that a has indeed changed. Outputs 3.
    Bond::StdOut()->PrintStr("a is: ")->PrintI(a)->PrintStr("\n");
}

The const keyword can be applied both to a pointer and to the value it points to, depending on where it is placed. A const before the * applies to the value pointed to, whereas a const after the * applies to the pointer itself.

int a = 1;
int b = 2;

const int *p1 = &a; // Pointer to a const int.
p1 = &b;            // OK: the pointer itself can change.
*p1 = 3;            // Error: the int it points to cannot be modified through it.

int * const p2 = &a; // Const pointer to an int.
*p2 = 3;             // OK: the int it points to can be modified.
p2 = &b;             // Error: the pointer itself cannot change.

A pointer to a non-const value can be implicitly converted to a pointer to a const value of the same type, but not the other way around, since that would allow a const value to be modified. Such a conversion requires an explicit cast.

The type void * is a pointer to a value of unknown type. Any pointer can be implicitly converted to a void * and, as in C (but not C++), a void * can be implicitly converted to any other pointer type. A void * cannot be dereferenced, since the type of the value it points to is unknown. Other conversions between different pointer types require an explicit cast.

Pointers are also useful when the amount of required memory is not known at compile time, so data structures like fixed sized arrays are not satisfactory. What follows is a complete program that interactively prompts the user for a number, dynamically allocates an array of that many integers, fills the array with user supplied data, repeats the stored data and finally deallocates the array. Note that Bond::Allocate returns a void *, which is implicitly converted to an int *. After the memory is freed, the pointer is set to null to indicate that it no longer points to valid memory.

include "io.bond";
include "memory.bond";

int main()
{
    Bond::StdOut()->PrintStr("Enter the number of elements:\n");
    const ulong n = Bond::StdIn()->ScanUL();

    int *array = Bond::Allocate(n * sizeof<int>);

    Bond::StdOut()->PrintStr("Enter ")->PrintUL(n)->PrintStr(" integers:\n");
    for (ulong i = 0; i < n; i++)
    {
        array[i] = Bond::StdIn()->ScanI();
    }

    Bond::StdOut()->PrintStr("You entered:\n");
    for (ulong i = 0; i < n; i++)
    {
        Bond::StdOut()->PrintI(array[i])->PrintStr("\n");
    }

    Bond::Free(array);
    array = null;

    return 0;
}

Before dereferencing a pointer that may be null, compare it with null. As with all conditions, the comparison must be explicit.

int *ptr = FindValue();
if (ptr != null)
{
    Bond::StdOut()->PrintI(*ptr)->PrintStr("\n");
}

Structure Types

A structure type, or struct, is a composite data type that groups related variables together under a single name. Unlike C, Bond structs can also contain member functions, a feature borrowed from C++.

Defining Structures

A struct is declared using the struct keyword followed by a name, a body enclosed in curly braces and a semicolon. The body contains a list of member variable declarations and member function definitions. Member variables and member functions can appear in any order within the body. Structs can be declared at file scope or within a namespace.

struct Point
{
    int x;
    int y;
};

Members can be of any type, including other struct types, pointers and arrays.

struct Line
{
    Point start;
    Point end;
};

A struct cannot contain a member of its own type, either directly or indirectly through other structs, since such a struct would have infinite size. It can, however, contain a pointer to its own type.

struct Node
{
    int value;
    Node *next; // OK: a pointer to a Node.
    // Node child; would be an error.
};

Unlike C++, member variable declarations cannot have initializers. Members are initialized when an instance is initialized, as described next.

Instantiation and Initialization

A variable of a struct type is declared the same way as any other variable. The variable is called an instance of that struct type. Simply declaring a struct variable allocates memory for all of its members on the stack.

Point p;
p.x = 10;
p.y = 20;

A struct instance can also be initialized with a brace-enclosed initializer list. The values in the list are assigned to the members in the order in which they are declared in the struct definition. If the list has fewer values than the struct has member variables, the remaining members are initialized to zero.

Point p = {10, 20};
Line segment = {{0, 0}, {5, 5}};
Point origin = {0}; // origin.y is also 0.

Struct values can be assigned from one variable to another. This copies the entire contents of the struct.

Point a = {3, 4};
Point b;
b = a; // b.x is 3 and b.y is 4.
Member Variables

The members of a struct instance are accessed using the dot operator . on a value of the struct type. When working with a pointer to a struct, the arrow operator -> is used instead. These operators work the same as in C.

Point p = {7, 11};
int px = p.x;     // Access member x by value.

Point *ptr = &p;
int py = ptr->y;  // Access member y through a pointer.
ptr->x = 8;       // Modify member x through a pointer.

Nested structs are accessed by chaining the dot and arrow operators.

Line segment = {{0, 0}, {5, 5}};
int endX = segment.end.x; // endX is 5.
Member Functions

Unlike C, structs in Bond can have member functions defined within the struct body. A member function is defined the same way as a regular function.

struct Rectangle
{
    int width;
    int height;

    int Area()
    {
        return this->width * this->height;
    }
};

A member function is called using the dot operator on an instance or the arrow operator on a pointer, followed by the function name and argument list.

Rectangle rect = {10, 5};
int a = rect.Area(); // a is 50.

Rectangle *rptr = &rect;
int b = rptr->Area(); // b is 50.

A member function can be declared const by placing the const keyword after the parameter list. A const member function cannot modify the member variables of the struct instance on which it is called, nor can it call non-const member functions of that instance. Only const member functions can be called on a const instance or through a pointer to const.

struct Vector3
{
    int x;
    int y;
    int z;

    int Dot(const Vector3 *other) const
    {
        return x * other->x + y * other->y + z * other->z;
    }
};

Member functions are always defined in the body of the struct; there is no separate syntax for defining them outside of it. A member function can call other member functions and access member variables regardless of the order in which they are declared within the struct.

The this Pointer

Inside a member function, the keyword this is a pointer to the struct instance on which the function was called. It can be used to access the instance's members explicitly using the arrow operator ->. The use of this is optional; member variables and member functions of the current instance can also be accessed directly by name.

struct Counter
{
    int count;

    void Increment()
    {
        this->count = this->count + 1; // Explicit use of this.
    }

    int GetCount() const
    {
        return count; // Implicit access without this.
    }
};

In a const member function, this is a pointer to const, so it cannot be used to modify members. Both forms of member access, with and without this, are equivalent.

Namespaces

Namespaces provide a mechanism for organizing code and preventing name collisions between identifiers. They are similar to C++ namespaces. As mentioned in the Notable Differences With C section, Bond has namespaces whereas C does not. All declarations of the Bond Standard Library are in the Bond namespace.

Defining Namespaces

A namespace is declared using the namespace keyword followed by a name and a body enclosed in curly braces. The body can contain any declarations that are allowed at file scope, including constants, functions, structs, enums, native blocks and nested namespaces. Include directives, however, can only appear at file scope.

Code within a namespace can refer to the other symbols of that namespace, and of the namespaces that enclose it, without qualification. In the following example, CircleArea refers to PI directly rather than as Geometry::PI.

namespace Geometry
{
    const double PI = 3.14159;

    double CircleArea(double radius)
    {
        return PI * radius * radius;
    }

    struct Point
    {
        int x;
        int y;
    };
}

Namespaces can be nested to create hierarchies of names.

namespace Graphics
{
    namespace Shapes
    {
        struct Circle
        {
            int x;
            int y;
            int radius;
        };
    }
}

The same namespace can be defined in multiple places. Declarations from each definition are combined into a single namespace.

namespace Util
{
    int Add(int a, int b) { return a + b; }
}

namespace Util
{
    int Subtract(int a, int b) { return a - b; }
}

// Both Add and Subtract are in the Util namespace.

Accessing Namespaced Symbols

Identifiers declared within a namespace are accessed using the scope resolution operator ::. The namespace name appears on the left side of the operator and the identifier on the right.

double area = Geometry::CircleArea(5.0);
Geometry::Point p;
p.x = 10;
p.y = 20;

For nested namespaces, the :: operator is chained to navigate the hierarchy.

Graphics::Shapes::Circle c;
c.x = 100;
c.y = 200;
c.radius = 50;

using Statements

The using keyword provides a way to bring namespaced identifiers into the current scope without having to qualify them with the :: operator. There are two forms of using statements.

A using directive, written as using namespace followed by a namespace name and a semicolon, brings all identifiers from the named namespace into the current scope.

void DrawScene()
{
    using namespace Geometry;

    // PI and CircleArea can now be used without the Geometry:: prefix.
    double area = CircleArea(5.0);
    Point p = {10, 20};
}

A using declaration, written as using followed by a qualified identifier and a semicolon, brings a single specific identifier into the current scope.

void CalculateArea()
{
    using Geometry::CircleArea;
    using Geometry::PI;

    // Only CircleArea and PI are accessible without the prefix.
    double area = CircleArea(5.0);
}

Using statements can only appear within a block scope such as the body of a function or a compound statement. Unlike C++, they cannot appear at file scope or directly within a namespace. The effect of a using statement is limited to the block in which it appears.

void Example()
{
    {
        using namespace Geometry;
        Point p = {1, 2}; // Point is visible here.
    }
    // Point is no longer visible without qualification.
    Geometry::Point q = {3, 4};
}

Using directives can be chained with nested namespaces. Once a namespace has been brought into scope, its child namespaces can be referenced without the parent prefix.

void Example()
{
    using namespace Graphics;
    using namespace Shapes;

    // Circle is accessible without the full Graphics::Shapes:: prefix.
    Circle c;
    c.radius = 10;
}

If using directives bring two different symbols with the same name into scope, an unqualified reference to that name is ambiguous and the compiler reports an error. The symbols can still be referenced by their qualified names.

namespace A { const int VALUE = 1; }
namespace B { const int VALUE = 2; }

int Example()
{
    using namespace A;
    using namespace B;
    // return VALUE;  would be an error since VALUE is ambiguous.
    return A::VALUE + B::VALUE; // OK.
}

Scope-Based Resource Management TODO

Native Blocks TODO