Note: This website is archived. For up-to-date information about D projects and development, please visit wiki.dlang.org.

Expressions

MiniD is a C-style language, and inherits most of the algebraic expressions from the C language family.

Throughout this page, metamethods will be mentioned. Metamethods are methods which are used to overload the built-in functionality of many operations in the language. For information on how to define and use them, see the Metamethods section.

The Comparison Method

Comparison of two values is performed by several operators. Comparison works as follows:

  1. If both sides are numerical types (int or float), return a negative integer if the first is less than the second, a positive integer if the first is greater than the second, or 0 if their difference is 0.
  2. If both sides are the same type:
    1. If both sides are null, return 0.
    2. If both sides are bool, return a negative integer if the first is false and the second true, a positive integer if the first is true and the second false, or 0 if they are the same truth value.
    3. If both sides are char, return a negative integer if the first has a smaller codepoint than the second, a positive integer if the first has a larger codepoint than the second, or 0 if they have the same codepoint.
    4. If both sides are string, return a negative integer if the first compares lexicographically before the second, a positive integer if it's the other way around, and 0 if they are equal in length and data.
    5. If both sides are are tables or both sides are instances, try to call the opCmp metamethod on them.
      1. If opCmp is found in the lhs, call it on that with the rhs as the parameter and return the result.
      2. If not, look for opCmp in the rhs. If found, call it on that with the lhs as the parameter, and return the negation of the result (since the comparison is backwards).
      3. If opCmp is not found in either, throw an error.
    6. For all other types, throw an error.
  3. If the two values are different types:
    1. If the lhs is a table or instance, try to call the opCmp metamethod on it.
      1. If opCmp is found in the lhs, call it on that with the rhs as the parameter and return the result.
      2. Otherwise, throw an error.
    2. If the rhs is a table or instance, try to call the opCmp metamethod on it.
      1. If opCmp is found in the rhs, call it on that with the lhs as the parameter and return the negation of the result (since the comparison is backwards).
      2. Otherwise, throw an error.
    3. Throw an error to the effect that the two types cannot be compared.

The Equality Method

Equality is separated from comparison for two reasons:

  • For some types, comparison (ordering) makes no sense, while equality does.
  • Equality can sometimes be much quicker to calculate than comparison.

The equality method is used by the == and != operators, and works as follows:

  1. If both sides are numerical types (int or float), return true if they are equal, and false otherwise.
  2. If both sides are the same type:
    1. If both sides are null, return true.
    2. If both sides are bool, return true if they are the same truth value, and false otherwise.
    3. If both sides are char, return true if they have the same codepoint, and false otherwise.
    4. If both sides are string, return true if they are equal in length and data, and false otherwise.
    5. If both sides are are tables or instances, try to call the opEquals metamethod on them.
      1. If opEquals is found in the lhs, call it on that with the rhs as the parameter and return the result.
      2. If not, look for opCmp in the rhs. If found, call it on that with the lhs as the parameter, and return the result.
      3. If opCmp is not found in either, throw an error.
    6. For all other types, throw an error.
  3. If the two values are different types:
    1. If the lhs is a table or instance, try to call the opEquals metamethod on it.
      1. If opEquals is found in the lhs, call it on that with the rhs as the parameter and return the result.
      2. Otherwise, throw an error.
    2. If the rhs is a table or instance, try to call the opEquals metamethod on it.
      1. If opEquals is found in the rhs, call it on that with the lhs as the parameter and return the result.
      2. Otherwise, throw an error.
    3. Throw an error to the effect that the two types cannot be compared.

Expressions

Expression:
	ConditionalExpression

Expressions all have a value.

Conditional Expressions

ConditionalExpression:
	OrOrExpression '?' Expression ':' ConditionalExpression

This is called the "conditional operator" or the "ternary operator." It works like an if-else statement, except it's an expression that yields a value. It works like this:

  1. The expression to the left of the '?' is evaluated.
  2. If the value from step 1 is true, the expression immediately after the '?' is evaluated and that value is returned.
  3. Otherwise, the expression after the ':' is evaluated and that value is returned.

Logical Expressions

OrOrExpression:
	AndAndExpression
	OrOrExpression '||' AndAndExpression

AndAndExpression:
	OrExpression
	AndAndExpression '&&' OrExpression

Logical Or (||)

  1. The LHS is evaluated.
  2. If the value of the LHS is true, the RHS is skipped and the value of the LHS is returned.
  3. Otherwise, the RHS is evaluated and returned.

Logical And (&&)

  1. The LHS is evaluated.
  2. If the value of the LHS is false, the RHS is skipped and the value of the LHS is returned.
  3. Otherwise, the RHS is evaluated and returned.

This method of conditionally evaluating the RHS for these operators is known as "short-circuit evaluation."

Bitwise Operators

OrExpression:
	XorExpression
	OrExpression '|' XorExpression

XorExpression:
	AndExpression
	XorExpression '^' AndExpression

AndExpression:
	CompareExpression
	AndExpression '&' CompareExpression

These perform typical bitwise manipulations. For all of these operations, execution works as follows:

  1. If both the LHS and the RHS are integers, the raw operation is performed on them and the result of that operation is returned.
  2. Otherwise, call the appropriate metamethod on the values.

Comparison Expressions

CompareExpression:
	ShiftExpression
	EqualExpression
	RelExpression

All comparison expressions have the same precedence. This makes things like "if(5 < x < 10)", which would not do what one would expect, illegal, although it does leave the possibility open for future syntactic expansion.

Equality and Identity

EqualExpression:
	ShiftExpression '==' ShiftExpression
	ShiftExpression '!=' ShiftExpression
	ShiftExpression 'is' ShiftExpression
	ShiftExpression '!' 'is' ShiftExpression

Equality (==) sees if two values have the same value. It gives the boolean value of the equality method (see the top).

Inequality is simply defined as the inversion of the truth value of equality; that is, it's false if the two objects are equal, and true otherwise.

Identity (is) is defined as two things being fundamentally the same -- same type, same contents. Identity does not use the equality or comparison methods. For non-reference types, this is almost the same as equality, the only exception being that "0 is 0.0" will evaluate to false unlike "0 == 0.0", since 0 and 0.0 are different types. For reference types, this sees if two references refer to the same object, and nothing more. This expression will never throw an error; it will simply return true if the two sides are the same type and value, and false otherwise.

If you want to see if something is null, use "x is null".

Comparison, as, and in

RelExpression:
	ShiftExpression '<' ShiftExpression
	ShiftExpression '<=' ShiftExpression
	ShiftExpression '>' ShiftExpression
	ShiftExpression '>=' ShiftExpression
	ShiftExpression '<=>' ShiftExpression
	ShiftExpression 'as' ShiftExpression
	ShiftExpression 'in' ShiftExpression
	ShiftExpression '!' 'in' ShiftExpression

The comparison operators (<, <=, >, and >=) use the comparison method (see the top), and evaluate to whether the comparison value is op 0. For example, < gets the comparison value, and evaluates to true if it's less than 0, else it evaluates to false.

The three-way comparison operator, <=>, is a bit different from the other comparison operators. It gives you the raw integer comparison value of performing the comparison method (see the top) on the two values. Common uses include when you're overloading opCmp and need to return a comparison value based on the values of some members, or when you're writing a sorting predicate function.

The as expression will attempt to cast the value on the left side to the class type on the right side. It works like this:

  1. If the type of the LHS is not instance, returns null.
  2. Otherwise, if LHS is RHS, returns the LHS.
  3. Otherwise, checks if the RHS appears anywhere in the base class chain of the LHS. If it does, returns the LHS, but if not, returns null.

The in and !in expressions test for the existence of elements in containers. Their results are always booleans. They work as follows:

  1. If the RHS is a string:
    1. If the LHS is a char, the RHS is searched for the character on the LHS, returning true if the character exists anywhere in the string, false otherwise.
    2. If the LHS is a string, the RHS is searched for a substring that matches the LHS, returning true if there is a substring that matches, false otherwise.
    3. Otherwise, an error is thrown.
  2. If the RHS is an array, its values will be searched element-by-element for the value on the LHS, returning true if the value exists in the array and false otherwise.
  3. If the RHS is a table:
    1. If the LHS is null, returns false.
    2. Otherwise, the RHS's keys will be searched for the value on the LHS.
  4. If the RHS is a namespace:
    1. If the LHS is not a string, an error is thrown.
    2. Otherwise, the LHS will be searched for in the keys of the RHS, returning true if such a name exists and false otherwise.
  5. For all other types, the opIn metamethod is attempted on the right hand side with the left hand side as the argument. The result is converted to a boolean, regardless of what type it is, and that boolean is returned.

!in is simply defined as the inversion of the truth value of the in operator.

Bit Shifting

ShiftExpression:
	ShiftExpression '<<' AddExpression
	ShiftExpression '>>' AddExpression
	ShiftExpression '>>>' AddExpression

These shift the operand on the left by the operand on the right. They work as follows:

  1. If both the LHS and the RHS are integers, the raw operation is performed on them and the result of that operation is returned.
  2. Otherwise, call the appropriate metamethod on the values.

There are two right-shift operators. The first, ">>", is signed right shift. This shifts all but the top bit (the sign bit) right, and "smears" the top bit into the new locations. This preserves sign with signed numbers. The other kind, ">>>", is unsigned right shift. This shifts all the bits to the right, and fills in the new locations with 0s.

Addition, Subtraction, and Concatenation

AddExpression:
	MulExpression
	AddExpression + MulExpression
	AddExpression - MulExpression
	AddExpression ~ MulExpression

Addition and subtraction work as follows:

  1. If both the LHS and the RHS are ints:
    1. If the operation is division or modulo, and the rhs is 0, an error is thrown.
    2. Otheriwse, the raw operation is performed on them and the integer result of the operation is returned.
  2. If one side is an int and the other a float, or if both are floats, the raw operation is performed on the values casted to floats and the result of that operation is returned.
  3. Otherwise, attempt the appropriate metamethod on the values.

Concatenation is, generally, the act of putting two lists of things together sequentially into a single list. The semantics of concatenation are somewhat complex, but in general they work as one would expect.

When it comes to concatenation, there are four groups of types, with each group behaving a certain way.

  • The "basic" group includes null, bool, int, float, function, namespace, thread, and nativeobj. These types do not and can not have any meaning for concatenation.
  • The "string" group includes string and char. These can be concatenated together to form strings.
  • The "array" group is just array. In general, concatenation of an array with most other things will yield an array.
  • The "object" group is instance and table. In general, concatenation of an object with something will result in opCat or opCat_r metamethods being called on one object.

Each of the four groups can possibly be concatenated with each of the other four. The behaviors are summed up in the following table. To read the table, choose the row that represents the left hand side of a concatenation, and the column that represents the right hand side. So "basic ~ array" is the cell in the "basic" row, in the "array" column.

basicstringarrayobject
basicErrorError[basic, array...]object.opCat_r(basic)
stringErrorstring1string2[string, array...]object.opCat_r(string)
array[array..., basic][array..., string][array1..., array2...]object.opCat_r(array)
or
[array..., object]
objectobject.opCat(basic)object.opCat(string)object.opCat(array)
or
[object, array...]
object1.opCat(object2)
or
object2.opCat_r(object1)

Some explanation of what the operations in the above table mean:

  • For arrays, something like "[array..., x]" means a new array whose elements are the elements of "array" followed by "x", and similarly for "[x, array...]".
  • In the cells where there is an "or", it means the first operations is attempted, and if that fails (i.e. there is no metamethod), it tries the second.
  • In the cells where there are metamethod calls, if all operations are attempted and fail, an error is thrown.

An implementation may choose to reduce the number of unnecessary temporaries for string and array concatenation by grouping together items that can be concatenated and creating a single string or array out of them. As long as the binary, left-associative semantics of concatenation are preserved, this is a legal optimization.

Multiplication, Division, and Modulo Arithmetic

MulExpression:
	UnaryExpression
	MulExpression '*' UnaryExpression
	MulExpression '/' UnaryExpression
	MulExpression '%' UnaryExpression

For multiplication:

  1. If both the LHS and the RHS are numbers (ints or floats), the raw operation is performed on them and the result of that operation is returned.
  2. Otherwise, call the appropriate metamethod on the values.

For division and modulo:

  1. If both sides are ints:
    1. If the RHS is 0, an error is thrown.
    2. Otherwise, the result of the raw operation performed on the two numbers is returned.
  2. If both sides are floats, or one side is an int and the other a float, the result of the raw operation performed on the two numbers is returned.
  3. Otherwise, call the appropriate metamethod on the values.

Unary Expressions

UnaryExpression:
	PostfixExpression
	'-' UnaryExpression
	'!' UnaryExpression
	'~' UnaryExpression
	'#' UnaryExpression
	'coroutine' UnaryExpression

Negation (-).

Works as follows:

  1. If the operand is an int or a float, returns the negation of that number.
  2. Otherwise, call the opNeg metamethod on the operand.

Negations of numeric literals, such as -4, are translated into negative numeric literals during the semantic pass of compilation.

Logical Not (!). This gives the logical inversion of the truth value of the operand. That is, if the operand evaluates to true, gives false, and vice versa.

Bitwise Complement (~). If the operand is an integer, gives the bitwise complement of it. Otherwise, the opCom metamethod is called on the operand.

Length (#). Sets or gets the length of a value. When used to get the length of a value, works as follows:

  1. If the operand is a table:
    1. If the table defines an opLength metamethod, returns the value of calling that method on the table.
    2. Otherwise, returns the number of key-value pairs in the table.
  2. If the operand is a string, returns the number of characters in the string.
  3. If the operand is an array, returns the number of elements in the array.
  4. For all other types, call the opLength metamethod on the operand.

This operator can also be used to set the length of an object, if it appears as the LHS of an assignment. It works as follows:

  1. If the operand is an array:
    1. If the RHS is an integer >= 0, the array's length is set to that integer.
    2. Otherwise, go to step 2.
  2. Call the opLengthAssign metamethod on the operand with the RHS of the assignment as the parameter.

The "#vararg" special form may not appear on the LHS of an assignment.

coroutine. This takes a function closure (either a script closure or a native closure), and creates a thread object from it. See Functions for information on coroutines.

Postfix Expressions

PostfixExpression:
	PrimaryExpression
	CallExpression
	PostfixExpression '[' ']'
	PostfixExpression '[' Expression ']'
	PostfixExpression '[' [Expression] '..' [Expression] ']'
	PostfixExpression '.' 'super'
	DotExpression

DotExpression:
	PostfixExpression '.' (Identifier | '(' Expression ')')

Index expressions. These allow you to get and set values inside container objects using some kind of key value as the index. There are two kinds of indexing: getting a value and setting a value. Indexing to get a value works as follows:

  1. If the LHS (the value before the indexing brackets) is an array:
    1. If the index is not an integer, an error is thrown.
    2. If the index is less than 0, add the length of the array to it.
    3. If, after the previous step, the index is still less than 0, or if the index is >= the length of the array, an error is thrown.
    4. Return the value in the slot of the array designated by the index.
  2. If the LHS is a string:
    1. If the index is not an integer, an error is thrown.
    2. If the index is less than 0, add the length of the string to it.
    3. If, after the previous step, the index is still less than 0, or if the index is >= the length of the string, an error is thrown.
    4. Return the character at the position in the string designated by the index.
  3. If the LHS is a table:
    1. If the index is null, go to step 4.
    2. Look up the value in the table using the index. If the value is not null, return it.
    3. If the table has an opIndex metamethod, call it on the table with the index as the parameter and return the result of the call.
    4. Otherwise, return null.
  4. Call the opIndex metamethod on the LHS with the index as the parameter and return the result.

Assigning into an indexed expression (index assigning) works as follows:

  1. If the LHS (the value before the indexing brackets) is an array:
    1. If the index is not an integer, an error is thrown.
    2. If the index is less than 0, add the length of the array to it.
    3. If, after the previous step, the index is still less than 0, or if the index is >= the length of the array, an error is thrown.
    4. Assign the value on the RHS of the assignment into the slot of the array designated by the index.
  2. If the LHS is a table:
    1. If the index is null, go to step 3.
    2. Get the value in the table at the index.
    3. If that value is null (that is, it does not exist):
      1. If the table has an opIndexAssign metamethod, call it on the table with the index and value as parameters.
      2. Otherwise, insert the value into the table with the index as the key.
    4. Otherwise:
      1. If the RHS of the assignment is null, remove the key-value pair with the index as the key from the table.
      2. Otherwise, update the existing key-value pair in the table so that the key-value pair's value becomes the RHS of the assignment.
  3. Call the opIndexAssign metamethod on the LHS with the index and the RHS of the assignment as parameters.

Slice expressions. These allow you to get subsets of list-like types. Like indexing, there are two kinds: getting a slice and setting a slice. The indices of a slice expression are both optional. Leaving an index out means "to this end of the container." So leaving the first index out means the slice starts at the first element; leaving the second index out means the slice ends at the last element. Omitted slice indices are replaced with the value null. "a[]" is just syntactic sugar for "a[..]" (which is in turn sugar for "a[null .. null]"). You can also use negative indices to start indexing from the end of the container, so -1 means the last element, -2 means the second-to-last, and so on.

Getting a slice works as follows:

  1. If the LHS (the value before the slicing brackets) is an array:
    1. If both the low index and the high index are null, return the LHS.
    2. If the low index is null, set the low index to 0.
    3. If the low index is not an integer, an error is thrown.
    4. If the low index is less than 0, add the length of the array to it.
    5. If the high index is null, set the high index to the length of the array.
    6. If the high index is not an integer, an error is thrown.
    7. If the high index is less than 0, add the length of the array to it.
    8. If the low index is > the high index, or the low index is < 0, or the low index is > the length of the array, or the high index is < 0, or the high index is > the length of the array, an error is thrown
    9. Return a new array object whose contents are a slice into the data of the original array object from the low index inclusive to the high index noninclusive.
  2. If the LHS is a string:
    1. If both the low index and the high index are null, return the LHS.
    2. If the low index is null, set the low index to 0.
    3. If the low index is not an integer, an error is thrown.
    4. If the low index is less than 0, add the length of the string to it.
    5. If the high index is null, set the high index to the length of the string.
    6. If the high index is not an integer, an error is thrown.
    7. If the high index is less than 0, add the length of the string to it.
    8. If the low index is > the high index, or the low index is < 0, or the low index is > the length of the string, or the high index is < 0, or the high index is > the length of the string, an error is thrown
    9. Return a new string whose contents are the characters from the original string from the low index inclusive to the high index noninclusive.
  3. Call the opSlice metamethod on the LHS with the low index and high index as parameters and return the result.

Array slices always point into the source array's data, so modifying the slice's data will modify the original array's data.

Setting a slice works as follows:

  1. If the LHS (the value before the slicing brackets) is an array:
    1. If both the low index and the high index are null, return the LHS.
    2. If the low index is null, set the low index to 0.
    3. If the low index is not an integer, an error is thrown.
    4. If the low index is less than 0, add the length of the array to it.
    5. If the high index is null, set the high index to the length of the array.
    6. If the high index is not an integer, an error is thrown.
    7. If the high index is less than 0, add the length of the array to it.
    8. If the low index is > the high index, or the low index is < 0, or the low index is > the length of the array, or the high index is < 0, or the high index is > the length of the array, an error is thrown.
    9. If the RHS of the assignment is an array:
      1. If the length of the RHS is not the same as the length of the slice, an error is thrown.
      2. Otherwise, copy the elements from the array on the RHS into the slice of the array on the LHS.
    10. Otherwise, an error is thrown.
  2. Call the opSliceAssign metamethod on the LHS with the low index, high index, and RHS of the assignment as parameters.

Dot-super expressions (a.super). These work as follows:

  1. If the LHS is an instance, returns the class from which it was instantiated.
  2. If the LHS is a class, returns its base class, or null if it has none (only the Object class has that property).
  3. If the LHS is a namespace, returns the parent namespace of that namespace, or null if it has none.
  4. For all other types, an error is thrown.

Field expressions (a.x). Fields are members of classes, instances, namespaces, and tables. Actually, for tables, fields are the same thing as keys, making field access and indexing the same operation, but for namespaces, classes, and instances, field access is a separate operation from indexing. The dot expression syntax where the dot is followed by a parenthesized expression, such as "a.(foo)", is how fields with dynamically-generated names are accessed from an object. In fact, the syntax "a.x" is just syntactic sugar for "a.("x")".

The field name in a field expression must always be a string. An error will be thrown if it is not.

Retrieving the value of a field works as follows:

  1. If the LHS is a table, works the same way as indexing (see indexing step 3). In fact, the opField metamethod is never called on tables, only opIndex.
  2. If the LHS is a class:
    1. Look up the field in the class.
    2. If it exists, return the value stored in the field.
    3. Otherwise, throw an error.
  3. If the LHS is a namespace or instance:
    1. Look up the field in the namespace.
    2. If it exists, return the value stored in the field.
    3. Otherwise, go to step 4.
  4. Call the opField metamethod on the LHS with the field name as the parameter and return the result.

Setting the value of a field works as follows:

  1. If the LHS is a table, works the same way as index assigning (see index assigning step 2). In fact, the opFieldAssign metamethod is never called on tables, only opIndexAssign.
  2. If the LHS is a class, assign the value into the slot of the class's fields namespace named by the name, creating a new slot if necessary.
  3. If the LHS is an instance:
    1. Look up the field in the instance.
    2. If the field does not exist:
      1. If the instance has an opFieldAssign metamethod, call it on the LHS with the field name and the RHS of the assignment as parameters.
      2. Otherwise, create a new slot in the LHS with the given name and give the the value of the RHS of the assignment.
    3. Otherwise, if the field exists but was found in one of the base classes of the LHS, create a new slot in the LHS with the given name and give the the value of the RHS of the assignment.
  4. If the LHS is a namespace, assign the value into the slot of the namespace named by the name, creating a new slot if necessary.
  5. Call the opFieldAssign metamethod on the LHS with the field name and RHS of the assignment as parameters.

Call Expressions

CallExpression:
	PostfixExpression ArgumentsWith
	DotExpression ArgumentsWith
	'super' '.' (Identifier | '(' Expression ')') Arguments

ArgumentsWith:
	Arguments
	'(' 'with' ExpressionList ')'

Arguments:
	'(' [ExpressionList] ')'
	'$' ExpressionList

ExpressionList:
	Expression {',' Expression}

There are three broad kinds of call expressions in MiniD: normal calls, method calls, and super calls. See Functions for more details on things like how the 'this' parameter is determined, and what significance the 'with' keyword has.

Normal calls have anything but a dot expression as their LHS. They work as follows:

  1. If the LHS is a function, call it with the given arguments and return the results.
  2. If the LHS is a thread:
    1. If the thread is in the initial or suspended states, resume it with the given arguments and return the results when it next yields.
    2. Otherwise, an error is thrown.
  3. For all other types, call the opCall metamethod on the LHS with the arguments as parameters and return the results.

Method calls are calls with a dot expression to their left, such as "a.f(x)" or "a.("f")(x)" (the first is sugar for the second). This looks up the method "f" in a, and calls it on a with x as the parameter. Method calls can be intercepted by overloading the opMethod metamethod. Method calls work something like this:

  1. The method is looked up in the LHS.
  2. If the method is found, it is called on the LHS with the given parameters, and the results of the call are returned.
  3. Otherwise, the opMethod metamethod is looked up in the LHS.
  4. If opMethod is found, it is called on the LHS with the method name as the first parameter, followed by the rest of the parameters, and the results of the call are returned.
  5. Otherwise, an error is thrown.

Lastly, there are super calls. If you want to make a call to a base class's implementation of a method, you can use a super call to do so. Super calls are valid expressions anywhere, but when executed, the function in which the super call appears must be running within the context of an instance. That is to say, you can't perform a super call in a "normal" function. When you perform a super call, you are actually looking for the implementation of the given method not in the class of "this", but rather in the base class of a hidden class. When you call a method on a value of type instance, its class is stored as the hidden class. When you perform a supercall, super method lookup starts with the hidden class and traverses the base class links. If/when an implementation of the super method is found, it is called with the current "this" as the new "this", but with the class that holds the implementation of the method as the new hidden class. Then, if that method makes another super call, lookup will start with the hidden class. If lookup always started with "this", double super calls would cause infinite loops.

There are also bits of syntactic sugar for function calls of the three major types, borrowed from Haskell. If you have several function calls in a row, such as "f(g(h(i(x))))", the closing parentheses can really start to stack up and become unreadable. In order to make deeply-nested calls more readable, you can write any function call that takes a single parameter instead as "f$ x", making the above expression become "f$ g$ h$ i$ x". You cannot pass an explicit context with this syntax, but you can pass any number of normal params. It can be used with any type of function call, so "o.f$ x" and "super.f$ x" are also legal.

Primary Expressions

PrimaryExpression:
	Identifier
	'this'
	'null'
	'true'
	'false'
	'vararg'
	IntLiteral
	FloatLiteral
	CharLiteral
	StringLiteral
	':' (Identifier | 'super' | '(' Expression ')')
	'function' [Identifier] FunctionBody
	'\\' Parameters (('->' Expression) | BlockStatement)
	'class' [Identifier] [':' Expression] '{' {ClassMember} '}'
	'(' Expression ')'
	TableCtorExp
	ArrayLiteral
	'namespace' Identifier [':' Expression] '{' {NamespaceMember} '}'
	'yield' '(' [Arguments] ')'

Identifiers. A lone identifier can refer to local variables, local variables declared in enclosing functions, and global variables. This is determined by searching for a local declaration in the function and enclosing functions; if none is found, the identifier is assumed to refer to a global.

this. this is a hidden parameter to every function which represents the context with which the function should operate. For some functions, the this parameter doesn't have much meaning, but for things like class methods, the this parameter is the class or instance on which the function was called. See Functions for more info.

null. null is a type all its own, and it means the absence of any useful type. All variables default to null. If you don't pass enough arguments to a function, the extra arguments are initialized to null. Extra variables on the left-hand side of an assignment are assigned null.

true and false. These are literals of the boolean type. They are the only values the boolean type can hold.

vararg. This is a special expression, only available in variadic functions (see Functions). You can think of vararg as a sort of function which returns multiple values, which correspond to the extra parameters passed to the function. You can use it like so:

function foo(vararg)
{
	local x, y = vararg // x and y will be set to the first two arguments (or null, if there are none)

	// Create an array whose fields are the varargs
	local args = [vararg]

	foreach(i, v; args)
		writefln("arg[{}] = {}", i, v)

	// We can also access the varargs as if they were an array without the need to create one
	for(i: 0 .. #vararg)
		writefln("arg[{}] = {}", i, vararg[i])

	// We can modify varargs just like any other parameter
	if(#vararg > 0)
		vararg[0] = 10

	// We can slice varargs to give a smaller portion of them
	return vararg[0 .. -1]
}

Literals. Integer, float, string, and character literals each correspond to their own types.

Member expressions. Within a class method, members and methods of 'this' must be explicitly accessed through 'this'. However, typing "this." all the time can be tedious, so member expressions are sugar for accessing members of 'this'. You can replace "this." with ':', so something like ":x" is equivalent to "this.x". The same applies to anything that can come after the dot in a dot expression - ":super" is the same as "this.super", and ":(blah)" is the same as "this.(blah)".

Function literals. These expressions return a "closure," or instance of the function. These can then be passed around, returned, called etc.

The first function literal syntax can be given no name (as in "function(){}"), in which case they are given an automatically-generated name by the compiler. You can also put a name before the parentheses to help make error messages easier to read. The body of a function literal can either be a statement or an equals sign followed by an expression. The expression is made to be the return value of the function literal. So writing "function(x) = x * x" is shorthand for "function(x) { return x * x }".

The second function literal syntax is borrowed from Haskell and is just a little more compact. Something like "\x -> x * x" is equivalent to "function(x) = x * x". You can also have multiple parameters by putting a normal parameter list after the backslash, like "\a, b -> a + b". Instead of an arrow followed by an expression, you can also use a block statement as the body of the literal. So "\x { foo(x); return x }" is equivalent to "function(x) { foo(x); return x }". There is no way to name a Haskell-style function literal.

Class literals. You can declare classes inline, much as you can functions. Class literals can be given an optional name, like "class A {}". If no name is given, it will be given an auto-generated name based on its source location. If no base class is given, it defaults to "Object", just like normal class definitions. The result of the class literal is the new class itself.

Parenthesized expressions. You simply use parentheses to change the order of operations. For example, the expression 4 + 5 * 6 will be evaluated using mathematical OOO - that is, multiplication will be done first, and then the addition, giving a result of 34. However, by enclosing the addition in parentheses, such as (4 + 5) * 6, the expression in the parentheses will be evaluated first - giving 9 - and then the multiplication occurs - giving 54. This should be familiar to anyone who knows a bit of simple algebra.

Parenthesized expressions take on a special meaning when they are placed around expressions which give multiple results. In this case, the number of results is fixed to exactly one. So while "vararg" would give a list of all the variadic arguments to a function (which could be 0 or more items), "(vararg)" gives exactly one result, which will be the first variadic argument, or null if none were passed.

Yield expressions. Yield expressions look and work a lot like a function call. They take parameters and can return multiple results. They are used to give up control of the currently-executing thread to the calling thread. For more information on yield expressions, see the Functions section.

Table Constructors

TableCtorExp:
	'{' [TableField {[','] TableField}] '}'
	'{' PlainTableField ForComprehension '}'

TableField:
	Identifier '=' Expression
	PlainTableField
	SimpleFunctionDeclaration

PlainTableField:
	'[' Expression ']' '=' Expression

ForComprehension:
	'for' Identifier {',' Identifier} 'in' Expression [',' Expression [',' Expression]] [IfComprehension] [ForComprehension]
	'for' Identifier 'in' Expression '..' Expression [',' Expression] [IfComprehension] [ForComprehension]

IfComprehension:
	'if' Expression

Table constructors create a new instance of a table and optionally fill them with data. They create a new table every time they are executed. Inside the braces exist the field initializers.

The first form of field allows you to create "members" in the table. This is something like the dot syntax, in that it's just sugar for using a string index. So something like { foo = 5 } means that the value 5 lives at index "foo".

The second form allows you to use any arbitrary expression as the index, by enclosing it in brackets. The first form is just sugar for ["name"] = value.

The third form allows you to use syntactic sugar for declaring a function as a value. Instead of writing

local t = 
{
	f = function()
	{

	}
}

You can write the more natural:

local t = 
{
	function f()
	{

	}
}

Commas between table fields are optional. However be careful not to forget a comma before a bracket table field, since otherwise it will be parsed as an index into the previous value:

// Commas entirely optional in this table
local t = 
{
	function f() { }
	x = 4
	g = function() { }
	y = 5
}

t = { x = 5 ["y"] = 10 } // parses ["y"] as an index into 5 and will give an error on the '='
t = { x = 5, ["y"] = 10 } // parses correctly

Lastly there are table comprehensions. A table comprehension is an expression based on set builder notation which allows you to compactly create and initialize a table based on complex expressions.

A table comprehension is indicated by writing a "plain" table field (the "[k] = v" form), followed by a for comprehension. These comprehensions can be nested arbitrarily deep. An example would do to show how they are actually evaluated:

local t = { x = 5, y = 10, z = 15 }
local rev = { [v] = k for k, v in t }

First we build a table 't' the normal way, by explicitly listing the members. Then we want to build a reverse lookup table that maps from the values in the original table back to the keys that mapped to them. We can do this very concisely using a table comprehension, as shown in the second line. The "for k, v in t" part is a lot like a foreach loop. The first expression in the table comprehension, the "[v] = k" part, is evaluated for each loop of the 'for'. This expression could be somewhat equivalently written:

local rev = {}

foreach(k, v; t)
	rev[v] = k

As mentioned before, comprehensions can be nested arbitrarily deep, as well as including conditional expressions using "if" comprehensions. A more complex example is shown in the array constructor section.

Array Literals

ArrayLiteral:
	'[' [Expression {[','] Expression}] ']'
	'[' Expression ForComprehension ']'

ForComprehension:
	'for' Identifier {',' Identifier} 'in' Expression [',' Expression [',' Expression]] [IfComprehension] [ForComprehension]
	'for' Identifier 'in' Expression '..' Expression [',' Expression] [IfComprehension] [ForComprehension]

IfComprehension:
	'if' Expression

Table constructors create a new instance of a table and optionally fill them with data. They create a new table every time they are executed. Inside the braces exist the field initializers.

Array constructors are similar to table constructors, but (obviously) create arrays instead of tables. Like table constructors, each time an array literal it executed, it creates a new array. The first expression is placed into element 0, the next into 1, and so on. Commas are optional between elements, but again, parsing issues can arise if, for example, you place two sub-array literals without commas:

[[1 2] [3 4]] // gives an error since it thinks the second sub-array is an index
[[1 2], [3 4]] // works as desired

Like table comprehensions, there are also array comprehensions. An array comprehension works very similarly to a table comprehension, but instead of explicitly specifying the index, the value from each iteration is appended onto the end of the array. A simple example:

local a = [x for x in 1 .. 6]
// a now contains [1, 2, 3, 4, 5]

(Note: the array.range function would be faster in this case.)

Note that in the case that the expression is itself an array, the array will be added as an element of the outer array, rather than being appended to the end of it.

local a = [[x, x] for x in 1 .. 6]
// a now contains [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]

This can be used to quickly create multidimensional arrays, such as "[array.new(10) for i in 0 .. 10]", which will create a 10 by 10 two-dimensional array.

Array comprehensions are very powerful and concise, allowing you to write expressions that build lists of complex series of values. For example, a Pythagorean Triple is a group of three integers which satisfy the Pythagorean Theorem, that is, a2 + b2 = c2. You can generate a list of the first n Pythagorean Triples using array comprehensions:

function pyth(n) = [[a, b, c] for a in 1 .. n + 1
                               for b in a .. n + 1
                               for c in b .. n + 1
                               if a * a + b * b == c * c]

Namespace Constructors

	'namespace' Identifier [':' Expression] '{' {NamespaceMember} '}'

NamespaceMember:
	SimpleFunctionDeclaration
	Identifier ['=' Expression] StatementTerminator

Namespace constructors look a lot like class declarations. The Identifier is the name that is given to the namespace. The optional Expression after the colon is the namespace's parent. The parent is only used when a global is looked up in a function that has this namespace as its environment and the global doesn't exist in this namespace. If no expression is given, the namespace's parent defaults to the current environment namespace. Any functions declared within the namespace will have their environments set to the namespace.