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.

Changes from MiniD 1

Several operations which can be overloaded by metamethods are affected by the new metamethod lookup rules. See Metamethods for details.

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 objects, 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 (array, function, namespace, thread, native object), throw an error.
  3. If the two values are different types:
    1. If the lhs is a table or object, 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 object, 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 objects, 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 (array, function, namespace, thread, native object), throw an error.
  3. If the two values are different types:
    1. If the lhs is a table or object, 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 object, 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.

Base Expressions

BaseExpression:
	Assignment
	Expression

An expression is either an assignment or a "normal" expression. Assignments don't have a value, while "normal" expressions do.

Assignments

Assignment:
	AssignmentLHS {',' AssignmentLHS} '=' Expression
	AssignmentLHS '+=' Expression
	AssignmentLHS '-=' Expression
	AssignmentLHS '~=' Expression
	AssignmentLHS '*=' Expression
	AssignmentLHS '/=' Expression
	AssignmentLHS '%=' Expression
	AssignmentLHS '<<=' Expression
	AssignmentLHS '>>=' Expression
	AssignmentLHS '>>>=' Expression
	AssignmentLHS '|=' Expression
	AssignmentLHS '^=' Expression
	AssignmentLHS '&=' Expression
	AssignmentLHS '?=' Expression
	'++' PrimaryExpression
	'--' PrimaryExpression
	PrimaryExpression '++'
	PrimaryExpression '--'

AssignmentLHS:
	Identifier
	// Note - for these, the PostfixExpression must start with Identifier, 'this', or '#'.
	PostfixExpression '[' ']'
	PostfixExpression '[' Expression ']'
	PostfixExpression '[' [Expression] '..' [Expression] ']'
	DotExpression

Assignments in MiniD come in two flavors: regular and multiple.

Regular assignments are what you see in virtually every other imperative language: a single value being assigned into a single destination.

Multiple assignments allow multiple destinations (on the LHS), and must have a "multivalue" expression on the RHS. The following expressions classify as multivalues:

  • Function call (of any variety)
  • vararg
  • Sliced vararg (i.e. "vararg[x .. y]")
  • yield

Functions can return multiple values. Variadic arguments are handled by the vararg expression, and can be sliced similar to arrays. The yield expression has to do with coroutines and can give multiple values that were passed to this coroutine. For more information on these topics, see the Functions section.

Assignment in MiniD always does a simple copy of the type and value of the value(s) on the RHS into the destination(s) on the LHS. Assignment cannot be overloaded and no extra operations (such as implicitly calling functions on the RHS) will be performed.

Operation assignments (op=) - These operators are shortcuts for longer expressions. Instead of writing x = x + 4, you can instead write x += 4. In order for this to be possible, the expression on the left side must be both a valid lvalue, or "place to put a value", and a valid rvalue, or "source of a value". Additionally, the LHS is only evaluated once, saving time for changing the value of a complex LHS. The one exception is for indexing and field access, where the final indexing or field access will be used to retrieve the original value, then the operation assignment will be performed on that temporary value, and finally the indexing or field assignment will be performed to place the value back into the object. For example, the following pieces of code are equivalent in function:

a[x].y += z

and:

local base = a[x] // this part is only evaluated once
local temp = base.y // field access
temp += z // operation assignment
base.y = temp // field assignment

The metamethod names for these are: opAddAssign, opSubAssign, opCatAssign, opMulAssign, opDivAssign, opModAssign, opShlAssign, opShrAssign, opUshrAssign, opOrAssign, opXorAssign, and opAndAssign.

The one operation assignment that stands out is concatenation assignment (~=). It is the only "mutation" operator that can be applied to string objects, although because strings are immutable, it's not really modifying the string, only the reference to it. The other odd thing about the concatenation assignment operator is that if the RHS is a sequence of concatenations (such as "a ~= b ~ c ~ d"), the RHS will be treated as a list of values to be concatenated. That is, "b ~ c ~ d" will not be evaluated as concatenations; they will be appended as a list to "a". This has significance for the opCatAssign metamethod, which takes a variadic list of arguments.

The concatenation assignment operator works as follows:

  1. If the LHS is a string or character:
    1. If all the values on the RHS are strings or characters, replace the LHS with the concatenation of the LHS and the RHS.
    2. Otherwise, throw an error.
  2. If the LHS is an array:
    1. If the RHS is an array, append the elements of the RHS array to the LHS array.
    2. Otherwise, append the single value on the RHS to the end of the LHS array.
  3. If the LHS is a table or object:
    1. Attempt the opCatAssign metamethod with the RHS values as the parameters.
  4. Otherwise, throw an error.

Implementations may append the values to the end of an array (rule 2) in a group in order to minimize the number of memory allocations needed.

Conditional assignment (?=) - This is an operator which will only assign the value on the right hand side to the left hand side if the left hand side currently holds null. So the expression "a ?= b" is semantically equivalent to "if(a is null) a = b", although 'a' is only evaluated once.

Pre- and Post-Increment and Decrement - These operators increment or decrement their target. Because they are assignments, they do not have a value, and therefore cannot be embedded in other expressions. The prefix forms are therefore semantically equivalent to the postfix forms; they only both exist for convenience. Their operation is as follows:

  1. If the target is an integer or a float, the operation is the same as adding or subtracting the integer value 1 to or from the value.
  2. Otherwise, call the opInc or opDec metamethod on the object.

Increment can be overloaded with the opInc metamethod, and decrement with opDec.

Changes from MiniD 1

In MiniD 1, the increment (++) and decrement (--) operators were just sugar for performing an add-assign with the integer value 1 as the RHS. Thus, they could not be overloaded separately. In MiniD 2, they have their own metamethods.

"Normal" Expressions

Expression:
	ConditionalExpression

Normal expressions all have a value. That's it.

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.

Changes from MiniD 1

All comparison expressions have had their precedence made the same.

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

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 object type on the right side. It works like this:

  1. If the type of the LHS is not object, returns null.
  2. Otherwise, if LHS is RHS, returns the LHS.
  3. Otherwise, checks if the RHS appears anywhere in the prototype 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 not a char, an error is thrown.
    2. Otherwise, the string is searched for the character on the LHS, returning true if the character exists anywhere in the string, false otherwise.
  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 object 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.

Changes from MiniD 1

In MiniD 1, the length operator (#) could only be used to get the length of an object, but MiniD 2 allows you to use it to set the length as well.

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 object, returns the prototype of that object, or null if it has none.
  2. If the LHS is a namespace, returns the parent namespace of that namespace, or null if it has none.
  3. For all other types, an error is thrown.

Field expressions (a.x). Fields are members of objects, namespaces, and tables. Actually, for tables, fields are the same thing as keys, making field access and indexing the same operation, but for namespaces and objects, 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 an object:
    1. Look up the field in the object.
    2. If it exists, return the value stored in the field.
    3. Otherwise, go to step 4.
  3. If the LHS is a namespace:
    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 an object:
    1. Look up the field in the object.
    2. If the field does not exist:
      1. If the object 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 prototype objects 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. Otherwise, update the slot in the LHS with the new value from the RHS of the assignment.
  3. 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.
  4. Call the opFieldAssign metamethod on the LHS with the field name and RHS of the assignment as parameters.

Changes from MiniD 1

Indexing (a["x"]) and field access (a.x) have been split into two separate operations for all types except tables, which retain the old behavior of treating field access as indexing with a string key.

The new "a[]" syntax is sugar for "a[..]".

When slicing arrays, there is now only one kind of slice-assigning. Namely, the ability to write "a[..] = 5" ("slice-fill") has been dropped. Experience has shown that having this expression have two possible meanings only causes confusion and bugs. This has been replaced by the array 'fill' method (i.e. "a.fill(5)"). The other form of slice-assigning, called "slice-copy," such as in "a[..] = [1, 2, 3]", has been retained.

".class" has been dropped as there are no longer classes.

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 object'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 object. 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 prototypes of "this", but rather in the prototypes of a hidden object. When you call a method on a value of type object, it is stored as the hidden object. When you perform a supercall, super method lookup starts with the hidden object and traverses the prototype 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 hidden object as the new hidden object. Then, if that method makes another super call, lookup will start with the hidden object. If lookup 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 more than one parameter to a function with this syntax, nor can you pass an explicit context. It can be used with any type of function call, though, so "o.f $ x" and "super.f $ x" are also legal.

Changes from MiniD 1

The function call expressions have been grammatically clarified and partially extended. Now the grammar shows the five possible kinds of calls: method calls, method calls with explicit context, function calls, function calls with explicit context, and super calls.

Super calls have been extended so that they are allowed anywhere, to better suit the more flexible object system. Their validity is no longer checked at compile time, but rather entirely at runtime.

The Haskell-style chained function call syntax has been added.

Primary Expressions

PrimaryExpression:
	Identifier
	'this'
	'null'
	'true'
	'false'
	'vararg'
	IntLiteral
	FloatLiteral
	CharLiteral
	StringLiteral
	':' (Identifier | 'super' | '(' Expression ')')
	'function' [Identifier] FunctionBody
	'\\' (Identifier | 'vararg' | Parameters) '->' Expression
	'object' [Identifier] [':' Expression] '{' {ObjectMember} '}'
	'(' 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 object methods, the this parameter is the object 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. When accessing members and methods of the same object within an object method, they have to be explicitly accessed through the 'this' parameter. 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 parenthesized parameter list after the backslash, like "\(a, b) -> a + b", or you can have it just take varargs, as in "\vararg -> f(vararg)". There is no way to name a Haskell-style function literal.

Object literals. You can declare objects inline, much as you can functions. Object literals can be given an optional name, like "object A {}". If no name is given, it will inherit the name of its parent object. If no parent object is given, it defaults to "Object". The result of the object literal is the new object 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.

Changes from MiniD 1

vararg. Four new "special forms" involving the vararg expression have been added, in order to make it easier and more efficient to use variadic functions.

  • You can get the number of variadic arguments passed to a function with #vararg.
  • You can get a variadic argument by using vararg[i], where i is in the range [0, #vararg).
  • You can also set the value of a variadic argument (they're just like normal parameters, after all) using vararg[i] = value.
  • You can get a slice of the variadic arguments passed to the function with vararg[x .. y]. It works like array slicing, in that negative indices mean "from the end." This expression returns a multivalue, much like the vararg expression itself.

These "special forms" are a breaking change from the way MiniD 1 works, where each of these operations would operate not on the variadic arguments themselves, but rather on the first variadic argument. In order to perform those operations in MiniD 2, simply replace the "vararg" in each of these with "(vararg)": "#(vararg)" gets the length of the first variadic argument, and so on.

Member expressions have been introduced.

Function literals have been affected by a change in the grammar for functions. Note the FunctionBody grammar element, which is now defined as:

FunctionBody:
	Parameters (Statement | '=' Expression)

Notably, the body of a function or function literal may be any statement, and not just a block statement, or it can be the '=' symbol followed by an expression. This is a breaking change from MiniD 1, where "function(x) x * x" is a function literal that returns the square of its argument. In MiniD 2, this would be written "function(x) = x * x". The first form would be caught as an error by the compiler as "x * x*" has no side effects, but something like "function(x) g(x)" would not be, since "g(x)" can exist as a statement, meaning that code that worked before will now break in MiniD 2. When converting MiniD 1 code to MiniD 2, be sure to find all instances of the use of function literals and make sure to insert '=' symbols after the parameter lists.

Haskell-style function literals have also been introduced.

Class literals have been replaced with object literals; their use and grammar are virtually identical.

Super calls have been moved up to the postfix expression 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.

Changes from MiniD 1

Commas between fields have been made optional everywhere.

Table comprehensions have been introduced.

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]

Changes from MiniD 1

Commas between elements have been made optional.

Array comprehensions have been added.

Namespace Constructors

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

NamespaceMember:
	SimpleFunctionDeclaration
	Identifier ['=' Expression] StatementTerminator

Namespace constructors look a lot like object 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.

Changes from MiniD 1

In MiniD 1, functions declared within the namespace would have their environment set to the environment of the currently-executing function. In MiniD 2, functions that you declare inside the namespace have the namespace set as their environment. This means that the "foreach" loop shown below that was necessary in MiniD 1 is no longer needed:

local ns = namespace X { ... };

foreach(k, v; ns)
	if(isFunction(v))
		v.environment(ns);

Secondly, namespaces declared without a parent namespace in MiniD 1 defaulted to having no parent, but in MiniD 2 will now default to having the current environment as the parent namespace.

With these two changes, namespaces should be a lot less tedious to declare and use.