From f1eeff583c0f6b69f37ef21551f9c54150dfa10e Mon Sep 17 00:00:00 2001 From: ctsk <9384305+ctsk@users.noreply.github.com> Date: Sat, 17 Sep 2022 22:17:52 +0200 Subject: [PATCH] [jlox] Classes --- .../main/java/xyz/ctsk/lox/AstPrinter.java | 38 +++++++++++++ .../main/java/xyz/ctsk/lox/FunctionType.java | 2 +- .../main/java/xyz/ctsk/lox/Interpreter.java | 45 ++++++++++++++- .../src/main/java/xyz/ctsk/lox/LoxClass.java | 44 +++++++++++++++ .../main/java/xyz/ctsk/lox/LoxFunction.java | 14 ++++- .../main/java/xyz/ctsk/lox/LoxInstance.java | 33 +++++++++++ .../src/main/java/xyz/ctsk/lox/Parser.java | 35 ++++++++++-- .../src/main/java/xyz/ctsk/lox/Resolver.java | 56 +++++++++++++++++++ .../main/java/xyz/ctsk/lox/package-info.java | 4 ++ 9 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 jlox/lox/src/main/java/xyz/ctsk/lox/LoxClass.java create mode 100644 jlox/lox/src/main/java/xyz/ctsk/lox/LoxInstance.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 69a8e32..dd55bb0 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/AstPrinter.java @@ -33,6 +33,12 @@ public class AstPrinter { return reverse ? wrap(left, right, op) : wrap(op, left, right); } + @Override + public String visitGetExpr(Expr.Get expr) { + var left = expr.object().accept(this); + return left + "." + expr.name().lexeme(); + } + @Override public String visitCallExpr(Expr.Call expr) { var fun = expr.callee().accept(this); @@ -64,6 +70,20 @@ public class AstPrinter { return reverse ? wrap(left, right, op) : wrap(op, left, right); } + @Override + public String visitSetExpr(Expr.Set expr) { + + + var left = expr.object().accept(this); + var val = expr.object().accept(this); + return wrap("=", left + "." + expr.name().lexeme(), val); + } + + @Override + public String visitThisExpr(Expr.This expr) { + return "this"; + } + @Override public String visitUnaryExpr(Expr.Unary expr) { var op = expr.operator().lexeme(); @@ -102,6 +122,12 @@ public class AstPrinter { expr.right().accept(this)); } + @Override + public String visitGetExpr(Expr.Get expr) { + var left = expr.object().accept(this); + return left + "." + expr.name().lexeme(); + } + @Override public String visitCallExpr(Expr.Call expr) { var fun = expr.callee().accept(this); @@ -129,6 +155,18 @@ public class AstPrinter { expr.right().accept(this)); } + @Override + public String visitSetExpr(Expr.Set expr) { + var left = expr.object().accept(this); + var val = expr.object().accept(this); + return left + "." + expr.name().lexeme() + " = " + val; + } + + @Override + public String visitThisExpr(Expr.This expr) { + return "this"; + } + @Override public String visitUnaryExpr(Expr.Unary expr) { return expr.operator().lexeme() + expr.right().accept(this); diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/FunctionType.java b/jlox/lox/src/main/java/xyz/ctsk/lox/FunctionType.java index 9218a77..172a084 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/FunctionType.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/FunctionType.java @@ -3,5 +3,5 @@ package xyz.ctsk.lox; public enum FunctionType { NONE, FUNCTION, - METHOD + INITIALIZER, METHOD } 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 fea0580..420b562 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Interpreter.java @@ -78,6 +78,21 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Void visitClassStmt(Stmt.Class stmt) { + environment.define(stmt.name().lexeme(), null); + + Map methods = new HashMap<>(); + for (var method : stmt.methods()) { + var function = new LoxFunction(method, environment, method.name().lexeme().equals("init")); + methods.put(method.name().lexeme(), function); + } + + LoxClass clazz = new LoxClass(stmt.name().lexeme(), methods); + environment.assign(stmt.name(), clazz); + return null; + } + @Override public Void visitExpressionStmt(Stmt.Expression stmt) { evaluate(stmt.expression()); @@ -86,7 +101,7 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { @Override public Void visitFunctionStmt(Stmt.Function stmt) { - environment.define(stmt.name().lexeme(), new LoxFunction(stmt, environment)); + environment.define(stmt.name().lexeme(), new LoxFunction(stmt, environment, false)); return null; } @@ -177,6 +192,15 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { }; } + @Override + public Object visitGetExpr(Expr.Get expr) { + var object = evaluate(expr.object()); + if (object instanceof LoxInstance loxInstance) { + return loxInstance.get(expr.name()); + } + throw new RuntimeError(expr.name(), "Only instances have properties."); + } + @Override public Object visitCallExpr(Expr.Call expr) { var callee = evaluate(expr.callee()); @@ -219,6 +243,23 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { return evaluate(expr.right()); } + @Override + public Object visitSetExpr(Expr.Set expr) { + var object = evaluate(expr.object()); + if (object instanceof LoxInstance instance) { + var value = evaluate(expr.value()); + instance.set(expr.name(), value); + return value; + } else { + throw new RuntimeError(expr.name(), "Only instances have fields."); + } + } + + @Override + public Object visitThisExpr(Expr.This expr) { + return lookupVariable(expr.keyword(), expr); + } + @Override public Object visitUnaryExpr(Expr.Unary expr) { var right = evaluate(expr.right()); @@ -235,7 +276,7 @@ public class Interpreter implements Expr.Visitor, Stmt.Visitor { return lookupVariable(expr.name(), expr); } - private Object lookupVariable(Token name, Expr.Variable expr) { + private Object lookupVariable(Token name, Expr expr) { var distance = locals.get(expr); if (distance != null) { return environment.getAt(distance, name.lexeme()); diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/LoxClass.java b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxClass.java new file mode 100644 index 0000000..4c04e8a --- /dev/null +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxClass.java @@ -0,0 +1,44 @@ +package xyz.ctsk.lox; + +import java.util.List; +import java.util.Map; + +public class LoxClass implements LoxCallable { + final String name; + private final Map methods; + + LoxClass(String name, Map methods) { + this.name = name; + this.methods = methods; + } + + @Override + public String toString() { + return name; + } + + @Override + public int arity() { + LoxFunction initializer = findMethod("init"); + if (initializer == null) return 0; + return initializer.arity(); + } + + @Override + public Object call(Interpreter interpreter, List arguments) { + var instance = new LoxInstance(this); + LoxFunction initializer = findMethod("init"); + if (initializer != null) { + initializer.bind(instance).call(interpreter, arguments); + } + return instance; + } + + LoxFunction findMethod(String name) { + if (methods.containsKey(name)) { + return methods.get(name); + } + + return null; + } +} diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/LoxFunction.java b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxFunction.java index cb5114f..11f92f3 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/LoxFunction.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxFunction.java @@ -6,9 +6,12 @@ public class LoxFunction implements LoxCallable { private final Stmt.Function declaration; private final Environment closure; - public LoxFunction(Stmt.Function declaration, Environment closure) { + private final boolean isInitializer; + + public LoxFunction(Stmt.Function declaration, Environment closure, boolean isInitializer) { this.declaration = declaration; this.closure = closure; + this.isInitializer = isInitializer; } @Override @@ -27,11 +30,20 @@ public class LoxFunction implements LoxCallable { try { interpreter.executeBlock(declaration.body(), environment); } catch (Return ret) { + if (isInitializer) return closure.getAt(0, "this"); return ret.value; } + + if (isInitializer) return closure.getAt(0, "this"); return null; } + LoxFunction bind(LoxInstance instance) { + Environment env = new Environment(closure); + env.define("this", instance); + return new LoxFunction(declaration, env, isInitializer); + } + @Override public String toString() { return ""; diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/LoxInstance.java b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxInstance.java new file mode 100644 index 0000000..286ea46 --- /dev/null +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/LoxInstance.java @@ -0,0 +1,33 @@ +package xyz.ctsk.lox; + +import java.util.HashMap; +import java.util.Map; + +public class LoxInstance { + private LoxClass clazz; + private final Map fields = new HashMap<>(); + + LoxInstance(LoxClass clazz) { + this.clazz = clazz; + } + + Object get(Token name) { + if (fields.containsKey(name.lexeme())) { + return fields.get(name.lexeme()); + } + + LoxFunction method = clazz.findMethod(name.lexeme()); + if (method != null) return method.bind(this); + + throw new RuntimeError(name, "Undefined property '%s'.".formatted(name.lexeme())); + } + + void set(Token name, Object value) { + fields.put(name.lexeme(), value); + } + + @Override + public String toString() { + return "<" + clazz.name + " instance>"; + } +} 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 8236bc0..494b482 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Parser.java @@ -10,9 +10,11 @@ import static xyz.ctsk.lox.TokenType.*; * A recursive descent parser for the following grammar: *

* program → declaration* EOF ; - * declaration → funDecl + * declaration → classDecl + * | funDecl * | varDecl * | statement ; + * classDecl → "class" IDENTIFIER "{" function* "}" ; * funDecl → "fun" function ; * function → IDENTIFIER "(" parameters? ")" block ; * parameters → IDENTIFIER ( "," IDENTIFIER )* ; @@ -35,7 +37,7 @@ import static xyz.ctsk.lox.TokenType.*; * exprStmt → expression ";" ; * expression → equality ; * expression → assignment ; - * assignment → IDENTIFIER "=" assignment + * assignment → ( call "." )? IDENTIFIER "=" assignment * | logic_or ; * logic_or → logic_and ( "or" logic_and )* ; * logic_and → equality ( "and" equality )* ; @@ -45,11 +47,12 @@ import static xyz.ctsk.lox.TokenType.*; * factor → unary ( ( "/" | "*" ) unary )* ; * unary → ( "!" | "-" ) unary * | call ; - * call → primary ( "(" arguments? ")" )* ; + * call → primary ( "(" arguments? ")" | "." IDENTIFIER )* ; * arguments → expression ( "," expression )* ; * primary → NUMBER | STRING | "true" | "false" | "nil" * | "(" expression ")" - * | IDENTIFIER; + * | IDENTIFIER + * | THIS ; */ public class Parser { private final List tokens; @@ -72,6 +75,7 @@ public class Parser { private Stmt declaration() { try { + if (match(CLASS)) return classDeclaration(); if (match(FUN)) return function(FunctionType.FUNCTION); if (match(VAR)) return varDeclaration(); return match(VAR) ? varDeclaration() : statement(); @@ -81,6 +85,21 @@ public class Parser { } } + private Stmt classDeclaration() { + var name = consume(IDENTIFIER, "Expect class name."); + consume(LEFT_BRACE, "Expect '{' before class body."); + + List methods = new ArrayList<>(); + + while (!check(RIGHT_BRACE) && !isAtEnd()) { + methods.add(function(FunctionType.METHOD)); + } + + consume(LEFT_BRACE, "Expect '}' after class body."); + return new Stmt.Class(name, methods); + + } + private Stmt.Function function(FunctionType kind) { Token name = consume(IDENTIFIER, "Expect %s name.".formatted(kind.toString().toLowerCase())); @@ -232,6 +251,8 @@ public class Parser { if (expr instanceof Expr.Variable varExpr) { return new Expr.Assign(varExpr.name(), value); + } else if (expr instanceof Expr.Get getExpr) { + return new Expr.Set(getExpr.object(), getExpr.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 @@ -330,7 +351,9 @@ public class Parser { while(true) { if (match(LEFT_PAREN)) { expr = finishCall(expr); - } else { + } else if (match(DOT)) { + Token name = consume(IDENTIFIER, "Expect property name after '.'."); + expr = new Expr.Get(expr, name); break; } } @@ -365,6 +388,8 @@ public class Parser { return new Expr.Literal(previous().literal()); } + if (match(THIS)) return new Expr.This(previous()); + if (match(IDENTIFIER)) { return new Expr.Variable(previous()); } diff --git a/jlox/lox/src/main/java/xyz/ctsk/lox/Resolver.java b/jlox/lox/src/main/java/xyz/ctsk/lox/Resolver.java index 0bb747e..00c7fa2 100644 --- a/jlox/lox/src/main/java/xyz/ctsk/lox/Resolver.java +++ b/jlox/lox/src/main/java/xyz/ctsk/lox/Resolver.java @@ -11,6 +11,10 @@ public class Resolver implements Expr.Visitor, Stmt.Visitor { private final Stack> scopes = new Stack<>(); private FunctionType currentFuntion = FunctionType.NONE; + private enum ClassType { NONE, CLASS } + + private ClassType currentClass = ClassType.NONE; + Resolver(Interpreter interpreter) { this.interpreter = interpreter; @@ -82,6 +86,31 @@ public class Resolver implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Void visitClassStmt(Stmt.Class stmt) { + var enclosing = currentClass; + currentClass = ClassType.CLASS; + declare(stmt.name()); + define(stmt.name()); + beginScope(); + scopes.peek().put("this", true); + + for (var method : stmt.methods()) { + FunctionType declaration = FunctionType.METHOD; + + if (method.name().lexeme().equals("init")) { + declaration = FunctionType.INITIALIZER; + } + + resolveFunction(method, declaration); + } + + endScope(); + + currentClass = enclosing; + return null; + } + @Override public Void visitExpressionStmt(Stmt.Expression stmt) { @@ -118,6 +147,9 @@ public class Resolver implements Expr.Visitor, Stmt.Visitor { } if (stmt.value() != null) { + if (currentFuntion == FunctionType.INITIALIZER) { + Lox.error(stmt.keyword(), "Can't return a value from an initializer."); + } resolve(stmt.value()); } return null; @@ -155,6 +187,12 @@ public class Resolver implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Void visitGetExpr(Expr.Get expr) { + resolve(expr.object()); + return null; + } + @Override public Void visitCallExpr(Expr.Call expr) { resolve(expr.callee()); @@ -180,6 +218,24 @@ public class Resolver implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Void visitSetExpr(Expr.Set expr) { + resolve(expr.value()); + resolve(expr.object()); + return null; + } + + @Override + public Void visitThisExpr(Expr.This expr) { + if (currentClass == ClassType.NONE) { + Lox.error(expr.keyword(), "Can't use 'this' outside of a class."); + return null; + } + + resolveLocal(expr, expr.keyword()); + return null; + } + @Override public Void visitUnaryExpr(Expr.Unary expr) { resolve(expr.right()); 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 e6fcba2..685bb9e 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 @@ -3,16 +3,20 @@ rules = { @Rule(head = "Assign", body = {"Token name", "Expr value"}), @Rule(head = "Binary", body = {"Expr left", "Token operator", "Expr right"}), + @Rule(head = "Get", body = {"Expr object", "Token name"}), @Rule(head = "Call", body = {"Expr callee", "Token paren", "List arguments"}), @Rule(head = "Grouping", body = {"Expr expression"}), @Rule(head = "Literal", body = {"Object value"}), @Rule(head = "Logical", body = {"Expr left", "Token operator", "Expr right"}), + @Rule(head = "Set", body = {"Expr object", "Token name", "Expr value"}), + @Rule(head = "This", body = {"Token keyword"}), @Rule(head = "Unary", body = {"Token operator", "Expr right"}), @Rule(head = "Variable", body = {"Token name"}) }), @Root(name = "Stmt", rules = { @Rule(head = "Block", body = {"List statements"}), + @Rule(head = "Class", body = {"Token name", "List methods"}), @Rule(head = "Expression", body = {"Expr expression"}), @Rule(head = "Function", body = {"Token name", "List params", "List body"}), @Rule(head = "If", body = {"Expr condition", "Stmt thenBranch", "Stmt elseBranch"}),