The first is the concept of a loop with a condition somewhere in the middle of it's body. This can already be done in C# (and indeed any language with a while loop) with:
while (true) {
// x x x
if (condition) break;
// x x x
}
In Fera, you can do the same thing with a little less typing and a bit more style:
do {
// x x x
break when (condition);
// x x x
}
This also allows for the programmer to perform infinite loops, though the compiler will emit a warning, letting you know that you've forgot to put in some kind of condition for your loop. You can also use the 'when' construct with 'next' and 'continue' where ever they can be used. The compiler simply reverses the construct, so 'next when (x);' turns into 'if (x) next;'.
Originally I went with 'break if (x);'. Some languages allow if() tails on any instruction, but they are usually not whitespace-agnostic. I looked at this case to decide against it:
if (x)
doSomething ();
else
doSomethingElse () // OOPS! forgot my semicolon!
if (x)
doSomethingTotallyDifferent ();
Forgetting semicolons happens all the time, and this error wouldn't even be acknowledged by the compiler, making this bug very hard to find. Adding the 'when' keyword avoids this ambiguity, and fits in nicely with the do{} loop syntax. I'll be further investigating whether it's a good idea to allow this for all instructions (or most anyway).
There are some specific error conditions for the infinite 'do' loop; for one, the construct:
do;
causes a compile-time error, not because we disfavor pure infinite loops, but rather because it looks retarded. Also, only certain instructions are allowed to be put as the action of an infinite do loop. Curly brace groups, standalone expressions which have known/unknown side effects, conditionals, or loops are allowed. So this construct 'do echo "annoying!";' is thankfully not allowed.
If you want a pure infinite loop, just use 'loop;'. This will always emit a warning, telling you it should never be used in production code (unless you explicitly ignore it, or elevate the warning to be treated like an error).
Another type of loop (which is not implemented yet by my compiler) is do/exits.
var int x = 0;
var int y = 3;
do {
// x x x
} exits {
case x > 10:
// x x x
case y < 0:
// x x x
}
This is syntactically very different from similar constructs in other languages, as it reuses the switch() syntax and logic and tosses it in with the do loop. The result is a pretty unobscured syntax that is easy to remember once you've learned the nuances of both.
Using 'break' in this loop will cause the exits to be evaluated, before control returns outside the loop. Using 'next' will also cause the exits to be evaluated, except that a lack of any exits will result in a new loop iteration. Within the 'exits' block, the 'continue' and 'break' work exactly like they do in switch/branch.
Owing to the fact that we use 'next' for loops and 'continue' for switches, you can use 'next' within an exit block to have the loop return to iteration. This is a unique property among the loop types that Fera offers, something which must be done with if() in C#.
Naturally the test casing marathon continues. Currently there are a total of 90 tests within the test suite, and 71 of them pass. 10 are expected fails, and the other 9 are regressions. I tested many of the earliest work on this project by hand, so now that I've added testcases for them, I discovered many bugs in the Fera grammar and of course the compiler itself.
One of the most perplexing problems was something I thought was happening in the compiler. The problem was happening in the do/while testcases which used a compound condition like so:
var int x = 0;
do {
// x x x
++x;
} while (x > 0 && x < 5);
This particular formulation ensures that the compiler isn't creating the FWhileLoop instruction which represents the loop with PostCondition = false. Doing so would result in the loop behaving like a while() loop, in which case it would never do the loop action.
The problem was that it was running infinitely, and there is a Console.Write(x) within it, which resulted in an infinite sea of digits across the console window.
I quickly figured out it was the compound nature of the condition, so I turned my attention to test 'if3', which tested the if() statement with a similar condition. It happened to be 'x == 3 && y == 4'. The testcase failed because the action of the if() never runs though it should.
Inspecting the CIL showed the comparison code to be something like this:
0 ldloc.0 ; load x to the stack
1 ldc.i4.3 ; load constant zero to the stack
2 dup ; save the top value
3 brfalse 8 ; skip the second condition if false
4 pop ; pop the unused save value
5 ldloc.1 ; load y
6 ldc.i4.4 ; load 4
7 ceq ; compare equality
8 nop ; label
Naturally, there should be a ceq inserted at #2. The way it is now, the first condition would always end up being true. It looked like the FComparison expression operand was not outputting 'ceq'. However upon inspecting that code it was fine and I was able to verify that it in fact was running the branch that emitted the instruction.
Only after much head scratching did I realize that the 'ceq' was there, but was far later in the body then it should be. At a hunch I checked the token listing for the file being compiled and sure enough, it was treating the expression like this:
x == (3 && y == 4)
Which would cause a more complete compiler to yelp but not here. So the tree of expression operands looked like:
FComparison
| FVariableAccess
| FBinaryBoolean
| | FConstant
| | FComparison
| | | FVariableAccess
| | | FConstant
I had to add a new level in the expression grammar called 'CValue' which is comprised of a 'Value' term and an optional set of ComparisonTails. Binary logic operations are handled in the level above it, causing comparisons to be treated as children of the binary logic operators. The tree thus became:
FBinaryBoolean
| FComparison
| | FVariableAccess
| | FConstant
| FComparison
| | FVariableAccess
| | FConstant
Pretty.
No comments:
Post a Comment