[jlox] Classes

This commit is contained in:
ctsk
2022-09-17 22:17:52 +02:00
parent a744ca18f9
commit f1eeff583c
9 changed files with 262 additions and 9 deletions

View File

@@ -33,6 +33,12 @@ public class AstPrinter {
return reverse ? wrap(left, right, op) : wrap(op, left, right); 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 @Override
public String visitCallExpr(Expr.Call expr) { public String visitCallExpr(Expr.Call expr) {
var fun = expr.callee().accept(this); var fun = expr.callee().accept(this);
@@ -64,6 +70,20 @@ public class AstPrinter {
return reverse ? wrap(left, right, op) : wrap(op, left, right); 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 @Override
public String visitUnaryExpr(Expr.Unary expr) { public String visitUnaryExpr(Expr.Unary expr) {
var op = expr.operator().lexeme(); var op = expr.operator().lexeme();
@@ -102,6 +122,12 @@ public class AstPrinter {
expr.right().accept(this)); expr.right().accept(this));
} }
@Override
public String visitGetExpr(Expr.Get expr) {
var left = expr.object().accept(this);
return left + "." + expr.name().lexeme();
}
@Override @Override
public String visitCallExpr(Expr.Call expr) { public String visitCallExpr(Expr.Call expr) {
var fun = expr.callee().accept(this); var fun = expr.callee().accept(this);
@@ -129,6 +155,18 @@ public class AstPrinter {
expr.right().accept(this)); 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 @Override
public String visitUnaryExpr(Expr.Unary expr) { public String visitUnaryExpr(Expr.Unary expr) {
return expr.operator().lexeme() + expr.right().accept(this); return expr.operator().lexeme() + expr.right().accept(this);

View File

@@ -3,5 +3,5 @@ package xyz.ctsk.lox;
public enum FunctionType { public enum FunctionType {
NONE, NONE,
FUNCTION, FUNCTION,
METHOD INITIALIZER, METHOD
} }

View File

@@ -78,6 +78,21 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
return null; return null;
} }
@Override
public Void visitClassStmt(Stmt.Class stmt) {
environment.define(stmt.name().lexeme(), null);
Map<String, LoxFunction> 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 @Override
public Void visitExpressionStmt(Stmt.Expression stmt) { public Void visitExpressionStmt(Stmt.Expression stmt) {
evaluate(stmt.expression()); evaluate(stmt.expression());
@@ -86,7 +101,7 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
@Override @Override
public Void visitFunctionStmt(Stmt.Function stmt) { 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; return null;
} }
@@ -177,6 +192,15 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
}; };
} }
@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 @Override
public Object visitCallExpr(Expr.Call expr) { public Object visitCallExpr(Expr.Call expr) {
var callee = evaluate(expr.callee()); var callee = evaluate(expr.callee());
@@ -219,6 +243,23 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
return evaluate(expr.right()); 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 @Override
public Object visitUnaryExpr(Expr.Unary expr) { public Object visitUnaryExpr(Expr.Unary expr) {
var right = evaluate(expr.right()); var right = evaluate(expr.right());
@@ -235,7 +276,7 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
return lookupVariable(expr.name(), expr); 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); var distance = locals.get(expr);
if (distance != null) { if (distance != null) {
return environment.getAt(distance, name.lexeme()); return environment.getAt(distance, name.lexeme());

View File

@@ -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<String, LoxFunction> methods;
LoxClass(String name, Map<String, LoxFunction> 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<Object> 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;
}
}

View File

@@ -6,9 +6,12 @@ public class LoxFunction implements LoxCallable {
private final Stmt.Function declaration; private final Stmt.Function declaration;
private final Environment closure; 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.declaration = declaration;
this.closure = closure; this.closure = closure;
this.isInitializer = isInitializer;
} }
@Override @Override
@@ -27,11 +30,20 @@ public class LoxFunction implements LoxCallable {
try { try {
interpreter.executeBlock(declaration.body(), environment); interpreter.executeBlock(declaration.body(), environment);
} catch (Return ret) { } catch (Return ret) {
if (isInitializer) return closure.getAt(0, "this");
return ret.value; return ret.value;
} }
if (isInitializer) return closure.getAt(0, "this");
return null; return null;
} }
LoxFunction bind(LoxInstance instance) {
Environment env = new Environment(closure);
env.define("this", instance);
return new LoxFunction(declaration, env, isInitializer);
}
@Override @Override
public String toString() { public String toString() {
return "<fn " + declaration.name().lexeme() + " >"; return "<fn " + declaration.name().lexeme() + " >";

View File

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

View File

@@ -10,9 +10,11 @@ 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 → declaration* EOF ; * program → declaration* EOF ;
* declaration → funDecl * declaration → classDecl
* | funDecl
* | varDecl * | varDecl
* | statement ; * | statement ;
* classDecl → "class" IDENTIFIER "{" function* "}" ;
* funDecl → "fun" function ; * funDecl → "fun" function ;
* function → IDENTIFIER "(" parameters? ")" block ; * function → IDENTIFIER "(" parameters? ")" block ;
* parameters → IDENTIFIER ( "," IDENTIFIER )* ; * parameters → IDENTIFIER ( "," IDENTIFIER )* ;
@@ -35,7 +37,7 @@ import static xyz.ctsk.lox.TokenType.*;
* exprStmt → expression ";" ; * exprStmt → expression ";" ;
* expression → equality ; * expression → equality ;
* expression → assignment ; * expression → assignment ;
* assignment → IDENTIFIER "=" assignment * assignment → ( call "." )? IDENTIFIER "=" assignment
* | logic_or ; * | logic_or ;
* logic_or → logic_and ( "or" logic_and )* ; * logic_or → logic_and ( "or" logic_and )* ;
* logic_and → equality ( "and" equality )* ; * logic_and → equality ( "and" equality )* ;
@@ -45,11 +47,12 @@ import static xyz.ctsk.lox.TokenType.*;
* factor → unary ( ( "/" | "*" ) unary )* ; * factor → unary ( ( "/" | "*" ) unary )* ;
* unary → ( "!" | "-" ) unary * unary → ( "!" | "-" ) unary
* | call ; * | call ;
* call → primary ( "(" arguments? ")" )* ; * call → primary ( "(" arguments? ")" | "." IDENTIFIER )* ;
* arguments → expression ( "," expression )* ; * arguments → expression ( "," expression )* ;
* primary → NUMBER | STRING | "true" | "false" | "nil" * primary → NUMBER | STRING | "true" | "false" | "nil"
* | "(" expression ")" * | "(" expression ")"
* | IDENTIFIER; * | IDENTIFIER
* | THIS ;
*/ */
public class Parser { public class Parser {
private final List<Token> tokens; private final List<Token> tokens;
@@ -72,6 +75,7 @@ public class Parser {
private Stmt declaration() { private Stmt declaration() {
try { try {
if (match(CLASS)) return classDeclaration();
if (match(FUN)) return function(FunctionType.FUNCTION); if (match(FUN)) return function(FunctionType.FUNCTION);
if (match(VAR)) return varDeclaration(); if (match(VAR)) return varDeclaration();
return match(VAR) ? varDeclaration() : statement(); 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<Stmt.Function> 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) { private Stmt.Function function(FunctionType kind) {
Token name = consume(IDENTIFIER, "Expect %s name.".formatted(kind.toString().toLowerCase())); Token name = consume(IDENTIFIER, "Expect %s name.".formatted(kind.toString().toLowerCase()));
@@ -232,6 +251,8 @@ public class Parser {
if (expr instanceof Expr.Variable varExpr) { if (expr instanceof Expr.Variable varExpr) {
return new Expr.Assign(varExpr.name(), value); return new Expr.Assign(varExpr.name(), value);
} else if (expr instanceof Expr.Get getExpr) {
return new Expr.Set(getExpr.object(), getExpr.name(), value);
} else { } else {
//We report an error but don't throw it because we do not need our parser to panic and sync. //We report an error but don't throw it because we do not need our parser to panic and sync.
//noinspection ThrowableNotThrown //noinspection ThrowableNotThrown
@@ -330,7 +351,9 @@ public class Parser {
while(true) { while(true) {
if (match(LEFT_PAREN)) { if (match(LEFT_PAREN)) {
expr = finishCall(expr); expr = finishCall(expr);
} else { } else if (match(DOT)) {
Token name = consume(IDENTIFIER, "Expect property name after '.'.");
expr = new Expr.Get(expr, name);
break; break;
} }
} }
@@ -365,6 +388,8 @@ public class Parser {
return new Expr.Literal(previous().literal()); return new Expr.Literal(previous().literal());
} }
if (match(THIS)) return new Expr.This(previous());
if (match(IDENTIFIER)) { if (match(IDENTIFIER)) {
return new Expr.Variable(previous()); return new Expr.Variable(previous());
} }

View File

@@ -11,6 +11,10 @@ public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
private final Stack<Map<String, Boolean>> scopes = new Stack<>(); private final Stack<Map<String, Boolean>> scopes = new Stack<>();
private FunctionType currentFuntion = FunctionType.NONE; private FunctionType currentFuntion = FunctionType.NONE;
private enum ClassType { NONE, CLASS }
private ClassType currentClass = ClassType.NONE;
Resolver(Interpreter interpreter) { Resolver(Interpreter interpreter) {
this.interpreter = interpreter; this.interpreter = interpreter;
@@ -82,6 +86,31 @@ public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
return null; 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 @Override
public Void visitExpressionStmt(Stmt.Expression stmt) { public Void visitExpressionStmt(Stmt.Expression stmt) {
@@ -118,6 +147,9 @@ public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
} }
if (stmt.value() != null) { if (stmt.value() != null) {
if (currentFuntion == FunctionType.INITIALIZER) {
Lox.error(stmt.keyword(), "Can't return a value from an initializer.");
}
resolve(stmt.value()); resolve(stmt.value());
} }
return null; return null;
@@ -155,6 +187,12 @@ public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
return null; return null;
} }
@Override
public Void visitGetExpr(Expr.Get expr) {
resolve(expr.object());
return null;
}
@Override @Override
public Void visitCallExpr(Expr.Call expr) { public Void visitCallExpr(Expr.Call expr) {
resolve(expr.callee()); resolve(expr.callee());
@@ -180,6 +218,24 @@ public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
return null; 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 @Override
public Void visitUnaryExpr(Expr.Unary expr) { public Void visitUnaryExpr(Expr.Unary expr) {
resolve(expr.right()); resolve(expr.right());

View File

@@ -3,16 +3,20 @@
rules = { rules = {
@Rule(head = "Assign", body = {"Token name", "Expr value"}), @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 = "Get", body = {"Expr object", "Token name"}),
@Rule(head = "Call", body = {"Expr callee", "Token paren", "List<Expr> arguments"}), @Rule(head = "Call", body = {"Expr callee", "Token paren", "List<Expr> arguments"}),
@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 = "Logical", body = {"Expr left", "Token operator", "Expr right"}), @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 = "Unary", body = {"Token operator", "Expr right"}),
@Rule(head = "Variable", body = {"Token name"}) @Rule(head = "Variable", body = {"Token name"})
}), }),
@Root(name = "Stmt", @Root(name = "Stmt",
rules = { rules = {
@Rule(head = "Block", body = {"List<Stmt> statements"}), @Rule(head = "Block", body = {"List<Stmt> statements"}),
@Rule(head = "Class", body = {"Token name", "List<Stmt.Function> methods"}),
@Rule(head = "Expression", body = {"Expr expression"}), @Rule(head = "Expression", body = {"Expr expression"}),
@Rule(head = "Function", body = {"Token name", "List<Token> params", "List<Stmt> body"}), @Rule(head = "Function", body = {"Token name", "List<Token> params", "List<Stmt> body"}),
@Rule(head = "If", body = {"Expr condition", "Stmt thenBranch", "Stmt elseBranch"}), @Rule(head = "If", body = {"Expr condition", "Stmt thenBranch", "Stmt elseBranch"}),