diff --git a/src/com/craftinginterpreters/lox/AstPrinter.java b/src/com/craftinginterpreters/lox/AstPrinter.java index 65f1916..a72dc3a 100644 --- a/src/com/craftinginterpreters/lox/AstPrinter.java +++ b/src/com/craftinginterpreters/lox/AstPrinter.java @@ -64,6 +64,11 @@ class AstPrinter implements Expr.Visitor, Stmt.Visitor { return builder.toString(); } + @Override + public String visitBreakStmt(Stmt.Break statement) { + return "break"; + } + @Override public String visitPrintStmt(Stmt.Print statement) { return parenthesize("print", statement.expression); diff --git a/src/com/craftinginterpreters/lox/Break.java b/src/com/craftinginterpreters/lox/Break.java new file mode 100644 index 0000000..708915f --- /dev/null +++ b/src/com/craftinginterpreters/lox/Break.java @@ -0,0 +1,5 @@ +package com.craftinginterpreters.lox; + +class Break extends RuntimeException { + +} diff --git a/src/com/craftinginterpreters/lox/CMakeLists.txt b/src/com/craftinginterpreters/lox/CMakeLists.txt index 447b2cf..18cdc77 100644 --- a/src/com/craftinginterpreters/lox/CMakeLists.txt +++ b/src/com/craftinginterpreters/lox/CMakeLists.txt @@ -20,4 +20,5 @@ add_jar(Lox Interpreter.java RuntimeError.java Environment.java + Break.java ENTRY_POINT com/craftinginterpreters/lox/Lox) diff --git a/src/com/craftinginterpreters/lox/Interpreter.java b/src/com/craftinginterpreters/lox/Interpreter.java index 9d0cfb3..22c8d4a 100644 --- a/src/com/craftinginterpreters/lox/Interpreter.java +++ b/src/com/craftinginterpreters/lox/Interpreter.java @@ -161,13 +161,22 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { @Override public Void visitWhileStmt(Stmt.While stmt) { - while (isTruthy(evaluate(stmt.condition))) { - execute(stmt.body); + try { + while (isTruthy(evaluate(stmt.condition))) { + execute(stmt.body); + } + } catch (Break ex) { + // We continue on. } return null; } + @Override + public Void visitBreakStmt(Stmt.Break stmt) { + throw new Break(); + } + @Override public Object visitAssignExpr(Expr.Assign expr) { Object value = evaluate(expr.value); diff --git a/src/com/craftinginterpreters/lox/Parser.java b/src/com/craftinginterpreters/lox/Parser.java index 7d7b27a..786d317 100644 --- a/src/com/craftinginterpreters/lox/Parser.java +++ b/src/com/craftinginterpreters/lox/Parser.java @@ -44,6 +44,8 @@ class Parser { } private Stmt statement() { + if (match(BREAK)) + return breakStatement(); if (match(FOR)) return forStatement(); if (match(IF)) @@ -84,13 +86,11 @@ class Parser { Stmt body = statement(); if (increment != null) { - body = new Stmt.Block( - Arrays.asList( - body, - new Stmt.Expression(increment))); + body = new Stmt.Block(Arrays.asList(body, new Stmt.Expression(increment))); } - if (condition == null) condition = new Expr.Literal(true); + if (condition == null) + condition = new Expr.Literal(true); body = new Stmt.While(condition, body); if (initializer != null) { @@ -100,6 +100,11 @@ class Parser { return body; } + private Stmt breakStatement() { + consume(SEMICOLON, "Expect ';' after break statement."); + return new Stmt.Break(); + } + private Stmt ifStatement() { consume(LEFT_PAREN, "Expect '(' after 'if'."); Expr condition = expression(); diff --git a/src/com/craftinginterpreters/lox/Scanner.java b/src/com/craftinginterpreters/lox/Scanner.java index e21f701..b0aba3f 100644 --- a/src/com/craftinginterpreters/lox/Scanner.java +++ b/src/com/craftinginterpreters/lox/Scanner.java @@ -33,6 +33,7 @@ class Scanner { keywords.put("true", TRUE); keywords.put("var", VAR); keywords.put("while", WHILE); + keywords.put("break", BREAK); } Scanner(String source) { @@ -54,91 +55,118 @@ class Scanner { char c = advance(); switch (c) { - case '(': addToken(LEFT_PAREN); break; - case ')': addToken(RIGHT_PAREN); break; - case '{': addToken(LEFT_BRACE); break; - case '}': addToken(RIGHT_BRACE); break; - case ',': addToken(COMMA); break; - case '.': addToken(DOT); break; - case '-': addToken(MINUS); break; - case '+': addToken(PLUS); break; - case ';': addToken(SEMICOLON); break; - case '*': addToken(STAR); break; - case '!': - addToken(match('=') ? BANG_EQUAL : BANG); - break; - case '=': - addToken(match('=') ? EQUAL_EQUAL : EQUAL); - break; - case '<': - addToken(match('=') ? LESS_EQUAL : LESS); - break; - case '>': - addToken(match('=') ? GREATER_EQUAL : GREATER); - break; - case '/': - if (match('/')) { - // A comment goes until the end of the line. - while (peek() != '\n' && !isAtEnd()) advance(); - } else { - addToken(SLASH); - } - break; - - case ' ': - case '\r': - case '\t': - // Ignore whitespace. - break; + case '(': + addToken(LEFT_PAREN); + break; + case ')': + addToken(RIGHT_PAREN); + break; + case '{': + addToken(LEFT_BRACE); + break; + case '}': + addToken(RIGHT_BRACE); + break; + case ',': + addToken(COMMA); + break; + case '.': + addToken(DOT); + break; + case '-': + addToken(MINUS); + break; + case '+': + addToken(PLUS); + break; + case ';': + addToken(SEMICOLON); + break; + case '*': + addToken(STAR); + break; + case '!': + addToken(match('=') ? BANG_EQUAL : BANG); + break; + case '=': + addToken(match('=') ? EQUAL_EQUAL : EQUAL); + break; + case '<': + addToken(match('=') ? LESS_EQUAL : LESS); + break; + case '>': + addToken(match('=') ? GREATER_EQUAL : GREATER); + break; + case '/': + if (match('/')) { + // A comment goes until the end of the line. + while (peek() != '\n' && !isAtEnd()) + advance(); + } else { + addToken(SLASH); + } + break; + + case ' ': + case '\r': + case '\t': + // Ignore whitespace. + break; // I guess this code isn't meant to run on Mac OS 9 and before. - case '\n': - line++; - break; + case '\n': + line++; + break; - case '"': string(); break; + case '"': + string(); + break; - default: - if (isDigit(c)) { - number(); - } else if (isAlpha(c)) { - identifier(); - } else { - Lox.error(line, "Unexpected character."); - } - break; + default: + if (isDigit(c)) { + number(); + } else if (isAlpha(c)) { + identifier(); + } else { + Lox.error(line, "Unexpected character."); + } + break; } } private void identifier() { - while (isAlphaNumeric(peek())) advance(); + while (isAlphaNumeric(peek())) + advance(); String text = source.substring(start, current); TokenType type = keywords.get(text); - if (type == null) type = IDENTIFIER; + if (type == null) + type = IDENTIFIER; addToken(type); } private void number() { - while (isDigit(peek())) advance(); + while (isDigit(peek())) + advance(); // Look for a fractional part. if (peek() == '.' && isDigit(peekNext())) { // Consume the "." advance(); - while (isDigit(peek())) advance(); + while (isDigit(peek())) + advance(); } - addToken(NUMBER, - Double.parseDouble(source.substring(start, current))); + addToken(NUMBER, Double.parseDouble(source.substring(start, current))); } // I guess we won't be able to include escaped characters in the string? At // least not escaped " characters. private void string() { while (peek() != '"' && !isAtEnd()) { - if (peek() == '\n') line++; + if (peek() == '\n') + line++; advance(); } @@ -156,27 +184,29 @@ class Scanner { } private boolean match(char expected) { - if (isAtEnd()) return false; - if (source.charAt(current) != expected) return false; + if (isAtEnd()) + return false; + if (source.charAt(current) != expected) + return false; current++; return true; } private char peek() { - if (isAtEnd()) return '\0'; + if (isAtEnd()) + return '\0'; return source.charAt(current); } private char peekNext() { - if (current + 1 >= source.length()) return '\0'; + if (current + 1 >= source.length()) + return '\0'; return source.charAt(current + 1); } private boolean isAlpha(char c) { - return (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - c == '_'; + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } private boolean isAlphaNumeric(char c) { diff --git a/src/com/craftinginterpreters/lox/TokenType.java b/src/com/craftinginterpreters/lox/TokenType.java index 9ab2a8b..f32ab81 100644 --- a/src/com/craftinginterpreters/lox/TokenType.java +++ b/src/com/craftinginterpreters/lox/TokenType.java @@ -2,19 +2,16 @@ package com.craftinginterpreters.lox; enum TokenType { // Single-character tokens. - LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, - SEMICOLON, SLASH, STAR, + LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR, // One or two character tokens. - BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, - LESS_EQUAL, + BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL, // Literals. IDENTIFIER, STRING, NUMBER, // Keywords. - AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS, - TRUE, VAR, WHILE, + AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE, BREAK, EOF } diff --git a/src/com/craftinginterpreters/tool/GenerateAst.java b/src/com/craftinginterpreters/tool/GenerateAst.java index 8845c6e..51b4ce3 100644 --- a/src/com/craftinginterpreters/tool/GenerateAst.java +++ b/src/com/craftinginterpreters/tool/GenerateAst.java @@ -22,7 +22,7 @@ public class GenerateAst { defineAst(outputDir, "Stmt", Arrays.asList("Block : List statements", "Expression : Expr expression", "If : Expr condition, Stmt thenBranch, Stmt elseBranch", "Print : Expr expression", - "Var : Token name, Expr initializer", "While : Expr condition, Stmt body")); + "Var : Token name, Expr initializer", "While : Expr condition, Stmt body", "Break : ")); } private static void defineAst(String outputDir, String baseName, List types) throws IOException { @@ -71,8 +71,10 @@ public class GenerateAst { // Store parameters in fields. String[] fields = fieldList.split(", "); for (String field : fields) { - String name = field.split(" ")[1]; - writer.println(" this." + name + " = " + name + ";"); + if (!field.isEmpty()) { + String name = field.split(" ")[1]; + writer.println(" this." + name + " = " + name + ";"); + } } writer.println(" }"); @@ -87,7 +89,9 @@ public class GenerateAst { // Fields writer.println(); for (String field : fields) { - writer.println(" final " + field + ";"); + if (!field.isEmpty()) { + writer.println(" final " + field + ";"); + } } writer.println(" }");