Statements

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

Statement:
	ImportStatement
	BlockStatement
	ExpressionStatement
	DeclarationStatement
	IfStatement
	WhileStatement
	DoWhileStatement
	ForStatement
	ForeachStatement
	SwitchStatement
	ContinueStatement
	BreakStatement
	ReturnStatement
	TryCatchStatement
	ThrowStatement

Statement Terminators

StatementTerminator:
	';'
	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
(g).h()

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.

Import Statements

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

SelectiveImports:
	':' SelectiveImport {',' 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 = some.really.long.name". This is the same as doing "import some.really.long.name; local name = some.really.long.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".

Changes from MiniD 1

Import renaming and renamed selective imports have been added.

Block Statements

BlockStatement:
	'{' {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

ExpressionStatement:
	BaseExpression 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 Attribute Tables

DeclarationStatement:
	VariableDeclaration StatementTerminator
	[AttributeTable] OtherDeclaration
	
AttributeTable:
	'</' [TableField {[','] TableField}] '/>'

OtherDeclaration:
	FunctionDeclaration
	ObjectDeclaration
	NamespaceDeclaration

For information on the kinds of declarations, see Declarations.

Function, object, and namespace declarations may optionally be preceded by what's called an attribute table. The table is attached to the object and not to the declaration, thus why you can't put them on normal variable declarations (i.e. you can't attach the table to an integer). Attribute tables are not used by the language in any way. They are simply there to attach some pieces of extra information to an object for your own use. The attribute table is a table, and as such the members in it are declared just like in a table constructor. You can't use table comprehensions in an attribute table.

Here's a simple example of using an attribute table:

</
	docs = "This is some documentation!"
/>
object O
{

}

// you can get the attribute table using the attributesOf function
writeln(attributesOf(O).docs)

Changes from MiniD 1

Attribute tables have been added.

If Statements

IfStatement:
	'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
	if(x)
	{

	}
}

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
else
	local z
	
// Both y and z are inaccessible here; they were defined in nested scopes

Changes from MiniD 1

Added the ability to declare a variable in the condition.

While and Do-While Statements

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

DoWhileStatement:
	'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:

while(false)
	local y

do
	local z
while(false)

// 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.

Changes from MiniD 1

Added the ability to declare a variable in the condition.

For Statements

ForStatement:
	'for' '(' [ForInitializer {',' ForInitializer}] ';' [Expression] ';' [BaseExpression {',' BaseExpression}] ')' Statement
	'for' '(' Identifier (':' | ';') Expression '..' Expression [',' Expression] ')' Statement

ForInitializer:
	BaseExpression
	LocalVarDeclaration

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 expressions or local variable declarations, or nothing at all. Thus, these are all legal:

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

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 condition cannot be an assignment (hence the "Expression" rule).

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:

{
	initializations;
	
	while(condition == true)
	{
		body;
		
		increments;
	}
}

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)
	writefln(i)

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

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)
	writefln(i)

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)
	writefln(a[i])

for(i: #a .. 0)
	writefln(a[i])

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)
	writefln(a[i])

for(i: #a .. 0, 2)
	writefln(a[i])

Changes from MiniD 1

Numeric for loops can now have either a colon or a semicolon after the index name.

Foreach Statements

ForeachStatement:
	'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. The first container value is the true iterator function; the second is the "invariant state," usually the container being iterated; and the third is the "control," usually the index. If the first value given in the foreach container is not a function, its opApply is looked up, and if it exists, it is called, retrieving the true iterator function and states for iteration.

Iteration begins by calling the iterator function with two parameters: the invariant state and the control. The iterator function is expected to return the new indices. Iteration ends when the first index is null. Otherwise, the first index is used as the control for the next iteration of the loop. Operation of the foreach statement could be summed up as:

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

	if(!isFunction(iterFunc))
		iterFunc, invState, control = iterFunc.opApply(invState)

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

	while(index1 !is null)
	{
		body

		index1, index2 .. = iterFunc(with invState, control)
		control = index1
	}
}

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])
	writefln(v);

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

Switch Statements

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

CaseStatement:
	'case' Expression {',' Expression} ':' {Statement}
	
DefaultStatement:
	'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).

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.

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

ContinueStatement:
	'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.

Break Statements

BreakStatement:
	'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.

Return Statements

ReturnStatement:
	'return' [Expression {',' Expression}] 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:

if(something)
	return

f()

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.

Exception Handling Statements

TryCatchStatement:
	'try' Statement (('catch' '(' Identifier ')' Statement) || ('finally' Statement))
	
ThrowStatement:
	'throw' Expression ';'

MiniD provides an exception handling mechanism based on D's. You can throw exceptions, catch them, and even have finally blocks which run regardless of whether an exception occurred or not. Exceptions, as long as they derive (in the D host code) from the MDException class, can be thrown and caught by both native and script code. Any exception thrown in native code which doesn't derive from MDException will not be caught by script code.

First, let's discuss the try-catch-finally statements.

Try-Catch-Finally Statements

These are how you set up exception handling in MiniD. Basically, if an uncaught exception occurs inside the try block, and there is a catch block, the catch block will be executed. The finally block is always executed, whether or not an exception happens or is caught.

Following a try block, there must be one catch statement, one finally statement, or both.

For example:

try
{
	// some exception-throwing code
}
catch(e)
{
	// the exception is caught and assigned to e.
	// the exception can be handled, and possibly re-thrown.
}
finally
{
	// 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.
}

try
{
	// code
}
finally
{
	// this will be executed regardless of whether an exception occurred in the try block or not
}

Now let's look at how exceptions are created.

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!"