From 2d1bad5701871e0825751e2bd0d3aebf939cb246 Mon Sep 17 00:00:00 2001 From: ctsk <9384305+ctsk@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:27:16 +0200 Subject: [PATCH] Add assigments + scoping --- .../main/java/xyz/ctsk/lox/AstPrinter.java | 37 ++++++--- .../main/java/xyz/ctsk/lox/Environment.java | 43 +++++++++++ .../main/java/xyz/ctsk/lox/Interpreter.java | 67 +++++++++++++---- jlox/lox/src/main/java/xyz/ctsk/lox/Lox.java | 2 +- .../src/main/java/xyz/ctsk/lox/Parser.java | 75 +++++++++++++++++-- .../main/java/xyz/ctsk/lox/package-info.java | 10 ++- .../ctsk/lox/codegen/GrammarProcessor.java | 6 +- 7 files changed, 202 insertions(+), 38 deletions(-) create mode 100644 jlox/lox/src/main/java/xyz/ctsk/lox/Environment.java diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java b/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java index ac91f04..fa0be09 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java @@ -11,34 +11,47 @@ public class AstPrinter { private record Polish(boolean parentheses, boolean reverse) implements Expr.Visitor { @Override - public String visitBinaryExpr(Expr.Binary binary) { - var left = binary.left().accept(this); - var op = binary.operator().lexeme(); - var right = binary.right().accept(this); + public String visitAssignExpr(Expr.Assign expr) { + var target = expr.name().lexeme(); + var value = expr.value().accept(this); + + return reverse ? wrap(target, value, "=") : wrap("=", target, value); + } + + @Override + public String visitBinaryExpr(Expr.Binary expr) { + var left = expr.left().accept(this); + var op = expr.operator().lexeme(); + var right = expr.right().accept(this); return reverse ? wrap(left, right, op) : wrap(op, left, right); } @Override - public String visitGroupingExpr(Expr.Grouping grouping) { - var inner = grouping.expression().accept(this); + public String visitGroupingExpr(Expr.Grouping expr) { + var inner = expr.expression().accept(this); return wrap(inner); } @Override - public String visitLiteralExpr(Expr.Literal literal) { - if (literal.value() == null) return "nil"; - return literal.value().toString(); + public String visitLiteralExpr(Expr.Literal expr) { + if (expr.value() == null) return "nil"; + return expr.value().toString(); } @Override - public String visitUnaryExpr(Expr.Unary unary) { - var op = unary.operator().lexeme(); - var right = unary.right().accept(this); + public String visitUnaryExpr(Expr.Unary expr) { + var op = expr.operator().lexeme(); + var right = expr.right().accept(this); return reverse ? wrap(right, op) : wrap(op, right); } + @Override + public String visitVariableExpr(Expr.Variable expr) { + return expr.name().lexeme(); + } + public String wrap(String... inner) { var inners = String.join(" ", inner); diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/Environment.java b/jlox/lox/src/main/java/xyz/ctsk/lox/Environment.java new file mode 100644 index 0000000..18e2791 --- /dev/null +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Environment.java @@ -0,0 +1,43 @@ +package xyz.ctsk.lox; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class Environment { + private final Environment enclosing; + private final Map values = new HashMap<>(); + + Environment() { + enclosing = null; + } + + Environment(Environment enclosing) { + this.enclosing = enclosing; + } + + void define(String name, Object value) { + values.put(name, value); + } + + void assign(Token name, Object value) { + if (values.containsKey(name.lexeme())) { + values.put(name.lexeme(), value); + } else if (enclosing != null) { + enclosing.assign(name, value); + } else { + throw new RuntimeError(name, "Undefined variable '%s'.".formatted(name.lexeme())); + } + } + + Object get(Token name) { + if (values.containsKey(name.lexeme())) { + return values.get(name.lexeme()); + } else if (enclosing != null) { + return enclosing.get(name); + } else { + var message = "Undefined variable '%s'.".formatted(name.lexeme()); + throw new RuntimeError(name, message); + } + } +} diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java b/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java index f8023d3..e9f0bfe 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java @@ -3,6 +3,7 @@ package xyz.ctsk.lox; import java.util.List; public class Interpreter implements Expr.Visitor, Stmt.Visitor { + private Environment environment = new Environment(); void interpret(List statements) { try { @@ -16,6 +17,17 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { stmt.accept(this); } + private void executeBlock(List statements, Environment environment) { + Environment previous = this.environment; + try { + this.environment = environment; + + statements.forEach(this::execute); + } finally { + this.environment = previous; + } + } + private Object evaluate(Expr expr) { return expr.accept(this); } @@ -31,6 +43,12 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { return text; } + @Override + public Void visitBlockStmt(Stmt.Block stmt) { + executeBlock(stmt.statements(), new Environment(environment)); + return null; + } + @Override public Void visitExpressionStmt(Stmt.Expression stmt) { evaluate(stmt.expression()); @@ -45,16 +63,30 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { } @Override - public Object visitBinaryExpr(Expr.Binary binary) { - var left = evaluate(binary.left()); - var right = evaluate(binary.right()); + public Void visitVarStmt(Stmt.Var stmt) { + Object value = stmt.initializer() == null ? null : evaluate(stmt.initializer()); + environment.define(stmt.name().lexeme(), value); + return null; + } - switch (binary.operator().type()) { + @Override + public Object visitAssignExpr(Expr.Assign expr) { + var value = evaluate(expr.value()); + environment.assign(expr.name(), value); + return value; + } + + @Override + public Object visitBinaryExpr(Expr.Binary expr) { + var left = evaluate(expr.left()); + var right = evaluate(expr.right()); + + switch (expr.operator().type()) { case MINUS, SLASH, STAR, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL -> - checkNumberOperands(binary.operator(), left, right); + checkNumberOperands(expr.operator(), left, right); } - return switch (binary.operator().type()) { + return switch (expr.operator().type()) { case MINUS -> (double) left - (double) right; case PLUS -> { if (left instanceof Double leftD && right instanceof Double rightD) { @@ -65,7 +97,7 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { yield leftStr + rightStr; } - throw new RuntimeError(binary.operator(), "Operands must be two numbers or two strings."); + throw new RuntimeError(expr.operator(), "Operands must be two numbers or two strings."); } case SLASH -> (double) left / (double) right; case STAR -> (double) left * (double) right; @@ -80,26 +112,31 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { } @Override - public Object visitGroupingExpr(Expr.Grouping grouping) { - return evaluate(grouping.expression()); + public Object visitGroupingExpr(Expr.Grouping expr) { + return evaluate(expr.expression()); } @Override - public Object visitLiteralExpr(Expr.Literal literal) { - return literal.value(); + public Object visitLiteralExpr(Expr.Literal expr) { + return expr.value(); } @Override - public Object visitUnaryExpr(Expr.Unary unary) { - var right = evaluate(unary.right()); + public Object visitUnaryExpr(Expr.Unary expr) { + var right = evaluate(expr.right()); - return switch(unary.operator().type()) { - case MINUS -> -asNumber(unary.operator(), right); + return switch(expr.operator().type()) { + case MINUS -> -asNumber(expr.operator(), right); case BANG -> !isTruthy(right); default -> null; }; } + @Override + public Object visitVariableExpr(Expr.Variable expr) { + return environment.get(expr.name()); + } + private boolean isTruthy(Object object) { if (object == null) return false; diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/Lox.java b/jlox/lox/src/main/java/xyz/ctsk/lox/Lox.java index 79afdda..63711e8 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Lox.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Lox.java @@ -53,7 +53,7 @@ public class Lox { var reader = new BufferedReader(input); while (true) { - System.out.println("> "); + System.out.print("> "); String line = reader.readLine(); if (line == null) break; run(line); diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java b/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java index 274c1a5..10b924c 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java @@ -1,5 +1,6 @@ package xyz.ctsk.lox; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; @@ -9,12 +10,20 @@ import static xyz.ctsk.lox.TokenType.*; * * A recursive descent parser for the following grammar: *

- * program → statement* EOF ; + * program → declaration* EOF ; + * declaration → varDecl + * | statement ; + * varDecl → "var" IDENTIFIER ( "=" expression )? ";" ; * statement → exprStmt - * | printStmt ; + * | printStmt + * | block ; + * block → "{" declaration* "}" ; * exprStmt → expression ";" ; * printStmt → "print" expression ";" ; * expression → equality ; + * expression → assignment ; + * assignment → IDENTIFIER "=" assignment + * | equality ; * equality → comparison ( ( "!=" | "==" ) comparison )* ; * comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ; * term → factor ( ( "-" | "+" ) factor )* ; @@ -22,7 +31,8 @@ import static xyz.ctsk.lox.TokenType.*; * unary → ( "!" | "-" ) unary * | primary ; * primary → NUMBER | STRING | "true" | "false" | "nil" - * | "(" expression ")" ; + * | "(" expression ")" + * | IDENTIFIER; */ public class Parser { private final List tokens; @@ -37,16 +47,37 @@ public class Parser { List statements = new ArrayList<>(); while (!isAtEnd()) { - statements.add(statement()); + statements.add(declaration()); } return statements; } + private Stmt declaration() { + try { + return match(VAR) ? varDeclaration() : statement(); + } catch (ParseError error) { + synchronize(); + return null; + } + } + + private Stmt varDeclaration() { + Token name = consume(IDENTIFIER, "Expect variable name."); + + Expr initializer = null; + if (match(EQUAL)) { + initializer = expression(); + } + + consume(SEMICOLON, "Expect ';' after variable declaration."); + return new Stmt.Var(name, initializer); + } private Stmt statement() { if (match(PRINT)) return printStatement(); + if (match(LEFT_BRACE)) return new Stmt.Block(blockStatement()); return expressionStatement(); } private Stmt printStatement() { @@ -55,6 +86,17 @@ public class Parser { return new Stmt.Print(value); } + private List blockStatement() { + var statements = new ArrayList(); + + while (!check(RIGHT_BRACE) && !isAtEnd()) { + statements.add(declaration()); + } + + consume(RIGHT_BRACE, "Expect '}' after block."); + return statements; + } + private Stmt expressionStatement() { Expr expr = expression(); consume(SEMICOLON, "Expect ';' after expression."); @@ -62,7 +104,26 @@ public class Parser { } private Expr expression() { - return equality(); + return assignment(); + } + + private Expr assignment() { + Expr expr = equality(); + + if (match(EQUAL)) { + Token equals = previous(); + Expr value = assignment(); + + if (expr instanceof Expr.Variable varExpr) { + return new Expr.Assign(varExpr.name(), value); + } else { + //We report an error but don't throw it because we do not need our parser to panic and sync. + //noinspection ThrowableNotThrown + error(equals, "Invalid assignment target."); + } + } + + return expr; } private Expr equality() { @@ -132,6 +193,10 @@ public class Parser { return new Expr.Literal(previous().literal()); } + if (match(IDENTIFIER)) { + return new Expr.Variable(previous()); + } + if (match(LEFT_PAREN)) { Expr expr = expression(); consume(RIGHT_PAREN, "Expect ')' after expression."); diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/package-info.java b/jlox/lox/src/main/java/xyz/ctsk/lox/package-info.java index ea8249e..b98049c 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/package-info.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/package-info.java @@ -1,15 +1,19 @@ @Grammar({ @Root(name = "Expr", rules = { + @Rule(head = "Assign", body = {"Token name", "Expr value"}), @Rule(head = "Binary", body = {"Expr left", "Token operator", "Expr right"}), @Rule(head = "Grouping", body = {"Expr expression"}), @Rule(head = "Literal", body = {"Object value"}), - @Rule(head = "Unary", body = {"Token operator", "Expr right"}) + @Rule(head = "Unary", body = {"Token operator", "Expr right"}), + @Rule(head = "Variable", body = {"Token name"}) }), @Root(name = "Stmt", rules = { - @Rule(head = "Expression", body = "Expr expression"), - @Rule(head = "Print", body = "Expr expression") + @Rule(head = "Block", body = {"List statements"}), + @Rule(head = "Expression", body = {"Expr expression"}), + @Rule(head = "Print", body = {"Expr expression"}), + @Rule(head = "Var", body = {"Token name", "Expr initializer"}) }) }) package xyz.ctsk.lox; diff --git a/jlox/tools/src/main/java/xyz/ctsk/lox/codegen/GrammarProcessor.java b/jlox/tools/src/main/java/xyz/ctsk/lox/codegen/GrammarProcessor.java index dcab574..550757b 100644 --- a/jlox/tools/src/main/java/xyz/ctsk/lox/codegen/GrammarProcessor.java +++ b/jlox/tools/src/main/java/xyz/ctsk/lox/codegen/GrammarProcessor.java @@ -34,7 +34,7 @@ public class GrammarProcessor extends AbstractProcessor { for (var rule : rules) { var visitorName = getVisitorName(rule.head(), parentName); - var parameter = "%s %s".formatted(rule.head(), rule.head().toLowerCase()); + var parameter = "%s %s".formatted(rule.head(), parentName.toLowerCase()); writer.write(""" R %s(%s); """.formatted(visitorName, parameter).indent(indent + INDENT)); @@ -71,6 +71,8 @@ public class GrammarProcessor extends AbstractProcessor { writer.write(""" package %s; + import java.util.List; + interface %s { """.formatted(packageName, baseName) ); @@ -80,7 +82,7 @@ public class GrammarProcessor extends AbstractProcessor { writer.write(""" R accept(Visitor visitor); - """ + """.indent(INDENT) ); for (var rule : root.rules()) {