Statements
Statements are the various logical structures that make up your program. If you think of a program as a list of step-by-step instructions, statements are the individual steps.
Statements can be classified into three groups: assignments, flow control, and "other".
Assignments
There are four classes of assignments in MiniD: "normal" assignment, operation (or augmentation) assignment, increment/decrement, and conditional assignment.
Something that should be said for all kinds of assignment: in MiniD, assignments are statements and not expressions. This means that assignments do not have a value and cannot be used for such shenanigans as:
x[y = f() % 2] = q++
You will have to split up horrid things like this into multiple statements.
Normal Assignments
Normal assignments are what you find in most other imperative languages. They take a value on the right-hand side and copy it into the destination on the left-hand side.
>>> global x >>> x = 5 >>> x => 5 >>>
Because MiniD is a dynamically-typed language and only values have types, the type of a variable is dependent upon what values you assign into it:
>>> typeof(x) => "int" >>> x = "hello" >>> typeof(x) => "string" >>>
In addition to normal assignment, MiniD also allows multiple assignment in certain cases. That is, you can write several, comma-separated destinations on the left-hand side of an assignment as long as the right-hand side can give multiple values. There are four kinds of expressions that can give multiple values: function calls, vararg, sliced vararg, and the yield expression. Of those four, the only one you're familiar with right now are function calls, so let's use that as an example.
Let's create a function that returns multiple values:
>>> function foo() return 1, 2, 3 >>>
And when we call it:
>>> foo() => 1, 2, 3 >>>
Now, let's assign it into multiple destinations:
>>> global y >>> x, y = foo() >>> x => 1 >>> y => 2 >>>
Notice something: foo returned 3 values, but we only gave it two destinations. What happened is that the third value was discarded.
The opposite scenario also behaves nicely:
>>> function bar() = 1 >>> x, y = bar() >>> x => 1 >>> y => null >>>
This time, the function bar only returned one value but we assigned it into two variables. No problem; y was simply initialized to null.
That's really all there is to say about normal assignment, so let's move on to..
Operation Assignment
Familiar to most C-style programmers, this is a class of assignment operators which use the destination as a source as well. These operators are formed by conjoining a binary operator with the assignment operator. For example, say you wanted to add a number to a variable. You could write it the long way:
>>> global x = 5 >>> x = x + 6 >>> x => 11 >>>
or you could write it the short way:
>>> x += 6 >>> x => 17 >>>
This works for +, -, *, /, ~, &, |, ^, <<, >>, and >>>.
The one operator that has slightly different behavior when used as an operation assignment is the concatenation operator. When used as ~=, it becomes the append operator. As explained earlier, when you use the append operator on an array, it will attempt to resize the array in-place, in order to improve performance, whereas using normal concatenation will always create a new array and copy the data from the sources.
Furthermore, when you use the append operator on a string or character, you'll end up with a new string in the destination.
>>> x = "hello" >>> x => "hello" >>> x ~= " world" >>> x => "hello world" >>>
Remember, we're not modifying the string, we're creating a new string and assigning it into x.
Now for...
Increment and Decrement
These are simple. They simply add one to or subtract one from their target.
>>> global x = 5 >>> x++ >>> x => 6 >>> ++x >>> x => 7 >>>
They work for floating-point numbers as well, and can be overloaded for user-defined types.
Since increment and decrement are assignments, they have no value, and since they have no value, there is no difference between the pre- and post-target forms. "x++" and "++x" are exactly the same thing.
Finally we have...
Conditional Assignment
MiniD initializes uninitialized variables and data to null. The conditional assignment operator fits in with that nicely. Given a statement such as "x ?= y", this means that if x is null, it will be given the value of y; otherwise, y is evaluated and its result discarded.
That's it for assignments.
Flow Control Statements
This is the second major category of statements. Most of these are borrowed faithfully from the C-style languages.
I've said it before but it bears repeating: for any conditional statements, null, boolean false, integer 0 and float 0.0 are all false; all other types and values are true.
The If-Else Statement
I don't really have to explain this, do I?
>>> global x = 5 >>> if(x) writeln("yep"); else writeln("nope") yep >>>
And just like any other C-style language, there's no special case for chained if-elses:
>>> global y = 0 >>> if(y) writeln(1); else if(x) writeln(2); else writeln(3) 2 >>>
One thing to note is that if statements allow you to declare a variable in the condition.
>>> if(local x = 5) writeln(x) 5 >>>
What this means is that it evaluates the variable initializer (in this case, 5), assigns it into the new variable, and then uses that same value as the condition for the if statement. The variable is visible both in the if body and in the else body.
This is most useful when dealing with a function that returns either null or some useful value. Another use case is with tables:
>>> global t = {x = "foo", y = "bar"} >>> if(local v = t.x) writeln(v) foo >>> if(local v = t.z) writeln(v); else writeln("oh no") oh no >>>
Since tables will give null when the lookup fails, this works nicely. However, keep in mind that if you're storing bools, ints, or floats, you might not be able to rely on this:
>>> t.x = 0 >>> if(local v = t.x) writeln(v); else writeln("oh no") oh no >>>
The While and Do-While Loops
Again: boooring. While loops evaluate the condition before the loop body executes, meaning the body executes 0 or more times; whereas do-while loops evaluation the condition after the body, meaning the body executes 1 or more times.
>>> global i = 0 >>> while(i < 10) { write(i); i++ } writeln() 0123456789 >>> i = 0 >>> do { writeln(i); i++ } while(i < 10); writeln() 0123456789 >>>
Just like if statements, while statements allow a variable to be declared in the condition, with the same semantics.
The For Loops
OK, now we're getting into somewhat more interesting territory. MiniD has a typical C-style for loop:
>>> for(local i = 0; i < 10; i++) { write(i) } writeln() 0123456789 >>>
You can also declare multiple variables at the beginning and have multiple statements in the increment part:
>>> for(local i = 0, local j = 10; i < 10; i++, j--) writeln(i, ", ", j) 0, 10 1, 9 2, 8 3, 7 4, 6 5, 5 6, 4 7, 3 8, 2 9, 1 >>>
But as for that first loop... honestly, how often do you write that kind of for loop? 80 percent of the time? 90? I'd even be willing to bet higher than that. For that reason, and for the reason that it's just so ugly and longwinded to type that, MiniD also provides the numeric for loop (which is also quite a bit faster than the C-style for loop for integral ranges):
>>> for(i: 0 .. 10) { write(i) } writeln() 0123456789 >>>
Note that this declares a local variable 'i' which is only visible inside the loop. This variable can be changed, but it will not affect the operation of the loop. You can use either a colon or a semicolon (;) after the loop index variable; the meaning is unchanged.
Also note that unlike a C-style for loop, the indices are evaluated only once, at the beginning of the loop, whereas with a C-style for loop, the condition is checked upon every iteration. This is usually a good thing, as it makes the loop more efficient, but it's good to keep it in mind in case you've got a C-style for loop which looks like it can be converted into a numeric loop but which depends upon the C-style behavior.
The indices of a for loop must be integers. The low index is always inclusive and the high is always exclusive. But unlike many other languages, MiniD will determine the low and high indices at runtime and will iterate in the appropriate direction:
>>> for(i: 10 .. 0) { write(i) } writeln() 9876543210 >>>
In many other languages, this loop would not execute since they would see that 10 is greater than 0 and simply skip it.
MiniD also allows an arbitrary step size (which, like the indices, must be an integer). This is written after a comma after this second index. The sign of the step is meaningless; only the magnitude matters, as the direction of iteration is based on the indices, not the step. The only invalid step value is 0.
>>> for(i: 0 .. 10, 2) { write(i) } writeln() 02468 >>> for(i: 0 .. 10, -2) { write(i) } writeln() 02468 >>> for(i: 0 .. 10, 0) { write(i) } writeln() Error: <no location available>: stdin(1:17): Step value of a numeric for loop may not be 0 >>>
Note, however, that the loop will exit immediately if the start and end indices are the same:
>>> for(i: 0 .. 0) writeln("hi!") >>>
The Foreach Loop
The foreach loop is a powerful generic iteration construct. It's most commonly used to iterate over the elements of tables, arrays and strings.
MiniD's foreach loop is most directly related to Lua's. There are no "iterator" objects like in Python, and the loop is not turned inside-out as in D. Instead, a foreach loop works on three values: a "next" function, a "static state" value, and an "index" value. Fortunately, most of the time you won't have to explicitly specify these, as foreach works along with something called opApply to get these values automatically for the builtin types, and when you define your own types, you'll be able to define your own opApply functions so that you can use foreach on them.
We won't go into how the foreach loop works and how to write opApply functions and iterators here, but I will show you the basic use of the loop.
The foreach loop can have one or more indices to the left of the semicolon, and then the "container" (which refers collectively to the 1, 2, or 3 values to the right of the semicolon). The typical protocol is that if you're iterating over some sequence of values, the first index will be the.. index of the value, and the second index will be the value itself. It's easier to demonstrate than explain:
>>> foreach(i, v; [5, 10, 15]) writefln("{}: {}", i, v) 0: 5 1: 10 2: 15 >>> foreach(k, v; {x = 5, y = 10}) writefln("{}: {}", k, v) y: 10 x: 5 >>>
See? It makes more sense when you see it.
If you use only one index, MiniD will do something slightly different: it will insert a "dummy" index before it, meaning that the index that you wrote will hold the values, rather than the indices.
>>> foreach(v; [5, 10, 15]) writeln(v) 5 10 15 >>> foreach(v; {x = 5, y = 10}) writefln(v) 10 5 >>>
You can think of those loops as being something more like "foreach(__dummy, v; ...)".
The Switch Statement
Switch statements are a way to much more concisely write sequences of if-else structures. Switch statements are more or less what you'd expect in a C-style language, right down to the somewhat-controversial requirement of a 'break' at the end of each case. MiniD does introduce a couple of nice features to them, though. One, switch statements can be used with any type, and not just integers (and strings, if you're comparing to D). Two, switch statements can use runtime-defined case values.
The following demonstrates that switches can use multiple types:
>>> foreach(v; [null, true, 5, "hi"]) ... { ... switch(v) ... { ... case null: writeln("it's null!"); break ... case true: writeln("it's true!"); break ... case "hi": writeln("it's greeting me!"); break ... default: writeln("I honestly don't know what it is!"); break ... } ... } it's null! it's true! I honestly don't know what it is! it's greeting me! >>>
You can also use runtime values (this is kind of a contrived example):
>>> for(i: 0 .. 5) ... for(j: 0 .. 5) ... switch(i) ... { ... case j: writeln("match! ", i); break; ... default: break; ... } match! 0 match! 1 match! 2 match! 3 match! 4 >>>
Being able to use runtime values is much more useful if you have, say, a bunch of constants (well, values that should be used as constants as MiniD has no constants) defined in another module. So you might have "case foo.X: ... case foo.Y: ... case foo.Z:" and so forth.
If a switch statement switches on a value which doesn't match any case and there is no default, an error is thrown:
>>> switch(5) { case 0: break; } Error: stdin(1): Switch without default >>>
Oh, there's one other thing I should probably mention: you can put multiple case values separated by commas in a single case statement.
>>> switch(5) ... { ... case 4, 5, 6: writeln("it's one of them!") ... } it's one of them! >>>
Continue and Break
These are two statements that go together. The continue statement allows you to start the next iteration of the innermost loop prematurely, while the break statement allows you to immediately stop execution of the innermost loop. break also does double duty as the means to ending a case statement inside a switch; without it, execution will continue on through to the next case statement:
>>> switch(0) { case 0: writeln("foo"); case 1: writeln("bar") } foo bar >>>
Put a break and it'll stop:
>>> switch(0) { case 0: writeln("foo"); break; case 1: writeln("bar") } foo >>>
The Return Statement
This statement allows you to stop execution of the current function, returning execution to the function that called it, and optionally give that function back one or more values. As you're probably aware by now, MiniD allows for multiple values to be returned from a function.
Also, every function you write will have an implicit "return;" inserted at the end of it. That is, if execution reaches the end of a function, the function will return nothing:
>>> function foo(x) ... { ... if(x == 5) ... return 10 ... } >>> foo(5) => 10 >>> foo(10) >>>
The Try-Catch-Finally and Throw Statements
MiniD adopts exception handling as its means of handling errors like many other modern imperative languages. MiniD's exception handling model is fairly simple. Values of any type can be thrown as exceptions. They can be caught and optionally rethrown. You can also set up finally blocks to execute code regardless of whether or not an exception occurred.
To set up an exception handler, you use a try-catch or try-catch-finally statement. try-finally statements are generally used when you just want to ensure that some code gets executed upon exiting a block of code. Here's an example try-catch-finally statement:
>>> function foo(n) ... { ... try ... someFunction(n) ... catch(e) ... writefln("I caught: '{}'", e) ... finally ... writeln("foo finally") ... } >>>
Let's also define the function it calls, someFunction, which will also show the use of the throw statement:
>>> function someFunction(n) ... { ... if(n > 10) ... throw "Too big!" ... return 10 / n ... }
Notice that we have both a throw statement and a division expression in this function, both of which could cause exceptions.
Now let's call foo with some choice values for n to see what happens:
>>> foo(5) foo finally >>> foo(12) I caught: 'Too big!' foo finally >>> foo(0) I caught: 'Integer divide by zero' foo finally >>>
"foo(5)" worked fine, no errors, and the finally block was executed at the end of the function. "foo(12)" failed because someFunction threw an exception. "foo(0)" failed because the division in someFunction failed. But notice that the finally block was executed after the catch block in both cases.
Finally blocks are executed whenever a function is left. If a function returns from inside a try-catch-finally or try-finally statement, the finally block will still be executed.
>>> try ... return ... finally ... writeln("o hai!") o hai! >>>
(For your information, it's OK to execute "return" in MDCL; it does nothing. But here it illustrates the point that the finally is still executed.)
That's really all there is to exception handling.
Other Statements
This is the final category of statements, and it consists of a few random statements that don't really fit under the other two categories.
The Assert Statement
This kind of statement is very useful for making sure that you're not doing anything stupid. You can (and are encouraged to) use these to make sure that parameters are of valid types or are in valid ranges; to ensure that internal object invariants aren't broken; to check that global data structures or constants haven't been accidentally tampered with; et cetera, et cetera, et cetera. You think something might break if you throw some crazy data at it? Put an assert there. If it doesn't break, it doesn't matter. But you'll thank yourself for doing so if it does break. And if you're really, really worried that all your assertions are slowing things down -- you can remove assertions with a switch in the compiler.
The syntax looks like a function call where the first parameter is an expression to be evaluated as a condition, and the optional second parameter is an expression that evaluates to a string and will be used as the assertion's message. The message expression is only evaluated if the condition fails, so you don't have to worry about performance drains from string concatenation in the message or whatever. If you don't give a message, one will be generated based on the filename and location of the assertion.
Assertions work by evaluating the condition. If the condition is true, it throws the message. That's it. If you wanted to, you could replace every assertion with an if statement, like so:
assert(cond, message) // is identical to if(!cond) throw message
In fact, this is exactly what the reference compiler does, although with the nice option of being able to turn off assertions if you so desire.
Import Statements
Import statements have to do with the module system. They are how you define one module's dependencies upon others. I won't go too deeply into how this works now, but the general syntax is pretty simple:
import foo.bar
This imports the module bar, which is in the package foo. If this import succeeds, you are guaranteed that there is a global namespace named "foo", and in that namespace, there is a namespace named "bar".
More on imports in a later chapter!
Block Statements
Anywhere where you can write one statement, you can write many statements if you use a block statement. It's simple enough: it starts with an open brace, ends with a closing brace, and has zero or more statements inside which are executed in order. Any local variables declared inside the block statement are visible only until the end of the block statement; see the declaration statement section a couple sections down.
Expression Statements
Aside from control flow statements and assignments, expression statements will probably be the most common things you write. An expression statement is a statement that's just an expression that's evaluated. Only expressions that have side effects (or rather, which possibly have side effects) can be used as statements. That is, something like "4 + 5" does not qualify as a statement since it has no effects. But a function call does qualify. Any results from the function call are discarded. Yield expressions also qualify. Conditional (?:), logical and and logical or statements qualify as long as one of their operands qualifies.
Declaration Statements
Declaration statements include variable, function, class, and namespace declarations. Right now, though, I'll only talk about variable declarations; the other three kinds of declarations will be covered later (and are really just sugar for variable declarations anyway!).
There are two kinds of variables in MiniD: locals and globals. We've been using globals for a while now, mostly because in MDCL they're more useful. But when you're writing real code inside modules, more than likely you'll be using locals a lot more than globals.
As was touched on before, a global variable is inserted as a key-value pair in the current module's namespace. In MDCL, there is no module and so the current namespace is _G, the global namespace. When you're writing a module, though, it's better to make as many things as you can local and use globals only for things that you want other modules to be able to see.
So what's a local? It's a variable that is visible and usable only within the scope in which it was declared. Local variables are also temporary and disappear after that scope ends. A module is also kind of like a big function that just happens to consist mostly of declaration statements instead of other kinds of code. So, if you declare a local in a module, it will be visible everywhere from where it is defined to the end of the module, but it won't be visible from outside the module.
module foobar local x = 10 function test() writeln("x = ", x)
If we then import this in MDCL:
>>> import foobar >>> foobar.test() x = 10 >>> foobar.x Error: stdin(1): Attempting to access nonexistent field 'x' from 'namespace foobar' >>>
Besides at module scope, locals have meaning inside functions too. They are simply local variables, much as you would declare in any other language. And as in most other languages, locals are only visible in the scope in which they are declared:
>>> function foo() ... { ... local x = 5 ... writeln(x) ... { ... local y = 10 ... writeln(y) ... } ... y = 3 // y no longer exists here... ... } >>> foo() 5 10 Error: foo(9): Attempting to get nonexistent global 'y' >>> x = 4 // x obviously no longer exists, it's only in foo Error: stdin(1): Attempting to get nonexistent global 'x' >>>
It is also possible to declare globals from within functions, but this isn't all that useful since global variable declarations will fail if another global of the same name already exists:
>>> global x >>> global x Error: stdin(1): Attempting to create global 'x' that already exists >>>
The End
Up next are functions.
