hello
", represented by a purple box:
hello
" but not "hell o
" or "Hello
".
We can change the rule to accept a sequence of tokens, optionally separated by whitespace (including newlines):
hello
" or "hi
"; but exactly one of them must be specified:
hello
" is compulsory, but "world
" is optional (however, it must follow "hello
"):
hello
" followed by one or more question marks:
hello
" followed by zero or more question marks:
// This is a comment until the end of the line /* This is a comment that's split over multiple lines */Comments can appear anywhere where whitespace is acceptable.
target = value;or they may incorporate a binary operator (binary-operator) to modify the target:
target += value;The latter is equivalent to the following (except that the target expression is evaluated only once):
target = target + value;Assignment targets are one of the following:
int a; a = 123; // Assign to variable int* b = &a; *b = 321; // Assign via pointer int[] c = [9, 2, 3]; c[0] = 1; // Assign to indexed element object d = {}; d.field = 321; // Assign to property
++target; --target;The statements above are analogous to:
target = target + 1; target = target - 1;They are included in the language so that classic index-based for-loop idioms can be used:
for (i = 0; i < 100; ++i) { // Loops from 0 to 99 inclusive }
if
statements can have optional else
clauses:
else
clause is immediately followed by another if
statement:
if (condition1) { // Do this only if 'condition1' is true } if (condition2) { // Do this only if 'condition2' is true } else { // Do this only if 'condition2' is false } if (condition3) { // Do this only if 'condition3' is true } else if (condition4) { // Do this only if 'condition3' is false but 'condition4' is true } else { // Do this if both 'condition3' and 'condition4' are false }
switch
statement acts like a series of if
statements:
case
statements need not be constant.
Each case
/default
clause must end in a break
, continue
, return
or throw
statement.
A break
statement breaks out of the switch
statement.
A continue
statement continues execution at the beginning of the next (or first) case
/default
clause.
For example:
switch (greeting) { case "hello": case "hi": english = true; continue; case greetingInLocalLanguage(): print("greeting is ", greeting, "\n"); break; default: throw "unknown greeting"; }
while
loop executes the statements within its curly brace block whilst a condition is true:
// Find the factorial of n var factorial = 1; while (n > 1) { factorial *= n; --n; }We can break out of the loop with a
break
statement or immediately go to the next iteration with continue
.
while (condition1) { if (condition2) { break; } if (condition3) { continue; } doSomething(); // 'continue' jumps to here } // 'break' jumps to here doSomethingAfter();
do
loop is similar to the while
loop, but executes the statements at least once:
// Print 10 to 1 in descending order var i = 10; do { print(i, "\n"); --i; } while (i > 0);We can break out of the loop with a
break
statement or immediately go to the next iteration with continue
.
do { if (condition1) { break; } if (condition2) { continue; } doSomething(); // 'continue' jumps to here } while (condition3); // 'break' jumps to here doSomethingAfter();
for
loop comes in two flavours.
The vanilla for
loop takes an initialization statement, a condition, an advance statement and a block of statements within curly braces.
// Sum the integers up to and including 'n' var sum = 0; for (var i = 1; i <= n; ++i) { sum += i; }The second flavour of the
for
loop is "foreach".
It loops over all the elements in an expression.
// Sum the elements of an array var array = [1, 2, 3, 5, 8, 13, 21]; var sum = 0; for (var i : array) { sum += i; }In both flavours of
for
, we can break out of the loop with a break
statement or go to the next iteration with continue
.
for (var i = 0; i < n; ++i) { if (condition1) { break; } if (condition2) { continue; } doSomething(); // 'continue' jumps to here } // 'break' jumps to here doSomethingAfter();
break
statement prematurely breaks out of a while
, do
or for
loop.
// Sum the elements until the total is greater than twenty var array = [1, 2, 3, 5, 8, 13, 21]; var sum = 0; for (var i : array) { sum += i; if (sum > 20) { break; } } // 'break' jumps to hereIt is also used to break out from a
switch
statement.
continue
statement immediately starts the next iteration of a while
, do
or for
loop.
switch
statement to continue execution at the beginning of the next (or first) case
/default
clause.
throw
statement raises an exception.
catch
clause that matches the type of the expression:
For example:
float safeSqrt(float x) { if (x < 0) { throw "attempt to take the square root of a negative number"; } return math.sqrt(x); } float y; try { y = safeSqrt(-1); } catch (any exception) { print("exception thrown: ", exception); // Handle the problem here }Within a
catch
clause, a throw
statement without an expression re-throws the original exception:
try { y = safeSqrt(-1); } catch (any exception) { print("exception thrown: ", exception); throw; // Re-throw the exception }
try
statement comes in two flavours.
A try
statement without a finally
clause must have at least one catch
clause:
catch
clause, if any, that matches the type of the exception thrown within the try
block, is executed.
try { runSomethingThatMightFail(); } catch (ArithmeticException exception) { reportProblem("arithmetic", exception); } catch (any exception) { reportProblem("something else", exception); }In
try
statements with a finally
clause, catch
clauses are optional:
finally
clause is always executed, whether or not an exception is thrown or caught.
var resource = acquireResource(); try { processResource(resource); } finally { // Guaranteed to be called releaseResource(resource); }
return
statement ends the function, returning the expression to the caller:
double computeSquare(double x) { // Return the square of 'x' return x * x; }For functions with no return value, the
return
statement without an expression is used:
void squareIt(double* p) { // Squares the value pointed to by 'p' *p *= *p; return; }
yield
statements return values to the caller without terminating the function:
int! fibonacci(int a, int b) { // Compute an infinite series of Fibonacci numbers for (;;) { yield a; var c = a + b; a = b; b = c; } }If the
yield ...
form of the statement is used, all values in the expression are yielded, one after another:
any! prefix(any head, any! tail) { // Yield 'head' then all the values in 'tail' yield head; yield ...tail; }
A
" to "Z
" or "a
" to "z
") or an underscore ("_
").
Subsequent characters may be ASCII letters, underscores, digits ("0
" to "9
") or non-ASCII Unicode.
Identifiers are case-sensitive.
Valid identifiers include:
hello
HelloWorld
hello_world
_
_hello
hello123
olá
(trailing accented letters are permitted)123hello
(must not start with a digit)hello world
(must not contain spaces)hello-world
(must not contain ASCII non-alphanumerics such as a hyphen)γειά_σου
(must not start with a non-ASCII character)void
is used for functions that do not return a value.
For example:
void greet() { print("hello\n"); }The function name ("
greet
" in the example above) introduces a new variable to the current scope.
The variable is initialized with a first-class entity that represents the function.
For example:
float square(float x) { return x * x; } print(square.name, "\n"); // Prints 'square' print(square(9), "\n"); // Prints '81'The parameter list contains the types and names of the function parameters, if any, separated by commas:
null
.
For example:
float logarithm(float x, float? base = null) { if (base == null) { return math.ln(x); } return math.ln(x) / math.ln(base); }Two kinds of variadic parameter are supported. Name-based variadic parameters are passed in as an object whose properties are the named parameters supplied by the caller. For example:
float divide(...object vargs) { return vargs.x / vargs.y; } print(divide(x: 3, y: 4, z: 6), "\n"); // Prints '0.75' (z is ignored)In the other kind, index-based variadic parameters are passed in to the function as an array, in the order supplied by the caller. For example:
int sum(...int[] vargs) { int total = 0; for (var element : vargs) { total += element; } return total; } print(sum(3, 1, 4, 1, 5, 9), "\n"); // Prints '23'
yield
statement within the statement block.
type Number = int|float; Number n; // Synonymous with 'int|float n'Generic type definitions can be parametrized by one or more other types:
type Dictionary<K, V> = { properties: { add: bool(K key, V value), remove: bool(K key), find: V?(K key) } };
int i; // 'i' is undefined i = 10; // 'i' now has the value tenIf an initial value is give, the expression type should be appropriate:
int i = 10; // 'i' has the value tenIf the
var
keyword is used, the type is inferred from the expression:
var i = 4; // 'i' is an 'int' with value four var j = i * i; // 'j' is an 'int' with value sixteenHowever, the inferred type is never nullable. To permit null values, use
var?
:
float? root(float x) { if (x < 0) { return null; } return math.sqrt(x); } var x = root(4); // 'x' is a 'float' with value two var? y = root(-4); // 'y' is a 'float?' with value null var z = root(-4); // This causes a runtime error
null
and the Boolean literal values false
and true
each have their own keywords.
0
, 123
, -123
and +123
.
Note that egg only support denary numeric literals; octal and hexadecimal are not supported.
0.0
, -123.45
, +1.0e-9
and 0.01e+050
.
Sequence | Interpretation | Unicode |
---|---|---|
\\ | Single backslash | U+005C |
\" | Double quote | U+0022 |
\b | Backspace | U+0008 |
\f | Form feed | U+000C |
\n | Newline | U+000A |
\r | Carriage return | U+000D |
\t | Horizontal tab | U+0009 |
\v | Vertical tab | U+000B |
\0 | NUL (ASCII 0) | U+0000 |
\U+hhhhh; | Unicode character in hexadecimal | U+hhhhh |
U+1F95A EGG
" followed by a newline:
print("\U+1f95a;\n");
var a = { "well-known": true, x: "hello", y: 123, z: [1, 2, 3] };Double quotes are needed for the first property because 'well-known' is not a valid property identifier (identifiers). The ellipsis
...
syntax allows us to copy all the properties from another object.
Note that in
var a = { x: "hello", y: 123, z: [1, 2, 3] }; var b = a; var c = { ...a };the resulting values of
b
and c
are subtly different.
This is because a
and b
refer to the same object, whereas the properties of a
are duplicated in the new object c
:
c.y = 321; // Do not affect 'a.y' b.y = 321; // Affects 'a.y' alsoProperties are assigned to the new object in the order they appear in the list. For example:
var a = { x: 1, y: 2, z: 3 }; var b = { ...a, y: 4 }; // 'b' equals '{ x: 1, y: 4, z: 3 }' var c = { y: 4, ...a }; // 'c' equals '{ x: 1, y: 2, z: 3 }'Object literals may be empty to denote an object that has no (initial) properties:
var e = {};
var a = [ true, // Element 0, a Boolean "hello", // Element 1, a string 123, // Element 2, an integer [1, 2, 3] // Element 4, another array ];Array elements may be heterogeneous (as in the case of
a
above) or homogeneous (all the elements have the same type):
int[] b = [1, 2, 3];The ellipsis
...
syntax allows us to copy all the elements from another array at that location:
var a = ["hello", 123, [1, 2, 3]]; var b = [true, ...a, 3.14159]; // Equals [true, "hello", 123, [1, 2, 3], 3.14159]It can be used to create a shallow copy of another array:
var c = [...a]; // Shallow copies 'a' into 'c'Array literals may be empty to denote an array that has no (initial) elements:
var e = [];
type
keyword:
...
syntax is not supported.
Type literals make use of well-known properties names.
See Type Schema for more information [TODO].
var w = x ? y : z;is analogous to
any? w; if (x) { w = y; } else { w = z; }Note that expression
y
is only evaluated if x
is true and expression z
is only evaluated if x
is false.
The expression x
must evaluate to a Boolean.
The potential type of w
is the union of the types of y
and z
.
Precedence | Binary Operator | Interpretation | Operand Types | Result Type |
---|---|---|---|---|
1 — lowest | ?? | Null-coalescing | L? R | L|R |
2 | || | Logical OR | bool | bool |
3 | && | Logical AND | bool | bool |
4 | | | Bitwise inclusive OR | bool int | bool int |
5 | ^ | Bitwise exclusive OR | bool int | bool int |
6 | & | Bitwise AND | bool int | bool int |
7 | == != | Equality Inequality | any? | bool |
8 | < > <= >= | Less than Greater than Less than or equal to Greater than or equal to | int float | bool |
9 | << >> >>> | Shift left Shift right Shift right unsigned | int | int |
10 | + - | Add Subtract | int float | int float |
11 — highest | * / % | Multiply Divide Remainder | int float | int float |
Unary Operator | Interpretation | Operand Type | Result Type |
---|---|---|---|
* | Pointer dereference | T* | T |
- | Numeric negation | int float | int float |
~ | Bitwise NOT | int | int |
! | Logical NOT | bool | bool |
++
and decrement --
are statements; they do not yield values.
See increment-decrement-statement.
()
.var func = int(int a, int b) { return a + b; }; print(func(2, 3), "\n"); // Prints 5 func = int(int a, int b) { return a - b; }; print(func(2, 3), "\n"); // Prints -1
int applyBinary(int(int a, int b) func, int x, int y) { return func(x, y); } var p = applyBinary((a, b) => { return a + b; }, 2, 3); print(p, "\n"); // Prints 5 var q = applyBinary((a, b) => { return a - b; }, 2, 3); print(q, "\n"); // Prints -1In the example above, because the bodies of the lambda expressions only contain
return
statements, we can use the shorter form:
var p = applyBinary((a, b) => a + b, 2, 3); print(p, "\n"); // Prints 5 var q = applyBinary((a, b) => a - b, 2, 3); print(q, "\n"); // Prints -1If there is only one parameter to the lambda, an even shorter form can be used:
int applyUnary(int(int a) func, int x) { return func(x); } var p = applyUnary(a => -a, 2); print(p, "\n"); // Prints -2 var q = applyUnary(a => a * a, 2); print(q, "\n"); // Prints 4Note that lambda expressions may only be used when the parameter types can be trivially inferred. At other times, the full function expression syntax must be used.
[]
.
The indexing expression may be of any type.
Indexes for standard arrays are integers.
By convention, array indices start at zero.
var arr = ["first", "second", "third", "fourth"]; print(arr[1], "\n"); // Prints "second"Indexes for standard objects are strings representing the property name:
var obj = { forename: "Charlie", surname: "Chaplin", year: 1889 }; print(obj["surname"], "\n"); // Prints "Chaplin"
length
which returns the number of elements in the array, as an integer:
var arr = ["a", "b", "c", "d"]; print(arr.length, "\n"); // Prints 4We can only access properties via the "dot" notation if those properties have names that conform to the identifier naming conventions (identifiers).
var obj = { forename: "Charlie", surname: "Chaplin", "year-of-birth": 1889 }; print(obj.forename, "\n"); // Prints "Charlie" print(obj["surname"], "\n"); // Prints "Chaplin". Could also use 'obj.surname' print(obj["year-of-birth"], "\n"); // Prints 1889. Cannot use "dot" notation
()
.null
is a value whereas type null
is a type.
Fundamental Type | Permissible Values | Inclusive Range |
---|---|---|
void | None | |
bool | Boolean | Only false or true |
int | Integers (64-bit) | -263 to 263-1 |
float | Floating-point numbers (IEEE double 64-bit) | About -10308 to 10308 |
string | Strings of zero or more Unicode code points | Each code point: U+0 to U+10FFFF |
object | ||
type null | Null | Only null |
void
cannot be used as the type of a variable or function parameter, but may be used as the return type of a function:
void greet(string name) { print("hello ", name, "\n"); }In addition to the fundamental types,
any
is synonymous with bool|int|float|string|object
:
void display(any value) { print("The value is ", value, "\n"); }
null
values.
For example:
string name; name = null; // Not permitted!To permit values to be nullable, a question mark
?
is appended to the type:
string? name; name = null; // PermittedTo clarify, the following two types are equivalent:
type NullableString1 = string?; type NullableString2 = string|type null;This even extends to
any
; to permit null
to be stored, use any?
.
void
or some type, say T
.
An iterator therefore looks like the following:
(T|void) someIterator() { // ... }A generator function therefore looks like:
(T|void)() someGenerator(any someParameter1, any someParameter2) { // ... }If a function takes an iterator as a parameter, it could look like this:
(T|void)() someFunction((T|void)() someInputIterator) { // ... }To simplify the syntax, the
!
suffix can be applied to a type T
to obtain the appropriate iterator:
T! someGenerator() { // ... } T! someFunction(T! someInputIterator) { // ... }
T
has a type T*
.
For example:
string comedian = "Laurel"; string* pointer = &comedian; // 'pointer' now points to variable 'comedian' *pointer = "Hardy"; // 'comedian' now contains "Hardy" string swashbuckler = "Flynn"; pointer = &swashbuckler; // 'pointer' now points to variable 'swashbuckler'Unlike some other languages, egg does not support pointer arithmetic. By default, pointers cannot be
null
unless their type explicitly uses the ?
suffix:
int i = 123; int*? p = &i; // 'p' can point to an integer or be null p = null;There is a subtle interaction between pointers and nullability:
Pointer Type | Read As | Interpretation |
---|---|---|
T* | A pointer to a T | Can only point to values of type T |
T*? | A nullable pointer to a T | Can be null or point to values of type T |
T?* | A pointer to a nullable T | Can only point to null or values of type T |
T?*? | A nullable pointer to a nullable T | Can be null, point to null or point to values of type T |
int totalPositives(float[] input, float* output) { int count = 0; *output = 0.0; for (var value : input) { if (value > 0) { *output += value; ++count; } } return count; }Pointers to pointers are valid.
[]
.
There are two kinds: maps and arrays.
Maps have an explicit index type and element type:
int[string] lookup1 = createLookup1(); // 'lookup1' maps strings to integers var value1 = lookup1["key"]; // 'value1' is of type 'int' if "key" is in 'lookup1'Arrays omit the index type:
string[] lookup2 = createLookup2(); // 'lookup2' is an array of strings var value2 = lookup2[3]; // 'value2' is of type 'string' if the array has at least four elementsArray indices are assumed to be non-negative integers. Generally speaking, we use arrays for contiguous lists of elements that are accessed at random.
type BinaryFunction = float(float, float); BinaryFunction add = (a, b) => a + b; print(add(3, 4), "\n"); // Prints 7 BinaryFunction sub = (a, b) => a - b; print(sub(3, 4), "\n"); // Prints -1In the example above,
BinaryFunction
is a type for a function that takes two floats and returns a float.
add
is an instance of that type that adds the two numbers.
sub
is an instance of BinaryFunction
that subtracts the two numbers.
Note that we must explicitly supply the type of add
and sub
when defining them so that the lambda parameter types can be trivially inferred.
See lambda-expression.
type Dictionary<K, V> = { properties: { add: bool(K key, V value), remove: bool(K key), find: V?(K key) } }; Dictionary<int, string> dict = createDictionary(); dict.add(8, "eight");The example above defines a dictionary generic type which takes two type parameters: the type of the dictionary key
K
and the type of the dictionary value V
.
The variable dict
is then introduced as a dictionary with the keys being integers and the values being strings.
bool
.
For example:
if (a < b) { print("a is smaller\n") } else { print("a is not smaller\n") }Boolean conditions must be explicitly
bool
; not implicit type conversions are performed.
if
statement is used to determine if the value is an integer (or a float that can be exactly converted):
int? toInteger(any? value) { if (int i = value) { return i; } return null; }Guard conditions are particularly useful when used in conjunction with iterators (iterator-type):
any?! takeAlternate(any?! input) { while (any? a = input()) yield a; if (any? _ = input()) { // Do not yield the element } else { break; } } }The scope of a guard variable does not extend to the
else
clause of a guarded if
statement.
For example:
if (var x = someFunction()) { // 'x' is defined here } else { // 'x' is not defined here }Guard conditions can only be used in
if
, for
and while
statements.
...
) to be used in sequence, orvoid
can be invoked via a call statement:
For example:
void greet(string name) { print("hello ", name, "\n"); } greet("world"); // Prints "hello world"The compiler may report a warning if a function call expression is used as a statement, thereby discarding the return value.
void
value, may be used as an expression.
For example:
float square(float x) { return x * x; } var s = square(1.5); print(s, "\n"); // Prints 2.25Functions that return a value but do not have any other side effects are known as pure.
@test(timeout: 0.001) void testFibonacci() { assert(fibonacci(8) == 21); }Attributes do not generally affect the functionality of a program but can be used by other subsystems for testing, documentation, static analysis, etc. In this example, the attribute may mark the function as a unit test and specifies a timeout value for a test runner to use.