The egg Programming Language Syntaxegg
This document contains details of the syntax of the egg programming language. It is not designed as a resource for learning the language from scratch, but programmers fluent in other computer languages may find it useful as an overview of egg.
Table of Contents

Informal Syntax

This section gives an informal overview of the syntax.

egg Railroad Diagrams

A railroad diagram is a visual representation of a syntax specification. egg uses a particular style of railroad diagram which we describe here. Each rule is enclosed in a cyan box with the rule name on a tab in the top-left. A green track flows into the rule from the left, passes through the rule box and exits to the right. In the example below, the rule "greeting" is defined simply as the token "hello", represented by a purple box:
{ "greeting": {"token": "hello", "inline": false, "left": 1, "right": 1} }
Note that tokens are indivisible and case-sensitive. The above rule would match the text "hello" but not "hell o" or "Hello". We can change the rule to accept a sequence of tokens, optionally separated by whitespace (including newlines):
{ "greeting": {"sequence": [{"token": "hello"}, {"token": "world"}], "inline": false, "left": 1, "right": 1} }
We can also introduce choices by putting junctions in the tracks. The following rule accepts either "hello" or "hi"; but exactly one of them must be specified:
{ "greeting": {"choice": [{"token": "hello"}, {"token": "hi"}], "inline": false} }
In the following rule, "hello" is compulsory, but "world" is optional (however, it must follow "hello"):
{ "greeting": {"sequence": [{"token": "hello"}, {"zeroOrOne": {"token": "world"}}], "inline": false, "left": 1} }
We can introduce repetition by making the tracks loops back on the themselves. The following rule matches "hello" followed by one or more question marks:
{ "greeting": {"sequence": [{"token": "hello"}, {"oneOrMore": {"token": "?"}}], "inline": false, "left": 1, "right": 1} }
By comparison, this rule matches "hello" followed by zero or more question marks:
{ "greeting": {"sequence": [{"token": "hello"}, {"zeroOrMore": {"token": "?"}}], "inline": false, "left": 1, "right": 1} }
Sometimes, the items in the rule are not tokens, but some other kind of terminal, described informally elsewhere. In this case, the terminal is drawn in a blue hexagon:
{ "name": {"oneOrMore": {"terminal": "letter"}, "inline": false, "left": 1, "right": 1} }
Finally, rules can be composed by referring to them within other rules using brown ovals:
{ "greeting-personal": {"sequence": [{"rule": "greeting"}, {"rule": "name"}], "inline": false, "left": 1, "right": 1} }

egg Syntax

egg programs are split up into distinct parts, named "modules". The source code for each module is typically stored in a separate text file.

Comments

Comments in egg source code can either be a single line or multiple lines:
{ "comment": {"choice": [{"sequence": [{"token": "//"}, {"zeroOrMore": {"terminal": "any-character-except-newline"} }, {"terminal": "newline"}]}, {"sequence": [{"token": "/*"}, {"zeroOrMore": {"terminal": "any-character"}}, {"token": "*/"}]}] } }
For example:
// 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.

Modules

Modules contain one or more statements. Each statement may be preceded one or more attributes (attributes).
module

Statements

A statement is one of the following: That is
statement

Simple Statement

A simple statement is an action, a type definition or a variable definition:
statement-simple

Action Statement

An action is an assignment, an increment/decrement or a function call statement:
statement-action

Assignment Statement

Assignments may simply store a value in a target, such as:
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: That is
assignment-target
For example:
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

Increment/Decrement Statement

In egg, increments and decrements are statements (not expressions) and must use the prefix unary operators:
++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
}

Flow Control

Flow control statements affect the order of execution of other statements.

If Statement

Like most computer languages, if statements can have optional else clauses:
statement-if
Note that the curly braces are compulsory, except when an 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

The switch statement acts like a series of if statements:
statement-switch
Unlike some other programming languages, expressions in the 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

The while loop executes the statements within its curly brace block whilst a condition is true:
statement-while
For example:
// 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

The do loop is similar to the while loop, but executes the statements at least once:
statement-do
For example:
// 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

The 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.
statement-for
For example:
// 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.
statement-foreach
For example:
// 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

A break statement prematurely breaks out of a while, do or for loop.
statement-break
For example:
// 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 here
It is also used to break out from a switch statement.

Continue Statement

A continue statement immediately starts the next iteration of a while, do or for loop.
statement-continue
It can also be used in a switch statement to continue execution at the beginning of the next (or first) case/default clause.

Throw Statement

A throw statement raises an exception.
statement-throw
Execution continues at the most recent, active 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

The try statement comes in two flavours. A try statement without a finally clause must have at least one catch clause:
statement-try
The first catch clause, if any, that matches the type of the exception thrown within the try block, is executed.
statement-catch
For example:
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:
statement-try-finally
The 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

For functions defined to return a value, the return statement ends the function, returning the expression to the caller:
statement-return
For example:
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 Statement

In generator functions, yield statements return values to the caller without terminating the function:
statement-yield
For example:
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;
}

Definitions

Definitions introduce new identifiers to the current scope.

Identifiers

There are four kinds of identifiers: identifier-attribute, identifier-property, identifier-type and identifier-variable. They are all synonyms for identifier:
identifier
Identifiers must begin with an ASCII letter ("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: Invalid identifiers include:

Function Definition

Function definitions are divided into four sections:
  1. The return type
  2. The function name (variable identifier)
  3. The parameter list inside parentheses
  4. The statement block inside curly braces
All four sections must be given:
definition-function
The return type is specified as a type expression. The return type 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:
definition-function-parameter
The only permitted default value for optional parameters is 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'

Generator Function Definition

Generator function definitions look like function definitions except they have at least one yield statement within the statement block.

Type Definition

Type definitions introduce new identifiers that are synonymous with an arbitrary type.
definition-type
For example:
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)
  }
};

Variable Definition

Variables can either be defined with a specific type, or can have their type inferred.
definition-variable
If an explicit type is given, but no value, the variable is initially undefined:
int i; // 'i' is undefined
i = 10; // 'i' now has the value ten
If an initial value is give, the expression type should be appropriate:
int i = 10; // 'i' has the value ten
If 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 sixteen
However, 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

Literals

Literals represent fixed values.
literal
The literal value null and the Boolean literal values false and true each have their own keywords.

Integer Literal

Integer literals are presented by a sequence of decimal digits, optionally prefix by a sign.
literal-int
Examples of valid integer literals include 0, 123, -123 and +123. Note that egg only support denary numeric literals; octal and hexadecimal are not supported.

Floating-Point Literal

Floating-point literals are similar but contain a compulsory decimal point followed by at least one digit. There may also be an optional, trailing, signed integer exponent.
literal-float
Examples of valid integer literals include 0.0, -123.45, +1.0e-9 and 0.01e+050.

String Literal

String literals are surrounded by double quote characters. They may contain any valid Unicode character.
literal-string
Special characters within the string may be escaped using backslash sequences:
SequenceInterpretationUnicode
\\Single backslashU+005C
\"Double quoteU+0022
\bBackspaceU+0008
\fForm feedU+000C
\nNewlineU+000A
\rCarriage returnU+000D
\tHorizontal tabU+0009
\vVertical tabU+000B
\0NUL (ASCII 0)U+0000
\U+hhhhh;Unicode character in hexadecimalU+hhhhh
For example, the following would print the Unicode character "U+1F95A EGG" followed by a newline:
print("\U+1f95a;\n");

Object Literal

Object literals are enclosed in curly braces:
literal-object
For example:
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' also
Properties 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 = {};

Array Literal

Array literals are enclosed in square brackets:
literal-array
For example:
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 Literal

Type literals are used in type definitions (type-definition) and in type expressions (type-expressions) where they are preceded by the type keyword:
literal-type
They are similar to object literals, except that the property values may be types and the ellipsis ... syntax is not supported. Type literals make use of well-known properties names. See Type Schema for more information [TODO].

Value Expressions

Value expressions (usually abbreviated to just "expressions") evaluate to values. Values have types; therefore expressions also have types.
expression

Ternary Operator

The ternary operator evaluates a Boolean expression and, depending on the result, returns the value of one of two expressions. For example, the following:
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.

Binary Operator

Binary operators take two operands: the left-hand side and the right-hand size.
expression-binary
The binary operators are defined thus:
PrecedenceBinary
Operator
InterpretationOperand
Types
Result
Type
1 — lowest??Null-coalescingL? RL|R
2||Logical ORboolbool
3&&Logical ANDboolbool
4|Bitwise inclusive ORbool
int
bool
int
5^Bitwise exclusive ORbool
int
bool
int
6&Bitwise ANDbool
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
intint
10+
-
Add
Subtract
int
float
int
float
11 — highest*
/
%
Multiply
Divide
Remainder
int
float
int
float
This is the same precedence order as other "curly brace" programming languages.

Unary Operator

Unary operators take a single operand. In the egg programming language, unary operators always precede the operand:
expression-unary
Therefore, unary operators do not need a precedence order, other than that they are applied before binary operators.
Unary
Operator
InterpretationOperand
Type
Result
Type
*Pointer dereferenceT*T
-Numeric negationint
float
int
float
~Bitwise NOTintint
!Logical NOTboolbool
Unlike many other programming languages, increment ++ and decrement -- are statements; they do not yield values. See increment-decrement-statement.

Primary Expression

A primary expression is one of the following: These may be followed by any number of indexing operations, property lookups or function calls:
expression-primary

Function Expression

Function expressions are like function definitions, but without a function name:
expression-function
For example:
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

Lambda Expression

Lambda expressions are like function expressions but are more concise because types are inferred.
expression-lambda
For example:
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 -1
In 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 -1
If 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 4
Note 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.

Indexing

An expression may be indexed using square brackets []. 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"

Properties

Properties are named values associated with objects. Standard arrays have a read-only property named length which returns the number of elements in the array, as an integer:
var arr = ["a", "b", "c", "d"];
print(arr.length, "\n"); // Prints 4
We 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

Type Expressions

Type expressions are unions of one or more types. Each item in the union is a primary type followed by zero or more suffixes.
type-expression
Primary types are one of the following: That is
type-expression-primary
Note that null is a value whereas type null is a type.

Fundamental Type

There are seven fundamental types:
Fundamental TypePermissible ValuesInclusive Range
voidNone
boolBooleanOnly false or true
intIntegers (64-bit)-263 to 263-1
floatFloating-point numbers (IEEE double 64-bit)About -10308 to 10308
stringStrings of zero or more Unicode code pointsEach code point: U+0 to U+10FFFF
object
type nullNullOnly null
Note that 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");
}

Nullable Type

By default, egg types do not permit 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; // Permitted
To 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?.

Iterator Type

Generator functions (generator-function-definition) are functions that return other functions known as "iterators". Iterators take no parameters and return either 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) {
 // ...
}

Pointer Type

A pointer points to another value. More than one pointer can point to the same value. We can change the value of the pointer or the value the pointer points to. A pointer to a value of type 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 TypeRead AsInterpretation
T*A pointer to a TCan only point to values of type T
T*?A nullable pointer to a TCan be null or point to values of type T
T?*A pointer to a nullable TCan only point to null or values of type T
T?*?A nullable pointer to a nullable TCan be null, point to null or point to values of type T
Pointer types are often used for function parameters to pass parameters by reference:
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.

Indexed Type

An indexed type value can be indexed using square brackets []. 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 elements
Array indices are assumed to be non-negative integers. Generally speaking, we use arrays for contiguous lists of elements that are accessed at random.

Function Type

Functions have types. The type of a function is dependent on the function's return type and parameter types, if any. The type is not dependent on the function name or the names of any parameters. For example:
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 -1
In 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.

Generic Type

The egg language supports simple generic types. These are defined in type definitions (type-definition) but instantiated by supplying the concrete types for the parameters:
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.

Conditions

Conditions are used in flow control statements (flow-control).
condition
They can either be Boolean expressions or guard conditions.

Boolean Condition

A Boolean condition is simply an expression of type 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.

Guard Condition

Guard conditions attempt to introduce a new variable to the scope. The condition passes if the value can be assigned to the new variable; otherwise, it fails. In the following example, a guarded 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.

Function Calls

Function calls can either be statements or expressions.
statement-call
The optional parameter list consists of parameters separated by commas:
parameter-list
Each parameter can be one of the following: Unnamed values and values from an ellipsis array are assigned to parameter variables in the order they appear in the function definition. Named values are then assigned to named parameters. It is invalid for a single function parameter to be assigned more than once (either via unnamed and named parameter assignment or via duplicated names at the call site).

Function Call Statement

A function with a return value of void 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.

Function Call Expression

A function that returns a non-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.25
Functions that return a value but do not have any other side effects are known as pure.

Attributes

Attributes are metadata that can be attached to module statements (including function definitions) or function parameter definitions.
attribute
The attribute name is one or more identifiers separated by full stops. This may be followed by a parameter list inside parentheses. There can be any number of attributes attached to the same statement or parameter. For example:
@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.

egg Syntax Poster

The syntax railroad diagrams above are available as a single poster suitable for printing at A3 size.

Formal Syntax

EBNF

Syntax in extended Backus-Naur form

Railroad

Syntax in Railroad Diagram Generator format