Adding "switch/case" and "for" statements to Jack

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

Adding "switch/case" and "for" statements to Jack

cadet1620
Administrator

Adding switch statement

The switch statement implements a modified if/elseif/else structure where each dependent code block jumps to the following code block. This implementation has a few differences from the classic C++/Java switch.
  • The case values are expressions that are compared against the switch value. This allows case values to be negative numbers. (Jack integer constants are non-negative.)
  • For ease of compiling, the default case, if present, must be the last case in the switch statement.

Grammar changes

switchStatement:  'switch' '(' expression ')' '{' switchBody '}'
switchBody:  switchCase* switchDefault?
switchCase:  'case' expression ':' statements
switchDefault:  'default' ':' statements
breakStatement:  'break' ';'

Implementation

compileSwitch() begins by saving the current jump targets for the break and continue statements; they may be in use by outer switch, for or while statements.[1]

It generates a new break target (VM label) and clears the continue target. Clearing the continue target signals that compileContinue()[1] should raise a compilation error because the continue command is not valid in the current context.

It then compiles the control expression which leaves the value on the top of the stack.

compileSwitch() calls compileSwitchBody() which compiles the case and default clauses. Note that the default clause is handled identically to a case clause, except that there is no value to compile or compare.

Each case clause requires three VM labels; one for the start of this case's command block, one for the end of this clause, and one for the start of the next case's command block. This third label becomes the first label when the next case clause is compiled.

Generate code that evaluates the case expression and compares the result to the control value. In the VM language, the only way to do this compare is destructive—the VM eq command consumes the control value and the case value, replacing them with the comparison result. An easy way to solve this is to duplicate the control value before compiling the case expression. Too bad there is no dup VM command; duplication requires a pop and two push commands.

After the test and a bit of spaghetti, generate the start block label and compile statements until the next 'case', 'default' or '}'. These indicate the end of the current case clause. (Spaghetti is good sometimes: if-goto x, goto y label x generates faster ASM than not, if-goto y.)

Generate a jump to the next case's command block so that this case will fall into the next case if there was no break statement.

Finally, generate the end-of-case label.

At the end of the switch body, generate the next command block label that was used at the end of the final case clause.

Here is pseudocode for a case clause.

    if case value == TOS
        goto CASE_(n)_CODE
    else
        goto CASE_(n)_END
CASE_(n)_CODE:
    statements
    goto CASE_(n+1)_CODE
CASE_(n)_END:

After the last case, generate a matching CASE_(n+1)_CODE label so that the goto CASE_#+1_CODE in the final case will have somewhere to go to.

After the switch body, generate the break target label.

Restore the saved break and continue targets.


Adding for statement

This is the standard C++/Java for statement, except that variables may not be defined within it[2] and the '{' and '}' are required.[3]

Grammar changes

forStatement:  'for' '(' forInit? ';' forTest? ';' forIncr? ')' {' statements }'
forInit:  assignmentList
forTest:  expression
forIncr:  assignmentList
assignmentList:  assignment (',' assignment)*
letStatement: 'let' assignment ';'
assignment: varName ('[' expression ']')? '=' expression
breakStatement:  'break' ';'
continueStatement:  'continue' ';'

Because Jack expressions do not support the ',' operator, multiple initializations and increments are handled by adding assignmentList. (letStatement is included just to show how assignment fits into the grammar.)

Implementation

compileFor begins by saving the current jump targets for the break and continue statements; they may be in use by outer for, switch or while statements.[1]

It generates new break and continue targets, and two internal VM labels.

Functionally, the for statement is

    forInit
    while (forTest) {
        statements
CONTINUE:
        forIncr
    }
BREAK:

That structure is tough to compile using left-to-right parsing with on-the-fly code generation because of the inverted order of statements and forIncr.

The code needs to be restructured so that statements comes last. Since the while exits by jumping to BREAK, there is handy dead space immediately before BREAK so statements can fit there and be logically inserted where it needs to be using goto statements. Here is what it looks like with the while expanded.

    forInit
LOOP:
    if (forTest) != 0
        goto BODY
    else
        goto BREAK
CONTINUE:
    forIncr    
    goto LOOP
BODY:
    statements
    goto CONTINUE
BREAK:

After generating the code, restore the saved break and continue targets.


[1]  If you have implemented Adding "break" and "continue" to Jack's while loops, you already have compileBreak() and compileContinue() routines and their state variables.
[2]  See Adding Nested Variable Scopes to the Jack Compiler, but be warned, this is a rather difficult project.
[3]  It is fairly easy to make the '{' '}' optional on for and while statements. See Removing the Requirement for Braces in Jack's "if" and "while" Statements.