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


Statements are the various logical structures that make up your program.


Statement Terminators

	EndOfLine // not consumed
	'}' // not consumed
	')' // not consumed
	']' // not consumed

Statements in MiniD may be terminated in several different ways. Like other C-style languages, they can be terminated with semicolons. However, they may also be terminated by an end-of-line, or any closing brace. Any statement terminator other than a semicolon is not consumed by the syntactic analysis.

There is a small grammatical ambiguity with C-style syntax when semicolons are removed. Consider the following code:

a = x.f

Now, how should this be parsed? Is it one statement, like "a = x.f(g).h()"? Or is it two statements on two lines? If you write code like this the compiler will give you an error to that effect. This can be fixed in two ways. If you meant for it to be one statement, move at least the paren from the second line to the first:

a = x.f(
g).h() // parsed as one statement

If you meant for it to be two statements, put a semicolon at the end of the first line.

a = x.f; // parsed as first statement
(g).h() // parsed as second statement

Empty statements (a lone ';') are disallowed in MiniD. This is to alleviate a common coding error, in which a statement which has a body is accidentally followed by a semicolon:

// valid C code
while(x > 10); // The "body" is actually this single semicolon!
	// Not the loop body!

This is illegal code in MiniD, since empty statements (the semicolon following the loop header) are disallowed.

Assert Statements

	'assert' '(' Expression [',' Expression] ')' StatementTerminator

This should be familiar to many C-style programmers. An assertion is a runtime consistency check. It checks a condition (the first expression), and if the condition evaluates to false, it throws an exception. If it evaluates to true, it does nothing.

The second expression, if given, is what will be thrown if the assertion fails. This expression is not evaluated unless the first expression evaluates to false. This exception value can be of any type, just like normal exceptions. If no second expression is given, it defaults to the string "Assertion failure at <file>(<line>:<column>)".

Assertions work almost identically to the following code:

	throw message

Except that implementations are highly encouraged to enable or disable assertions with a compiler switch. Disabling an assertion just turns it into a no-op (no code is generated for it).

Import Statements

	'import' [Identifier '='] Identifier {'.' Identifier} [SelectiveImports] StatementTerminator
	'import' [Identifier '='] '(' Expression ')' [SelectiveImports] StatementTerminator

	':' SelectiveImport {',' SelectiveImport}

	[Identifier '='] Identifier

Import statements are how you create dependencies between modules. See Modules for more info on modules and imports.

Unlike in D, an import statement does not simply define a dependency, it actually performs an action. Because of this, import statements may appear anywhere a normal statement may appear, and can be executed conditionally etc. just like any other statement.

There are two main forms of import statements. The first form looks like a D import, in that the module name is given by a list of period-delimited identifiers. The second form looks a bit like a function call. The second form allows you to put any expression between the parentheses. This expression must evaluate to a string value that contains the name of the module to import. In fact, an import of the first form, such as "import a.b.c", is exactly equivalent to an import of the second form using a string literal, such as "import("a.b.c")".

You can rename an import by putting an identifier followed by an equals sign before the name of the module to import, as in "import name =". This is the same as doing "import; local name =".

Import statements can optionally be followed by a list of so-called "selective imports." These are declared as locals in the current scope and are assigned the values of the symbols of the same name in the imported module. You can also rename the selective imports on a per-name basis. "import foo : x = y" is the same as "import foo; local x = foo.y". If you don't rename them, such as in "import foo : x", it's just like doing "import foo; local x = foo.x".

Block Statements

	'{' {Statement} '}'

A block statement is a group of statements enclosed in braces. The statements inside are executed in order. A block statement can be used anywhere a single statement can appear.

Block statements introduce a scope. Any variables declared inside the block are accessible only inside that block and any blocks inside it. Block statements can also exist on their own inside other blocks; they don't have to appear as the statement of a function or control statement.

Expression Statements

	Expression StatementTerminator

You can evaluate expressions by using an expression statement. This is what most of your code will consist of - function calls and assignments, both of which are expressions. It is illegal to have a no-effect expression, such as 4 + 5;. Any extra results of an expression statement are discarded.

Declaration Statements and Decorators

	VariableDeclaration StatementTerminator
	[Decorators] OtherDeclaration
	Decorator {Decorator}

	'@' Identifier {'.' Identifier} ['(' ('with' Expression [',' ExpressionList] | [ExpressionList]) ')']


For information on the kinds of declarations, see Declarations.

Function, class, and namespace declarations may optionally be preceded by decorators. If you've used them in Python, you should be pretty familiar with them already.

Decorators are a way to perform some kind of transformation on a declaration before putting the resulting object into the name that's being declared. You might use them to register objects with some kind of central repository as they're declared, or to add some extra functionality to an object, or convert an object to an entirely different kind of object. The possibilities are up to you.

	foo = 5
class C


attrs is a function declared in the base library that allows you to attach attributes to classes, namespaces, and functions. Attributes are simply tables for your own use; they are not used by the language or runtime at all.

Decorators can only be used on function, class, and namespace declarations. They are really a kind of syntactic sugar for function calls. The above is somewhat similar to:

global C = attrs(class C

}, { foo = 5 })

Notice that the class declaration becomes the first parameter to the decorator function call. This isn't an exact translation of what really goes on, as the class is actually created and inserted into the global, then its members are added, and then it's passed to the decorator and the result goes into the global.

In general, decorators are called last-to-first, and they are called with the next decorator call as their first parameter:

function f() = 5

// is identical to:

global f = foo(bar(baz(function f() = 5)))

Note that if the decorated declaration is local, the local name for the declaration will be inserted before the declaration is evaluated:

local function f() = f // returns itself

// same as
local f
f = foo(function f() = f)

If Statements

	'if' '(' ['local' Identifier '='] Expression ')' Statement ['else' Statement]

With 'if' statements, the expression enclosed in the parentheses (the condition) is evaluated. If it evaluates to true, the statement following the 'if' is run. If the condition is false, and there is an 'else' clause, the 'else' clause is run. If the condition is false and there is no 'else' clause, execution jumps to the statement following the 'if' statement.

You can optionally declare a variable that will be assigned the value of the condition expression. The following code:

if(local x = something)


is similar to:

	local x = something


although it isn't entirely identical, since a variable declared in the condition is not visible in the 'else' block, if any exists.

Both the if body and the else body introduce a scope, so the following code, while not doing much, is legal:

if(x == 4)
	local y
	local z
// Both y and z are inaccessible here; they were defined in nested scopes

While and Do-While Statements

	'while' '(' ['local' Identifier '='] Expression ')' Statement

	'do' Statement 'while' '(' Expression ')'

These are two similar kinds of loops. While loops evaluate the condition. If it is false, execution jumps to the statement after the while statement. If the condition is true, it evaluates its own statement (the "body"), and then tries the condition again. It will keep executing the condition and body until the condition evaluates to false. Because the condition is tested before the loop, there is a chance that the body will never be run; thus, the body of a while statement may execute 0 or more times.

Do-While loops first run the body, and then test the condition. If it evaluates to true, the body is run again, and the condition is checked again. It will continue to run the body and check the condition until the condition evaluates to false, in which case execution will continue at the statement after the do-while statement. Because the body is run before the condition is checked, the body of the do-while statement will execute 1 or more times.

Similarly to how you can declare a local variable in the condition of an if statement, you can do the same thing with while loops.

Both while and do-while statements introduce a scope like if statements. So again, the following code is legal:

	local y

	local z

// Both y and z are inaccessible here

By making the condition always true, the only way to exit either kind of loop is with a break statement.

For Statements

	'for' '(' [ForInitializer] ';' [Expression] ';' [ForIncrement] ')' Statement
	'for' '(' Identifier (':' | ';') Expression '..' Expression [',' Expression] ')' Statement

	ForInitializer ',' ForInitializer

	ForIncrement ',' ForIncrement

There are two types of for loops: the C-style for loop, and the Numeric for loop.

The C-style for loop has four parts to it: the initialization, the condition, the increment, and the body.

The initialization is the first part inside the parentheses. It can be either any number of side-effecting expressions, assignments, or local variable declarations, or nothing at all. Thus, these are all legal:

for(f(); ...
for(i = 0; ...
for(local i = 0; ...
for(local i = 0, local j = 0; ...
for( ;...

The initialization is run once, before the loop begins. If you declare a variable inside the initialization, it will only be accessible within the rest of the loop header and in the body.

The condition is what is used to determine if the loop should continue to run. The condition is checked at the beginning of the loop; thus, like the body of a while loop, the body of a for loop may execute 0 or more times. The condition is optional; by leaving out the condition, the only way to exit the loop is with a break statement.

The increment is any number of expressions evaluated at the end of the loop body, if the loop body is executed. This is commonly used to increment a loop index variable, hence the name. The incremement expressions can be any expression, including assignments.

The overall function of a for loop can be summed up as follows:

	while(condition == true)

Notice the block around the entire thing; this is because any variables declared in the initialization cannot be accessed after the loop.

The second kind of for loop is the Numeric for loop. Very often, you need a for loop to just iterate through a range of integer values. The C-style for loop can look a bit intimidating in this case, and the language implementation can't make any assumptions about your intentions with a for loop, so it can be hard to optimize C-style for loops. Numeric for loops are made to be easier to type and faster to execute, as they have much simpler semantics.

Numeric for loops are a bit like foreach loops, in that they have an index which is an implicitly-declared local. That is, you cannot use variables which were declared before the loop as the loop index. This is to make the implementation simpler, and to prevent against code modifying the loop index inside the loop.

You write a numeric for loop like so:

for(i: 0 .. 10)

// You can also use a semicolon instead of a colon.  This doesn't mean anything different.
for(i; 0 .. 10)

Both of these loops will print out the values 0 through 9. Notice that the low limit is inclusive, but the high limit is noninclusive. This is like slice expressions.

Unlike some other languages MiniD doesn't care whether the first limit is less than the second or not. It will automatically figure out which way the loop is supposed to go, regardless of the ordering of the limits of the loop. Furthermore, the rule that the high limit is noninclusive holds for "backwards" for loops as well. Thus:

for(i: 10 .. 0)

Will print out the values 9 through 0. The reason this rule exists is so that the same limits will produce the same sequence of values, regardless of the order of the limits. This also makes it trivial to iterate through an array forwards or backwards:

local a = [1, 2, 3, 4, 5]

for(i: 0 .. #a)

for(i: #a .. 0)

After the limits, you can write an optional step value. The default step is 1; that is, the loop index will change by 1 every iteration of the loop (regardless of the direction). The step value you specify may not be 0. It can be positive or negative, but the sign doesn't matter; MiniD will figure out which way the loop should go based on the limits, not the step. Notice in the following example, positive 2 is used as the step in both loops, regardless of whether the loop goes forward or back.

local a = [1, 2, 3, 4, 5, 6]

for(i: 0 .. #a, 2)

for(i: #a .. 0, 2)

Foreach Statements

	'foreach' '(' Identifier {',' Identifier} ';' Expression [',' Expression [',' Expression]] ')' Statement

The foreach statement is commonly used to iterate through a container's elements, and perform a piece of code on each element.

All indices are implicitly declared as local, and are only visible inside the loop. This means previously-defined variables cannot be used. This simplifies the implementation of foreach loops.

Foreach loops actually accept three values for the container, though commonly these are provided as return values from an "iterator bootstrap" function, usually in the form of an opApply metamethod. The first container value is the iterator function or thread; the second is the "invariant state," usually the container being iterated, which is passed as 'this' to the iterator; and the third is the "control," usually the current index.

If the first value given in the foreach container is not a function or thread, its opApply is looked up, and if it exists, it is called, retrieving the true iterator and states for iteration. If none exists, it is an error.

If the iterator is a function, each iteration of the loop, the iterator function is called with the invariant state as the 'this' parameter and the control as the first parameter. The iterator function is expected to return the new indices. Iteration ends when the first index that is returned is null. Otherwise, the first index is used as the control value for the next iteration of the loop.

If the iterator is a thread, before iteration begins, the thread must be in the 'initial' state. Each iteration of the loop, the thread is called with the invariant state as the 'this' parameter and the control as the first parameter. The iterator thread is expected to yield the new indices. Iteration ends when the thread enters the dead state. When that happens, any values that may have been returned are not used for a final iteration of the loop.

Operation of the foreach statement could be summed up as:

	local iterator, invState, control = ..  // stuff after the semicolon
	local index1, index2 .. // names before the semicolon

	if(!isFunction(iterator) && !isThread(iterator))
		iterator, invState, control = iterator.opApply(invState)

		if(!isFunction(iterator) && !isThread(iterator))
			throw "Error!"

	if(isThread(iterator) && !iterator.isInitial())
		throw "Error!"

		index1, index2 .. = iterator(with invState, control)

			if(index1 is null)

			control = index1
		else if(iterator.isDead())


Notice that when opApply is called, it is passed the invariant state as a parameter. This allows you to pass values into opApply methods to, for example, choose different methods of iteration. As an example, the standard library provides implementations of opApply for strings and arrays which allow reverse iteration by passing in the string "reverse" as the parameter. For example:

foreach(i, v; [1, 2, 3, 4, 5], "reverse")
	write(v, " ")

This will print out the values "5 4 3 2 1".

There is also a special case when there is only one index given in the foreach loop header. In this case, there will be a hidden index inserted before the written index, which will be unusable, and so the written index will take on the values returned by the iterator function rather than the indices. So the code

foreach(v; [5, 10, 15])

Will print "5 10 15" and not "0 1 2".

Switch Statements

	'switch' '(' Expression ')' '{' CaseStatement {CaseStatement} [DefaultStatement] '}'

	'case' Expression ({',' Expression} | '..' Expression) ':' {Statement}
	'default' ':' {Statement}

Switch statements allow you to choose between multiple possible values of a given expression, and execute different code based on that. This commonly replaces if(x == 1){} else if(x == 2){} else if(x == 3)... style statements.

The switch expression is evaluated. The result of the expression is searched for in the cases inside; if there is a case that has that value, execution continues there. If there is no case that matches the value, the default statement is executed. If there is no match to any case and there is no default statement, an exception will be thrown (as in D).

There are two types of cases: normal cases and ranged cases.

Normal cases just have a single value, or a list of values (which is the same as writing multiple cases with single values). Case values can be either constant or non-constant. When the switch is performed, first all non-constant cases are evaluated and tested against the switched value in the order that they are defined. If the switched value and the non-constant case value are the same type and they compare equal (calling the opCmp metamethod if needed), that case is jumped to. After any and all non-constant cases have been exhausted, it switches among any constant cases and the default case. You can get the best performance, then, by using more constant cases than non-constant cases, and by placing the most commonly-used non-constant cases near the top of the switch statement.

Ranged cases take the form "case lo .. hi:" and allow you to test if the switch value is in the range [lo, hi]. Note that the upper bound is inclusive, unlike in slices. This is more intuitive in cases, since you'll probably be using this to do things like "case '0' .. '9':" or "case 'a' .. 'z':". Ranged cases are evaluated by testing if the switch value is greater-or-equal to the low value and less-or-equal to the high value. If it is, the case is jumped to; otherwise the next case is evaluated. Note that the values may be non-constant, in which case there is nothing stopping you from making the low value higher than the high value, making it possible to make a case that is not possible to execute.

As in C and D, if the end of a case statement is reached, execution will continue with the next lexical case statement; that is, execution will "fall through." Many times, this is not what is intended, and so in order to prevent this, most case statements end with a break statement. Using break inside a switch statement will jump to the statement following the switch statement.

Continue Statements

	'continue' StatementTerminator

Continue statements can only be used inside for, foreach, while, and do-while statements. Using continue will immediately jump to the next iteration of the loop. In the case of for loops, the increment will be executed, the condition will be tested, and if the condition is true, the body will start again. In foreach loops, the iterator function will be called again, and if the first index is non-null, the body will start again. For while loops, it goes back to the condition and tests it again; for do-while loops, it will jump to the condition, test it, and if it is true, will run the body of the loop again.

Continue statements cannot be used inside finally blocks to continue a control structure outside the finally, as continuing may or may not be legal based on the reason the finally block was executed:

		continue // illegal

Break Statements

	'break' StatementTerminator

Break statements can be used inside for, foreach, while, do-while, and switch statements. In the case of the loop statements, execution will jump directly to the statement following the loop statement. No conditions will be tested or increments will be run. In the case of switch statements, execution will also jump directly to the statements following the switch statement. Break statements only break the innermost breakable statement.

Break statements cannot be used inside finally blocks to break a control structure outside the finally, as breaking may or may not be legal based on the reason the finally block was executed:

		break // illegal

Return Statements

	'return' [ExpressionList] StatementTerminator

Return statements are used to exit a function, optionally returning any number of values.

An implicit return is inserted at the end of every function, so it is legal to write

function foo(x)
	if(x > 5)
		return 10

The function will return 10 if x is greater than 5, and null otherwise.

There is no grammatical ambiguity with return statements if they have no values. For example:



This will not be parsed as "return f()", but rather as a no-value return followed by a call to 'f' that's outside the 'if' statement.

Return statements may not appear inside finally blocks. This is because returning may or may not be a valid action depending on the reason that the finally block is being executed.

Tail Calls

When you return the value of a function call (such as in the code "return foo()"), that return-call can be turned into tailcall. A tailcall is a form of call which does not increase the size of the call stack. Since the current function is returning, there's no need to keep its stack frame around; so instead, its stack frame is removed and the stack frame of the function that is being called takes its place. You can use this to perform practically infinite recursion.

For a return statement to qualify as a tailcall, it must meet the following qualifications:

  • returns exactly one value
  • the single return value must be a function call of some kind (normal, method, or super)
  • it must be outside a try or catch block

Here are some examples of tailcalls and not-tailcalls:

return foo() // tailcall
return // tailcall
return 3, foo() // not a tailcall; returning two values
return (foo()) // not a tailcall; have to adjust results to one value after foo() returns

	return foo() // not a tailcall; inside a try block
	return forble() // illegal anyway

Try-Catch-Finally Statements

	'try' Statement (('catch' '(' Identifier ')' Statement) || ('finally' Statement))

MiniD provides an exception handling mechanism similar to the one found in many other languages. You can throw exceptions, catch them, and have finally blocks which run regardless of whether an exception occurred or not.

The syntax used for setting up exception handling frames is almost the same as found in many other C-style languages. You use a try block, which sets up a handling frame. Code inside the try statement may or may not throw an exception. How the try block is left determines what handlers are executed.

Following a try block, there must be one catch statement, one finally statement, or both. In the case of both (a try-catch-finally), it is semantically equivalent to a try-catch nested inside a try-finally. For example:

	// some exception-throwing code
	// the exception is caught and assigned to e.
	// the exception can be handled, and possibly re-thrown.
	// if there is no exception in the try block, this is executed after it.
	// if the catch block catches the exception, this will still be executed after the catch block is finished.

// functionally equivalent to:

		// the try code
		// the catch code
	// the finally code

Inside try, catch, and finally blocks, there are some restrictions. Within try and catch blocks, you cannot perform tailcalls. Calls of the form "return foo()" will perform a normal call of "foo()" followed by returning the values that it returned. If tailcalls were allowed, the exception handling (EH) frames would be able to pile up infinitely deep. Inside finally blocks, break, continue, and return statements are disallowed. See their respective sections for more information.

Finally blocks are executed if their corresponding try block is left in any way. The try block could leave normally; return; break; continue; or throw an exception, and in all cases the finally block will be executed. Nested try blocks are executed in the reverse order of their nesting (most deeply-nested first, least deeply-nested last).

The scope action statements as described below are simply syntactic sugar for try-catch-finally statements.

Throw Statements

Throw statements allow you to create an exception. When the exception is created, the call stack is unwound. Any finally clauses on the stack are executed along the way. This continues until a catch clause is found. At that point, the exception is put into that catch block's variable and execution resumes there.

Any type of value can be thrown in MiniD, although the convention in the standard library is to throw strings:

throw "Invalid size!"

Scope Action Statements

	'scope' '(' ('exit' | 'success' | 'failure') ')' Statement

Sometimes you want to do something upon exiting a scope based on some condition. For example, many times in system APIs you will have to "lock" some resource to gain access to it. Of course, you have to unlock it when you're done so that other code and the system can access it. In a language that has exceptions, if an exception is thrown between the lock and the unlock, the unlock is never called and the resource remains unavailable. This is bad. The typical way to combat this is with a try-finally:


	// do some stuff here which may or may not throw an exception
	resource.unlock() // called regardless

The problem with this is that even in small functions there is a large visual separation between the lock and the unlock. Then you add some code to the try block and it gets really hard to see.

Instead, in this case you can use a scope action statement; in particular, a scope(exit) statement.


	resource.unlock() // called regardless of how this scope exits

// do some stuff here which may or may not throw an exception

This does exactly the same thing as the first snippet, but it's very easy to see that the resource really is being unlocked. In fact, the latter statement is the same as the former. Scope action statements are really just syntactic sugar for more complex try-catch-finally statements.

scope(exit) statements are executed when the scope that contains them is exited in any way (normally, breaking, continuing, returning, or throwing). As was already demonstrated, this is because they're the same as using a try-finally.

scope(success) statements are executed when the scope exits in any way other than by an exception being thrown. Something like this:

scope(success) foo()

is identical to:


	local _succeeded = true

		_succeeded = false
		throw _exception

That is, "foo()" will only be executed if the rest of the statements completed successfully.

scope(failure) is the opposite of scope(success) and will only be executed when the scope is exited by throwing an exception. Something like this:

scope(failure) foo()

is identical to:


	throw _exception

scope(success) and scope(failure) are used less often than scope(exit), but they can still be very useful for things like logging.

Since scope(success) and scope(exit) are implemented in terms of finally blocks, they have the same restrictions as finally blocks: you cannot break, continue, or return from them. scope(failure) can, as it uses a catch block.

Because of the way they're rewritten as try-catch-finally statements, scope action statements will be executed in the reverse order that they were defined.

	writeln$ "bar!"
	writeln$ "foo!"

return 5 // prints "foo!" and then "bar!"

One last thing: if a scope action statement appears as the last statement in a scope, it can be rewritten as a simpler form. That is, something like:

	scope(x) foo()

If 'x' is 'exit' or 'success', the entire scope statement will just be rewritten as "foo()", as the scope will always execute successfully. If 'x' is 'failure', the entire scope statement will be removed, as there is no way for "foo()" to be called; it is dead code.


	AssignmentLHS {',' AssignmentLHS} '=' ExpressionList
	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 '--'

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

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 multiple sources (on the RHS), like so:

x, y, z = 1, 2, 3

The way sources are matched up to their destinations is as follows:

  1. If there is an equal number of sources and destinations, there is no problem; just copy the sources into the corresponding destinations.
  2. If there are more sources than destinations, the additional sources are discarded.
  3. If there are more destinations than sources, the additional destinations are set to null.

A cute idiom that falls out as a consequence of the way multiple assignment works is that a variable swap becomes a single line:

x, y = y, x

Multivalue-returning expressions (which include function calls of any type, varargs, sliced varargs, or the yield expression) also interact with multiple assignment, in that the number of sources is not known until runtime. The behavior is the same, though:

function f() return 2, 3
local x, y, z = 1, f() // x = 1, y = 2, z = 3
x, y = f() // x = 2, y = 3
local w
x, y, z, w = 1, f() // x = 1, y = 2, z = 3, w = null

The order of operations for assignments is as follows:

  1. The expressions on the LHS are evaluated left-to-right.
  2. The expressions on the RHS are evaluated left-to-right.
  3. The RHS is moved into the LHS one value at a time, right-to-left.

A question might come up: what is the output of the following code?

function f() return 5, 1

local a = [1, 2, 3]
local i = 0
a[i], i = f()


Since the RHS is moved into the LHS right-to-left, you might be worried that i is assigned 1 before a[i] is evaluated, causing the output to be "[1, 5, 3]". This is not the case, because the LHS is evaluated before the assignment occurs. So even though i is assigned 1, it is still a[0] which is assigned into, meaning the output is "[5, 2, 3]".

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


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 instance:
    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.