Add assigments + scoping

This commit is contained in:
ctsk
2022-09-09 12:27:16 +02:00
parent 78cb1521df
commit 2d1bad5701
7 changed files with 202 additions and 38 deletions

View File

@@ -11,34 +11,47 @@ public class AstPrinter {
private record Polish(boolean parentheses, boolean reverse) implements Expr.Visitor<String> { private record Polish(boolean parentheses, boolean reverse) implements Expr.Visitor<String> {
@Override @Override
public String visitBinaryExpr(Expr.Binary binary) { public String visitAssignExpr(Expr.Assign expr) {
var left = binary.left().accept(this); var target = expr.name().lexeme();
var op = binary.operator().lexeme(); var value = expr.value().accept(this);
var right = binary.right().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); return reverse ? wrap(left, right, op) : wrap(op, left, right);
} }
@Override @Override
public String visitGroupingExpr(Expr.Grouping grouping) { public String visitGroupingExpr(Expr.Grouping expr) {
var inner = grouping.expression().accept(this); var inner = expr.expression().accept(this);
return wrap(inner); return wrap(inner);
} }
@Override @Override
public String visitLiteralExpr(Expr.Literal literal) { public String visitLiteralExpr(Expr.Literal expr) {
if (literal.value() == null) return "nil"; if (expr.value() == null) return "nil";
return literal.value().toString(); return expr.value().toString();
} }
@Override @Override
public String visitUnaryExpr(Expr.Unary unary) { public String visitUnaryExpr(Expr.Unary expr) {
var op = unary.operator().lexeme(); var op = expr.operator().lexeme();
var right = unary.right().accept(this); var right = expr.right().accept(this);
return reverse ? wrap(right, op) : wrap(op, right); return reverse ? wrap(right, op) : wrap(op, right);
} }
@Override
public String visitVariableExpr(Expr.Variable expr) {
return expr.name().lexeme();
}
public String wrap(String... inner) { public String wrap(String... inner) {
var inners = String.join(" ", inner); var inners = String.join(" ", inner);

View File

@@ -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<String, Object> 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);
}
}
}

View File

@@ -3,6 +3,7 @@ package xyz.ctsk.lox;
import java.util.List; import java.util.List;
public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> { public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
private Environment environment = new Environment();
void interpret(List<Stmt> statements) { void interpret(List<Stmt> statements) {
try { try {
@@ -16,6 +17,17 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
stmt.accept(this); stmt.accept(this);
} }
private void executeBlock(List<Stmt> statements, Environment environment) {
Environment previous = this.environment;
try {
this.environment = environment;
statements.forEach(this::execute);
} finally {
this.environment = previous;
}
}
private Object evaluate(Expr expr) { private Object evaluate(Expr expr) {
return expr.accept(this); return expr.accept(this);
} }
@@ -31,6 +43,12 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
return text; return text;
} }
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
executeBlock(stmt.statements(), new Environment(environment));
return null;
}
@Override @Override
public Void visitExpressionStmt(Stmt.Expression stmt) { public Void visitExpressionStmt(Stmt.Expression stmt) {
evaluate(stmt.expression()); evaluate(stmt.expression());
@@ -45,16 +63,30 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
} }
@Override @Override
public Object visitBinaryExpr(Expr.Binary binary) { public Void visitVarStmt(Stmt.Var stmt) {
var left = evaluate(binary.left()); Object value = stmt.initializer() == null ? null : evaluate(stmt.initializer());
var right = evaluate(binary.right()); 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 -> 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 MINUS -> (double) left - (double) right;
case PLUS -> { case PLUS -> {
if (left instanceof Double leftD && right instanceof Double rightD) { if (left instanceof Double leftD && right instanceof Double rightD) {
@@ -65,7 +97,7 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
yield leftStr + rightStr; 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 SLASH -> (double) left / (double) right;
case STAR -> (double) left * (double) right; case STAR -> (double) left * (double) right;
@@ -80,26 +112,31 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
} }
@Override @Override
public Object visitGroupingExpr(Expr.Grouping grouping) { public Object visitGroupingExpr(Expr.Grouping expr) {
return evaluate(grouping.expression()); return evaluate(expr.expression());
} }
@Override @Override
public Object visitLiteralExpr(Expr.Literal literal) { public Object visitLiteralExpr(Expr.Literal expr) {
return literal.value(); return expr.value();
} }
@Override @Override
public Object visitUnaryExpr(Expr.Unary unary) { public Object visitUnaryExpr(Expr.Unary expr) {
var right = evaluate(unary.right()); var right = evaluate(expr.right());
return switch(unary.operator().type()) { return switch(expr.operator().type()) {
case MINUS -> -asNumber(unary.operator(), right); case MINUS -> -asNumber(expr.operator(), right);
case BANG -> !isTruthy(right); case BANG -> !isTruthy(right);
default -> null; default -> null;
}; };
} }
@Override
public Object visitVariableExpr(Expr.Variable expr) {
return environment.get(expr.name());
}
private boolean isTruthy(Object object) { private boolean isTruthy(Object object) {
if (object == null) return false; if (object == null) return false;

View File

@@ -53,7 +53,7 @@ public class Lox {
var reader = new BufferedReader(input); var reader = new BufferedReader(input);
while (true) { while (true) {
System.out.println("> "); System.out.print("> ");
String line = reader.readLine(); String line = reader.readLine();
if (line == null) break; if (line == null) break;
run(line); run(line);

View File

@@ -1,5 +1,6 @@
package xyz.ctsk.lox; package xyz.ctsk.lox;
import java.sql.Statement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -9,12 +10,20 @@ import static xyz.ctsk.lox.TokenType.*;
* *
* A recursive descent parser for the following grammar: * A recursive descent parser for the following grammar:
* <p> * <p>
* program → statement* EOF ; * program → declaration* EOF ;
* declaration → varDecl
* | statement ;
* varDecl → "var" IDENTIFIER ( "=" expression )? ";" ;
* statement → exprStmt * statement → exprStmt
* | printStmt ; * | printStmt
* | block ;
* block → "{" declaration* "}" ;
* exprStmt → expression ";" ; * exprStmt → expression ";" ;
* printStmt → "print" expression ";" ; * printStmt → "print" expression ";" ;
* expression → equality ; * expression → equality ;
* expression → assignment ;
* assignment → IDENTIFIER "=" assignment
* | equality ;
* equality → comparison ( ( "!=" | "==" ) comparison )* ; * equality → comparison ( ( "!=" | "==" ) comparison )* ;
* comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ; * comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
* term → factor ( ( "-" | "+" ) factor )* ; * term → factor ( ( "-" | "+" ) factor )* ;
@@ -22,7 +31,8 @@ import static xyz.ctsk.lox.TokenType.*;
* unary → ( "!" | "-" ) unary * unary → ( "!" | "-" ) unary
* | primary ; * | primary ;
* primary → NUMBER | STRING | "true" | "false" | "nil" * primary → NUMBER | STRING | "true" | "false" | "nil"
* | "(" expression ")" ; * | "(" expression ")"
* | IDENTIFIER;
*/ */
public class Parser { public class Parser {
private final List<Token> tokens; private final List<Token> tokens;
@@ -37,16 +47,37 @@ public class Parser {
List<Stmt> statements = new ArrayList<>(); List<Stmt> statements = new ArrayList<>();
while (!isAtEnd()) { while (!isAtEnd()) {
statements.add(statement()); statements.add(declaration());
} }
return statements; 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() { private Stmt statement() {
if (match(PRINT)) return printStatement(); if (match(PRINT)) return printStatement();
if (match(LEFT_BRACE)) return new Stmt.Block(blockStatement());
return expressionStatement(); return expressionStatement();
} }
private Stmt printStatement() { private Stmt printStatement() {
@@ -55,6 +86,17 @@ public class Parser {
return new Stmt.Print(value); return new Stmt.Print(value);
} }
private List<Stmt> blockStatement() {
var statements = new ArrayList<Stmt>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
statements.add(declaration());
}
consume(RIGHT_BRACE, "Expect '}' after block.");
return statements;
}
private Stmt expressionStatement() { private Stmt expressionStatement() {
Expr expr = expression(); Expr expr = expression();
consume(SEMICOLON, "Expect ';' after expression."); consume(SEMICOLON, "Expect ';' after expression.");
@@ -62,7 +104,26 @@ public class Parser {
} }
private Expr expression() { 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() { private Expr equality() {
@@ -132,6 +193,10 @@ public class Parser {
return new Expr.Literal(previous().literal()); return new Expr.Literal(previous().literal());
} }
if (match(IDENTIFIER)) {
return new Expr.Variable(previous());
}
if (match(LEFT_PAREN)) { if (match(LEFT_PAREN)) {
Expr expr = expression(); Expr expr = expression();
consume(RIGHT_PAREN, "Expect ')' after expression."); consume(RIGHT_PAREN, "Expect ')' after expression.");

View File

@@ -1,15 +1,19 @@
@Grammar({ @Grammar({
@Root(name = "Expr", @Root(name = "Expr",
rules = { rules = {
@Rule(head = "Assign", body = {"Token name", "Expr value"}),
@Rule(head = "Binary", body = {"Expr left", "Token operator", "Expr right"}), @Rule(head = "Binary", body = {"Expr left", "Token operator", "Expr right"}),
@Rule(head = "Grouping", body = {"Expr expression"}), @Rule(head = "Grouping", body = {"Expr expression"}),
@Rule(head = "Literal", body = {"Object value"}), @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", @Root(name = "Stmt",
rules = { rules = {
@Rule(head = "Expression", body = "Expr expression"), @Rule(head = "Block", body = {"List<Stmt> statements"}),
@Rule(head = "Print", body = "Expr expression") @Rule(head = "Expression", body = {"Expr expression"}),
@Rule(head = "Print", body = {"Expr expression"}),
@Rule(head = "Var", body = {"Token name", "Expr initializer"})
}) })
}) })
package xyz.ctsk.lox; package xyz.ctsk.lox;

View File

@@ -34,7 +34,7 @@ public class GrammarProcessor extends AbstractProcessor {
for (var rule : rules) { for (var rule : rules) {
var visitorName = getVisitorName(rule.head(), parentName); 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(""" writer.write("""
R %s(%s); R %s(%s);
""".formatted(visitorName, parameter).indent(indent + INDENT)); """.formatted(visitorName, parameter).indent(indent + INDENT));
@@ -71,6 +71,8 @@ public class GrammarProcessor extends AbstractProcessor {
writer.write(""" writer.write("""
package %s; package %s;
import java.util.List;
interface %s { interface %s {
""".formatted(packageName, baseName) """.formatted(packageName, baseName)
); );
@@ -80,7 +82,7 @@ public class GrammarProcessor extends AbstractProcessor {
writer.write(""" writer.write("""
<R> R accept(Visitor<R> visitor); <R> R accept(Visitor<R> visitor);
""" """.indent(INDENT)
); );
for (var rule : root.rules()) { for (var rule : root.rules()) {