[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);
}
@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);

View File

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

View File

@@ -78,6 +78,21 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
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
public Void visitExpressionStmt(Stmt.Expression stmt) {
evaluate(stmt.expression());
@@ -86,7 +101,7 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
@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<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
public Object visitCallExpr(Expr.Call expr) {
var callee = evaluate(expr.callee());
@@ -219,6 +243,23 @@ public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
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<Object>, Stmt.Visitor<Void> {
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());

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 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 "<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:
* <p>
* 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<Token> 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<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) {
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());
}

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 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<Void>, Stmt.Visitor<Void> {
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<Void>, Stmt.Visitor<Void> {
}
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<Void>, Stmt.Visitor<Void> {
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<Void>, Stmt.Visitor<Void> {
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());

View File

@@ -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<Expr> 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<Stmt> statements"}),
@Rule(head = "Class", body = {"Token name", "List<Stmt.Function> methods"}),
@Rule(head = "Expression", body = {"Expr expression"}),
@Rule(head = "Function", body = {"Token name", "List<Token> params", "List<Stmt> body"}),
@Rule(head = "If", body = {"Expr condition", "Stmt thenBranch", "Stmt elseBranch"}),