Adding "break" and "continue" to Jack's while loops

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Adding "break" and "continue" to Jack's while loops

cadet1620
Administrator
This post was updated on .
[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

VMEmulator showing test 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