[See
Adding Named Constants to Jack for another Jack language extension.]
As I was writing the
Jack Jack Compiler I had to write rather
tortured structure to achieve single exit point code to ensure that there were
no leaking Strings. For example:
['ok' is true coming into this code.]
let break = false;
while (ok & ( ~ break)) {
do xml.writeToken(token);
let ok = _expectIdentifier();
if (ok) {
// Add class variable to symbol table
let ok = symbolTable.define(token.identifier(), type, kind);
if ( ~ ok) {
do _printDuplicateSymbol(token.identifier());
}
} if (ok) {
do token.advance();
do xml.writeToken(token);
if (_isSymbol(44)) { // ','
do token.advance();
} else {
let break = true;
}
}
}
I was wishing that Jack had break statements for its while
loops. With break statements, this code could be rewritten:
while (true) {
do xml.writeToken(token);
let ok = _expectIdentifier();
if ( ~ ok) {
break;
}
// Add class variable to symbol table
let ok = symbolTable.define(token.identifier(), type, kind);
if ( ~ ok) {
do _printDuplicateSymbol(token.identifier());
break;
}
do token.advance();
do xml.writeToken(token);
if (_isSymbol(44)) { // ','
do token.advance(); }
else {
break;
}
}
It's certainly clearer in this code that the loop exits on the first error that
is encountered.
What would it take to add break and continue statements?
Grammatically,
break and
continue are trivial; the are just two
more statement types in the
statement rule:
<statement> |
::= |
<letStatement> |
<ifStatement> |
<whileStatement> |
<doStatement> |
<returnStatement> |
<breakStatement> |
<continueStatement> |
Semantically, break and continue may only appear within the
body of a while statement, or an element nested within the body. The
break or continue refers to the deepest while
statement containing them. (The only nestable elements
containing statements are if and while.)
In my compiler, compileWhile() starts by creating three unique labels
(Strings) that will be used in the generated code:
continueLabel = uniqueLabel(); // Before conditional test
bodyLabel = uniqueLabel(); // Before statement body
breakLabel = uniqueLabel(); // After statement body
These are local variables that are destroyed at the end of compileWhile().
break and continue statements will need to generate code that
jumps to either breakLabel or continueLabel.
How will compileBreak() access the breakLabel variable
set in compileWhile()?
breakLabel and
continueLabel will
need to become class variables:
this.continueLabel = uniqueLabel(); // Before conditional test
bodyLabel = uniqueLabel(); // Before statement body
this.breakLabel = uniqueLabel(); // After statement body
These class variables will need to be initialized to NULL so that the
compileBreak() and compileContinue() can know when these
statements are illegal.
They also need to be reset to NULL at the end of compileWhile()
so that break and continue statements will be invalid
outside the loop.
What about nested while statements?
Nested
while statements need to use different
breakLabel and
continueLabel values than their parent
while loop(s).
while (...) {
...
while (...) {
...
if (...) {
continue; // Jump to the beginning of the inner while.
}
...
}
if (...) {
continue; // Jump to the beginning of the outer while.
}
}
compileWhile() is a recursive function. When it is called for the nested
while loop, it will overwrite the class variables when it allocates the
new breakLabel and continueLabel. All break and
continue statements in its scope will jump to the right place.
The problem is what happens when this compileWhile() returns and the
outer compileWhile() resumes. The breakLabel and
continueLabel are NULL. The outer continue will be flagged as
a "break outside of while" error.
Nested Scopes
Languages like C++ and Java allow variables to be defined inside any { } pair.
This is called a nested scope. Those variables are only accessible inside the
{ } pair. Also in the nested scope are things less visible to the programmer,
for instance the
break and
continue information. Adding nested
scopes like this to the Jack compiler would be a lot of work.
Fortunately, all that needs to be saved is the context for any outer
while statement; this is just the two class variables breakLabel
and continueLabel. They can be saved in local variables on the stack.
void compileWhile() {
saveContinueLabel = this.continueLabel;
saveBreakLabel = this.breakLabel;
this.continueLabel = uniqueLabel(); // Before conditional test
bodyLabel = uniqueLabel(); // Before statement body
this.breakLabel = uniqueLabel(); // After statement body
...
[Explicitly deallocate this.continueLabel and this.breakLabel
if required by programming language.]
this.continueLabel = saveContinueLabel;
this.breakLabel = saveBreakLabel;
}
Since breakLabel and continueLabel were NULL on entry to the
outermost while statement, they will be restored to NULL on exiting
that loop. Any break or continue statements outside of the
outermost while loop will be flagged as errors.
Final Result
Main.jack:
class Main {
function void main() {
var int i, j;
while (i < 10) {
do Output.printInt(i);
do Output.printChar(32);
let j = 0;
while (j < (i+1)) {
if (j = 2) { // Don't print when j = 2.
let j = j+1;
continue;
}
if (j > 5) { // Don't print when j > 5.
break;
}
do Output.printInt(j);
do Output.printChar(32);
let j = j+1;
}
do Output.println();
if (i < 4) { // Count by 1 until i >= 4.
let i = i+1;
continue; // Test restored continue address.
}
let i = i+2; // Count by 2.
}
return;
}
}
Here's Main.vm generated by my compiler. The comments in the .vm file showing source lines, VM function command numbers, and variable names are controlled by a command line switch. (As is this language extension.)
--Mark