From 8c85232b90a5b0751e0ce0abed07e4ea1fc0f921 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Tue, 24 Dec 2013 10:15:50 -0800 Subject: [PATCH] Break statements. --- src/wren_compiler.c | 134 +++++++++++++++++++++++----- test/break/in_function_in_loop.wren | 7 ++ test/break/in_method_in_loop.wren | 9 ++ test/break/in_while_loop.wren | 15 ++++ test/break/nested_loop.wren | 31 +++++++ test/break/outside_loop.wren | 1 + 6 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 test/break/in_function_in_loop.wren create mode 100644 test/break/in_method_in_loop.wren create mode 100644 test/break/in_while_loop.wren create mode 100644 test/break/nested_loop.wren create mode 100644 test/break/outside_loop.wren diff --git a/src/wren_compiler.c b/src/wren_compiler.c index 99548cd9..2936f6cd 100644 --- a/src/wren_compiler.c +++ b/src/wren_compiler.c @@ -76,10 +76,12 @@ typedef enum TOKEN_EQEQ, TOKEN_BANGEQ, + TOKEN_BREAK, TOKEN_CLASS, TOKEN_ELSE, TOKEN_FALSE, TOKEN_FN, + TOKEN_FOR, TOKEN_IF, TOKEN_IS, TOKEN_NEW, @@ -204,6 +206,10 @@ typedef struct sCompiler // in effect at all. Any variables declared will be global. int scopeDepth; + // Index of the first instruction of the body of the innermost loop currently + // being compiled. Will be -1 if not currently inside a loop. + int loopBody; + // The name of the method this compiler is compiling, or NULL if this // compiler is not for a method. Note that this is just the bare method name, // and not its full signature. @@ -299,6 +305,7 @@ static int initCompiler(Compiler* compiler, Parser* parser, Compiler* parent, compiler->parser = parser; compiler->parent = parent; compiler->numCodes = 0; + compiler->loopBody = -1; compiler->methodName = methodName; compiler->methodLength = methodLength; @@ -485,10 +492,12 @@ static void readName(Parser* parser, TokenType type) nextChar(parser); } + if (isKeyword(parser, "break")) type = TOKEN_BREAK; if (isKeyword(parser, "class")) type = TOKEN_CLASS; if (isKeyword(parser, "else")) type = TOKEN_ELSE; if (isKeyword(parser, "false")) type = TOKEN_FALSE; if (isKeyword(parser, "fn")) type = TOKEN_FN; + if (isKeyword(parser, "for")) type = TOKEN_FOR; if (isKeyword(parser, "if")) type = TOKEN_IF; if (isKeyword(parser, "is")) type = TOKEN_IS; if (isKeyword(parser, "new")) type = TOKEN_NEW; @@ -707,6 +716,7 @@ static void nextToken(Parser* parser) case TOKEN_BANGEQ: case TOKEN_CLASS: case TOKEN_ELSE: + case TOKEN_FOR: case TOKEN_IF: case TOKEN_IS: case TOKEN_NEW: @@ -1683,10 +1693,12 @@ GrammarRule rules[] = /* TOKEN_GTEQ */ INFIX_OPERATOR(PREC_COMPARISON, ">= "), /* TOKEN_EQEQ */ INFIX_OPERATOR(PREC_EQUALITY, "== "), /* TOKEN_BANGEQ */ INFIX_OPERATOR(PREC_EQUALITY, "!= "), + /* TOKEN_BREAK */ UNUSED, /* TOKEN_CLASS */ UNUSED, /* TOKEN_ELSE */ UNUSED, /* TOKEN_FALSE */ PREFIX(boolean), /* TOKEN_FN */ PREFIX(function), + /* TOKEN_FOR */ UNUSED, /* TOKEN_IF */ UNUSED, /* TOKEN_IS */ INFIX(PREC_IS, is), /* TOKEN_NEW */ { new_, NULL, constructorSignature, PREC_NONE, NULL }, @@ -1883,10 +1895,111 @@ static void classStatement(Compiler* compiler) defineVariable(compiler, symbol); } +// Returns the number of arguments to the instruction at [ip] in [fn]'s +// bytecode. +static int getNumArguments(ObjFn* fn, int ip) +{ + Code instruction = fn->bytecode[ip]; + switch (instruction) + { + case CODE_NULL: + case CODE_FALSE: + case CODE_TRUE: + case CODE_POP: + case CODE_IS: + case CODE_CLOSE_UPVALUE: + case CODE_RETURN: + case CODE_NEW: + return 0; + + // Instructions with two arguments: + case CODE_METHOD_INSTANCE: + case CODE_METHOD_STATIC: + return 2; + + case CODE_CLOSURE: + { + int constant = fn->bytecode[ip + 1]; + ObjFn* loadedFn = AS_FN(fn->constants[constant]); + + // There is an argument for the constant, then one for each upvalue. + return 1 + loadedFn->numUpvalues; + } + + default: + // Most instructions have one argument. + return 1; + } +} + +static void whileStatement(Compiler* compiler) +{ + // Remember what instruction to loop back to. + int loopStart = compiler->numCodes - 1; + + // Compile the condition. + consume(compiler, TOKEN_LEFT_PAREN, "Expect '(' after 'while'."); + expression(compiler); + consume(compiler, TOKEN_RIGHT_PAREN, "Expect ')' after while condition."); + + emit(compiler, CODE_JUMP_IF); + int exitJump = emit(compiler, 255); + + // Compile the body. + int outerLoopBody = compiler->loopBody; + compiler->loopBody = compiler->numCodes; + + block(compiler); + + // Loop back to the top. + emit(compiler, CODE_LOOP); + int loopOffset = compiler->numCodes - loopStart; + emit(compiler, loopOffset); + + patchJump(compiler, exitJump); + + // Find any break placeholder instructions (which will be CODE_END in the + // bytecode) and replace them with real jumps. + int i = compiler->loopBody; + while (i < compiler->numCodes) + { + if (compiler->fn->bytecode[i] == CODE_END) + { + compiler->fn->bytecode[i] = CODE_JUMP; + patchJump(compiler, i + 1); + i += 2; + } + else + { + // Skip this instruction and its arguments. + i += 1 + getNumArguments(compiler->fn, i); + } + } + + compiler->loopBody = outerLoopBody; +} + // Compiles a statement. These can only appear at the top-level or within // curly blocks. Unlike expressions, these do not leave a value on the stack. void statement(Compiler* compiler) { + if (match(compiler, TOKEN_BREAK)) + { + if (compiler->loopBody == -1) + { + error(compiler, "Cannot use 'break' outside of a loop."); + } + + // Emit a placeholder instruction for the jump to the end of the body. When + // we're done compiling the loop body and know where the end is, we'll + // replace these with `CODE_JUMP` instructions with appropriate offsets. + // We use `CODE_END` here because that can't occur in the middle of + // bytecode. + emit(compiler, CODE_END); + emit(compiler, 0); + return; + } + if (match(compiler, TOKEN_CLASS)) { classStatement(compiler); @@ -1962,26 +2075,7 @@ void statement(Compiler* compiler) if (match(compiler, TOKEN_WHILE)) { - // Remember what instruction to loop back to. - int loopStart = compiler->numCodes - 1; - - // Compile the condition. - consume(compiler, TOKEN_LEFT_PAREN, "Expect '(' after 'while'."); - expression(compiler); - consume(compiler, TOKEN_RIGHT_PAREN, "Expect ')' after while condition."); - - emit(compiler, CODE_JUMP_IF); - int exitJump = emit(compiler, 255); - - // Compile the body. - block(compiler); - - // Loop back to the top. - emit(compiler, CODE_LOOP); - int loopOffset = compiler->numCodes - loopStart; - emit(compiler, loopOffset); - - patchJump(compiler, exitJump); + whileStatement(compiler); return; } diff --git a/test/break/in_function_in_loop.wren b/test/break/in_function_in_loop.wren new file mode 100644 index 00000000..115a0286 --- /dev/null +++ b/test/break/in_function_in_loop.wren @@ -0,0 +1,7 @@ +var done = false +while (!done) { + fn { + break // expect error + } + done = true +} \ No newline at end of file diff --git a/test/break/in_method_in_loop.wren b/test/break/in_method_in_loop.wren new file mode 100644 index 00000000..c36577c7 --- /dev/null +++ b/test/break/in_method_in_loop.wren @@ -0,0 +1,9 @@ +var done = false +while (!done) { + class Foo { + method { + break // expect error + } + } + done = true +} diff --git a/test/break/in_while_loop.wren b/test/break/in_while_loop.wren new file mode 100644 index 00000000..ca458d5e --- /dev/null +++ b/test/break/in_while_loop.wren @@ -0,0 +1,15 @@ +var i = 0 +while (true) { + i = i + 1 + IO.write(i) + if (i > 2) { + // TODO: Should not require block for break. + break + } + IO.write(i) +} +// expect: 1 +// expect: 1 +// expect: 2 +// expect: 2 +// expect: 3 diff --git a/test/break/nested_loop.wren b/test/break/nested_loop.wren new file mode 100644 index 00000000..9d21792f --- /dev/null +++ b/test/break/nested_loop.wren @@ -0,0 +1,31 @@ +var i = 0 +while (true) { + IO.write("outer " + i.toString) + if (i > 1) { + // TODO: Should not require block for break. + break + } + + var j = 0 + while (true) { + IO.write("inner " + j.toString) + if (j > 1) { + // TODO: Should not require block for break. + break + } + + j = j + 1 + } + + i = i + 1 +} + +// expect: outer 0 +// expect: inner 0 +// expect: inner 1 +// expect: inner 2 +// expect: outer 1 +// expect: inner 0 +// expect: inner 1 +// expect: inner 2 +// expect: outer 2 diff --git a/test/break/outside_loop.wren b/test/break/outside_loop.wren new file mode 100644 index 00000000..7f35809a --- /dev/null +++ b/test/break/outside_loop.wren @@ -0,0 +1 @@ +break // expect error