From 4844e4344789e9d280485b5917a6a6ae67559118 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Sun, 9 Feb 2025 13:57:27 +0100 Subject: [PATCH] AST in java --- .../ch/vanwa/lox_interpreter/AstPrinter.java | 43 ++++ .../java/ch/vanwa/lox_interpreter/Expr.java | 35 +++ .../java/ch/vanwa/lox_interpreter/Lox.java | 28 ++- .../java/ch/vanwa/lox_interpreter/Parser.java | 204 ++++++++++++++++++ .../main/java/ch/vanwa/tool/GenerateAst.java | 27 +++ .../ch/vanwa/lox_interpreter/AppTest.java | 3 +- 6 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/AstPrinter.java create mode 100644 java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Parser.java diff --git a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/AstPrinter.java b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/AstPrinter.java new file mode 100644 index 0000000..0e30ff5 --- /dev/null +++ b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/AstPrinter.java @@ -0,0 +1,43 @@ +package ch.vanwa.lox_interpreter; + +class AstPrinter implements Expr.Visitor { + String print(Expr expr) { + return expr.accept(this); + } + + @Override + public String visitBinaryExpr(Expr.Binary expr) { + return parenthesize(expr.operator.lexeme(), + expr.left, expr.right); + } + + @Override + public String visitGroupingExpr(Expr.Grouping expr) { + return parenthesize("group", expr.expression); + } + + @Override + public String visitLiteralExpr(Expr.Literal expr) { + if (expr.value == null) + return "nil"; + return expr.value.toString(); + } + + @Override + public String visitUnaryExpr(Expr.Unary expr) { + return parenthesize(expr.operator.lexeme(), expr.right); + } + + private String parenthesize(String name, Expr... exprs) { + StringBuilder builder = new StringBuilder(); + + builder.append("(").append(name); + for (Expr expr : exprs) { + builder.append(" "); + builder.append(expr.accept(this)); + } + builder.append(")"); + + return builder.toString(); + } +} diff --git a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Expr.java b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Expr.java index ab28035..eb73c65 100644 --- a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Expr.java +++ b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Expr.java @@ -3,6 +3,16 @@ package ch.vanwa.lox_interpreter; import java.util.List; abstract class Expr { + interface Visitor { + R visitBinaryExpr(Binary expr); + + R visitGroupingExpr(Grouping expr); + + R visitLiteralExpr(Literal expr); + + R visitUnaryExpr(Unary expr); + } + static class Binary extends Expr { Binary(Expr left, Token operator, Expr right) { this.left = left; @@ -10,31 +20,56 @@ abstract class Expr { this.right = right; } + @Override + R accept(Visitor visitor) { + return visitor.visitBinaryExpr(this); + } + final Expr left; final Token operator; final Expr right; } + static class Grouping extends Expr { Grouping(Expr expression) { this.expression = expression; } + @Override + R accept(Visitor visitor) { + return visitor.visitGroupingExpr(this); + } + final Expr expression; } + static class Literal extends Expr { Literal(Object value) { this.value = value; } + @Override + R accept(Visitor visitor) { + return visitor.visitLiteralExpr(this); + } + final Object value; } + static class Unary extends Expr { Unary(Token operator, Expr right) { this.operator = operator; this.right = right; } + @Override + R accept(Visitor visitor) { + return visitor.visitUnaryExpr(this); + } + final Token operator; final Expr right; } + + abstract R accept(Visitor visitor); } diff --git a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Lox.java b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Lox.java index cb1c8b0..aede878 100644 --- a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Lox.java +++ b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Lox.java @@ -55,19 +55,19 @@ public class Lox { * * @return List of scanned tokens. */ - public static List run(String source) { + public static void run(String source) { var scanner = new Scanner(source); List tokens = scanner.scanTokens(); + Parser parser = new Parser(tokens); + Expr expression = parser.parse(); + if (hadError) + return; - for (Token token : tokens) { - System.out.println(token); - } - - return tokens; + System.out.println(new AstPrinter().print(expression)); } /** - * Print an error message. + * Print a simple error message. * * @param line Line number where the error ocurred. * @param message Error message. @@ -76,6 +76,20 @@ public class Lox { report(line, "", message); } + /** + * Print an error message for a token. + * + * @param line Line number where the error ocurred. + * @param message Error message. + */ + static void error(Token token, String message) { + if (token.type() == TokenType.EOF) { + report(token.line(), " at end", message); + } else { + report(token.line(), " at '" + token.lexeme() + "'", message); + } + } + /** * Print a message to stderr. * diff --git a/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Parser.java b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Parser.java new file mode 100644 index 0000000..3ca0fbc --- /dev/null +++ b/java/lox-interpreter/src/main/java/ch/vanwa/lox_interpreter/Parser.java @@ -0,0 +1,204 @@ +package ch.vanwa.lox_interpreter; + +import java.util.List; + +import static ch.vanwa.lox_interpreter.TokenType.*; + +/** + * Parse tokens into an AST. + * + * Implement the grammar specified at + * https://craftinginterpreters.com/appendix-i.html#expressions + */ +class Parser { + /** + * Thrown if a parsing error occurs. + */ + private static class ParseError extends RuntimeException { + } + + /** + * List of tokens to parse. + */ + private final List tokens; + + /** + * Index of the current token. + */ + private int current = 0; + + /** + * Initialize a new parser with the given tokens. + * + * @param tokens List of tokens to parse. + */ + Parser(List tokens) { + this.tokens = tokens; + } + + /** + * Start parsing. + * + * @return Expression signifying the whole program or null, if parsing fails. + */ + Expr parse() { + try { + return expression(); + } catch (ParseError error) { + return null; + } + } + + private Expr expression() { + return equality(); + } + + private Expr equality() { + Expr expr = comparison(); + + while (match(BANG_EQUAL, EQUAL_EQUAL)) { + Token operator = previous(); + Expr right = comparison(); + expr = new Expr.Binary(expr, operator, right); + } + + return expr; + } + + private Expr comparison() { + Expr expr = term(); + + while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) { + Token operator = previous(); + Expr right = term(); + expr = new Expr.Binary(expr, operator, right); + } + + return expr; + } + + private Expr term() { + Expr expr = factor(); + + while (match(MINUS, PLUS)) { + Token operator = previous(); + Expr right = factor(); + expr = new Expr.Binary(expr, operator, right); + } + + return expr; + } + + private Expr factor() { + Expr expr = unary(); + + while (match(SLASH, STAR)) { + Token operator = previous(); + Expr right = unary(); + expr = new Expr.Binary(expr, operator, right); + } + + return expr; + } + + private Expr unary() { + if (match(BANG, MINUS)) { + Token operator = previous(); + Expr right = unary(); + return new Expr.Unary(operator, right); + } + + return primary(); + } + + private Expr primary() { + if (match(FALSE)) + return new Expr.Literal(false); + if (match(TRUE)) + return new Expr.Literal(true); + if (match(NIL)) + return new Expr.Literal(null); + + if (match(NUMBER, STRING)) { + return new Expr.Literal(previous().literal()); + } + + if (match(LEFT_PAREN)) { + Expr expr = expression(); + consume(RIGHT_PAREN, "Expect ')' after expression."); + return new Expr.Grouping(expr); + } + + throw error(peek(), "Expect expression."); + } + + private boolean match(TokenType... types) { + for (TokenType type : types) { + if (check(type)) { + advance(); + return true; + } + } + + return false; + } + + private Token consume(TokenType type, String message) { + if (check(type)) + return advance(); + + throw error(peek(), message); + } + + private boolean check(TokenType type) { + if (isAtEnd()) + return false; + return peek().type() == type; + } + + private Token advance() { + if (!isAtEnd()) + current++; + return previous(); + } + + private boolean isAtEnd() { + return peek().type() == EOF; + } + + private Token peek() { + return tokens.get(current); + } + + private Token previous() { + return tokens.get(current - 1); + } + + private ParseError error(Token token, String message) { + Lox.error(token, message); + return new ParseError(); + } + + private void synchronize() { + advance(); + + while (!isAtEnd()) { + if (previous().type() == SEMICOLON) + return; + + switch (peek().type()) { + case CLASS: + case FUN: + case VAR: + case FOR: + case IF: + case WHILE: + case PRINT: + case RETURN: + return; + } + + advance(); + } + } +} diff --git a/java/lox-interpreter/src/main/java/ch/vanwa/tool/GenerateAst.java b/java/lox-interpreter/src/main/java/ch/vanwa/tool/GenerateAst.java index ad6e201..40daee3 100644 --- a/java/lox-interpreter/src/main/java/ch/vanwa/tool/GenerateAst.java +++ b/java/lox-interpreter/src/main/java/ch/vanwa/tool/GenerateAst.java @@ -32,6 +32,8 @@ public class GenerateAst { writer.println(); writer.println("abstract class " + baseName + " {"); + defineVisitor(writer, baseName, types); + // The AST classes. for (String type : types) { String className = type.split(":")[0].trim(); @@ -39,6 +41,10 @@ public class GenerateAst { defineType(writer, baseName, className, fields); } + // The base accept() method. + writer.println(); + writer.println(" abstract R accept(Visitor visitor);"); + writer.println("}"); writer.close(); } @@ -61,6 +67,14 @@ public class GenerateAst { writer.println(" }"); + // Visitor pattern. + writer.println(); + writer.println(" @Override"); + writer.println(" R accept(Visitor visitor) {"); + writer.println(" return visitor.visit" + + className + baseName + "(this);"); + writer.println(" }"); + // Fields. writer.println(); for (String field : fields) { @@ -69,4 +83,17 @@ public class GenerateAst { writer.println(" }"); } + + private static void defineVisitor( + PrintWriter writer, String baseName, List types) { + writer.println(" interface Visitor {"); + + for (String type : types) { + String typeName = type.split(":")[0].trim(); + writer.println(" R visit" + typeName + baseName + "(" + + typeName + " " + baseName.toLowerCase() + ");"); + } + + writer.println(" }"); + } } diff --git a/java/lox-interpreter/src/test/java/ch/vanwa/lox_interpreter/AppTest.java b/java/lox-interpreter/src/test/java/ch/vanwa/lox_interpreter/AppTest.java index d5f92b2..4b8dc4a 100644 --- a/java/lox-interpreter/src/test/java/ch/vanwa/lox_interpreter/AppTest.java +++ b/java/lox-interpreter/src/test/java/ch/vanwa/lox_interpreter/AppTest.java @@ -14,7 +14,8 @@ public class AppTest { */ @Test public void scanOneLine() { - var tokens = Lox.run("var language = \"lox\""); + final var scanner = new Scanner("var language = \"lox\""); + final var tokens = scanner.scanTokens(); assertEquals(5, tokens.size());