commit 4902b342c1d5a728ff5219f3d9bb0074c9f1135a
parent fccd5ccafaeed8f7692998ca6ddf74e4598c93c1
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date: Sat, 15 Oct 2022 02:05:30 +0200
Add classes
Diffstat:
11 files changed, 298 insertions(+), 32 deletions(-)
diff --git a/bacon.lox b/bacon.lox
@@ -0,0 +1,7 @@
+class Bacon {
+ eat() {
+ print "Crunch crunch crunch!";
+ }
+}
+
+Bacon().eat(); // Prints "Crunch crunch crunch!".
diff --git a/source/app.d b/source/app.d
@@ -108,15 +108,23 @@ class Lox {
}
}
+ static void warning(const TokenI token, string message) {
+ if (token.type == TokenType.EOF) {
+ report(token.line, " at end", message, true);
+ } else {
+ report(token.line, " at '" ~ token.lexeme ~ "'", message, true);
+ }
+ }
+
static void error(RuntimeError err) {
error(err.token, err.msg);
hadRuntimeError = true;
}
- static void report(int line, string where, string msg) {
- writefln("[line %s] Error%s: %s", line, where, msg);
- hadError = true;
+ static void report(int line, string where, string msg, bool warning = false) {
+ writefln("[line %s] %s%s: %s", line, warning ? "Warning" : "Error", where, msg);
+ if (!warning) this.hadError = true;
}
}
diff --git a/source/ast.d b/source/ast.d
@@ -13,6 +13,10 @@ static immutable string[][] expressions = [
["Logical", "Expr left", "TokenI operator", "Expr right"],
["Call", "Expr callee", "TokenI paren", "Expr[] arguments"],
["Function", "TokenI[] params", "Stmt[] body"],
+ ["Class", "Var[] methods"],
+ ["Get", "Expr object", "TokenI name"],
+ ["Set", "Expr object", "TokenI name", "Expr value"],
+ ["This", "TokenI keyword"],
];
static immutable string[][] statements = [
diff --git a/source/astprinter.d b/source/astprinter.d
@@ -38,12 +38,24 @@ class AstPrinter : ExprVisitor {
void visit(Function fun) {
printed = "(fun (" ~ fun.params.map!(p => p.lexeme).join(" ") ~ ") {...})";
}
+ void visit(Class cls) {
+ printed = "(class " ~ cls.methods.map!(p => p.name.lexeme ~ "()").join(" ") ~ ")";
+ }
void visit(Unary unary) {
printed = parenthesize(unary.operator.lexeme, unary.right);
}
void visit(Call call) {
printed = parenthesize("call", call.callee ~ call.arguments);
}
+ void visit(Get get) {
+ printed = parenthesize("get", get.object, new Variable(get.name));
+ }
+ void visit(Set get) {
+ printed = parenthesize("set", get.object, new Variable(get.name), get.value);
+ }
+ void visit(This th) {
+ printed = th.keyword.lexeme;
+ }
private string parenthesize(string name, Expr[] exprs ...) {
return "(" ~ name ~ " " ~ exprs.map!(e => print(e)).join(" ") ~ ")";
}
diff --git a/source/cls.d b/source/cls.d
@@ -0,0 +1,49 @@
+import callable;
+import std.variant;
+import interpreter;
+import instance;
+import ast;
+import fun;
+import std.array;
+
+class Cls : Callable {
+ private Var[] props;
+ private ulong _arity;
+
+ this(Var[] props) {
+ this.props = props;
+ _arity = 0;
+ foreach(prop; props) {
+ if (prop.name.lexeme == "init") {
+ if (auto fun = cast(Function) prop.initializer) {
+ _arity = fun.params.length;
+ }
+ }
+ }
+ }
+
+ Variant call(Interpreter interpreter, Variant[] arguments) {
+ Variant[string] fields = null;
+ foreach(prop; props) {
+ fields[prop.name.lexeme] = interpreter.evaluate(prop.initializer);
+ }
+ Instance instance = new Instance(this, fields);
+ if (auto fun = "init" in fields) {
+ if (fun.convertsTo!(Fun)) {
+ auto ifun = fun.get!(Fun);
+ ifun.setInitializer();
+ ifun.bind(instance).call(interpreter, arguments);
+ *fun = Variant(ifun);
+ }
+ }
+ return Variant(instance);
+ }
+
+ ulong arity() {
+ return _arity;
+ }
+
+ void toString(scope void delegate(const(char)[]) sink) const {
+ sink("<class>");
+ }
+}
diff --git a/source/fun.d b/source/fun.d
@@ -3,14 +3,23 @@ import ast;
import interpreter;
import environment;
import std.variant;
+import instance;
+import tokentype;
+import token;
class Fun : Callable {
private Function fun;
private Environment closure;
+ private bool isInitializer;
- this(Function fun, Environment closure) {
+ this(Function fun, Environment closure, bool isInitializer = false) {
this.closure = closure;
this.fun = fun;
+ this.isInitializer = isInitializer;
+ }
+
+ void setInitializer() {
+ isInitializer = true;
}
Variant call(Interpreter interpreter, Variant[] arguments) {
@@ -22,9 +31,22 @@ class Fun : Callable {
try {
interpreter.executeBlock(fun.body, environment);
} catch (Interpreter.ReturnCalled ret) {
- return ret.value;
+ return retv(ret.value);
}
- return Variant(null);
+
+ return retv(Variant(null));
+ }
+
+ Variant retv(Variant value) {
+ if (isInitializer) return Variant(new Instance(closure.getAt(
+ TokenI(TokenType.THIS, "this", null, -1), 0).get!(Instance)));
+ return value;
+ }
+
+ Fun bind(Instance instance) {
+ Environment environment = new Environment(closure);
+ environment.define("this", Variant(instance));
+ return new Fun(fun, environment, isInitializer);
}
ulong arity() {
diff --git a/source/instance.d b/source/instance.d
@@ -0,0 +1,39 @@
+import cls;
+import token;
+import error;
+import std.variant;
+import fun;
+
+class Instance {
+ private Cls cls;
+ private Variant[string] fields;
+
+ this(Cls cls, Variant[string] fields) {
+ this.cls = cls;
+ this.fields = fields;
+ }
+
+ this(Instance inst) {
+ this.cls = inst.cls;
+ this.fields = inst.fields.dup;
+ }
+
+ Variant get(TokenI name) {
+ if (auto field = name.lexeme in fields) {
+ if (field.convertsTo!(Fun)) {
+ return Variant(field.get!(Fun).bind(this));
+ }
+ return *field;
+ }
+ throw new RuntimeError(name,
+ "Undefined property '" ~ name.lexeme ~ "'.");
+ }
+
+ void set(TokenI name, Variant value) {
+ fields[name.lexeme] = value;
+ }
+
+ void toString(scope void delegate(const(char)[]) sink) const {
+ sink("<class instance>");
+ }
+}
diff --git a/source/interpreter.d b/source/interpreter.d
@@ -1,4 +1,4 @@
-import std.variant;
+import std.variant : Variant;
import std.format;
import std.algorithm;
import std.stdio;
@@ -13,6 +13,8 @@ import astprinter;
import environment;
import callable;
import fun;
+import cls;
+import instance;
class Interpreter : StmtVisitor, ExprVisitor {
private Variant value;
@@ -141,6 +143,34 @@ class Interpreter : StmtVisitor, ExprVisitor {
value = new Fun(expr, environment);
}
+ void visit(Class expr) {
+ value = new Cls(expr.methods);
+ }
+
+ void visit(Get expr) {
+ Variant object = evaluate(expr.object);
+ if (!object.convertsTo!(Instance)) {
+ throw new RuntimeError(expr.name,
+ "Attempt to acces property of non-instace object");
+ }
+ value = object.get!(Instance).get(expr.name);
+ }
+
+ void visit(Set expr) {
+ Variant object = evaluate(expr.object);
+ if (!object.convertsTo!(Instance)) {
+ throw new RuntimeError(expr.name,
+ "Attempt to set property of non-instace object");
+ }
+ Variant val = evaluate(expr.value);
+ object.get!(Instance).set(expr.name, val);
+ value = val;
+ }
+
+ void visit(This expr) {
+ value = lookUpVariable(expr.keyword, expr);
+ }
+
void visit(Call expr) {
Variant callee = evaluate(expr.callee);
Variant[] arguments = [];
@@ -301,7 +331,7 @@ class Interpreter : StmtVisitor, ExprVisitor {
}
}
- private Variant evaluate(Expr expr) {
+ Variant evaluate(Expr expr) {
expr.accept(this);
return value;
}
diff --git a/source/parser.d b/source/parser.d
@@ -1,13 +1,14 @@
import token;
import tokentype;
import std.container;
-import std.variant;
+import std.variant : Variant;
import ast;
import app;
/*
program → statement* EOF ;
-declaration → funDecl
+declaration → classDecl
+ | funDecl
| varDecl
| statement ;
varDecl → "var" IDENTIFIER ( "=" expression )? ";" ;
@@ -22,6 +23,8 @@ statement → exprStmt
returnStmt → "return" expression? ";" ;
funDecl → "fun" IDENTIFIER function ;
function → "(" parameters? ")" block ;
+classDecl → "class" IDENTIFIER class ;
+class → "{" ( IDENTIFIER ( function | "=" expression ) )* "}" ;
parameters → IDENTIFIER ( "," IDENTIFIER )* ;
forStmt → "for" "(" ( varDecl | exprStmt | ";" )
expression? ";"
@@ -34,7 +37,7 @@ exprStmt → expression ";" ;
printStmt → "print" expression ";" ;
expression → separator ;
separator → assignment ( "," assignment )* ;
-assignment → IDENTIFIER "=" assignment
+assignment → ( call "." )? IDENTIFIER "=" assignment
| ternary ;
ternary → logic_or ( "?" expression ":" ternary )? ;
logic_or → logic_and ( "or" logic_and )* ;
@@ -45,9 +48,10 @@ term → factor ( ( "-" | "+" ) factor )* ;
factor → unary ( ( "/" | "*" ) unary )* ;
unary → ( "!" | "-" | "ast" ) unary
| call ;
-call → funExpr ( "(" arguments? ")" )* ;
-funExpr → "fun" function ;
-primary → NUMBER | STRING | "true" | "false" | "nil"
+call → funExpr ( "(" arguments? ")" | "." IDENTIFIER )* ;
+funExpr → "fun" function | classExpr ;
+classExpr → "class" class | primary ;
+primary → NUMBER | STRING | "true" | "false" | "nil" | "this"
| "(" expression ")" | IDENTIFIER ;
arguments → expression ( "," expression )* ;
*/
@@ -81,6 +85,9 @@ class Parser {
if (check(TokenType.FUN) && peekNext().type == TokenType.IDENTIFIER) {
advance();
return statement!(Var)(advance(), fun());
+ } else if (check(TokenType.CLASS) && peekNext().type == TokenType.IDENTIFIER) {
+ advance();
+ return statement!(Var)(advance(), _class());
} else {
return matchStatement();
}
@@ -107,6 +114,26 @@ class Parser {
Stmt[] bod = block();
return new Function(parameters, bod);
}
+
+ private Expr _class() {
+ consume(TokenType.LEFT_BRACE, "Expect '{' before class body.");
+
+ Var[] fields;
+ while (!isAtEnd() && match(TokenType.IDENTIFIER)) {
+ TokenI name = previous();
+ if (check(TokenType.LEFT_PAREN)) {
+ fields ~= statement!(Var)(name, fun());
+ } else if (match(TokenType.EQUAL)) {
+ fields ~= statement!(Var)(name, expression());
+ } else {
+ Lox.error(name, "Expect field declaration");
+ }
+ }
+
+ consume(TokenType.RIGHT_BRACE, "Expect '}' after class.");
+
+ return new Class(fields);
+ }
private Stmt varDeclaration() {
TokenI name = consume(TokenType.IDENTIFIER, "Expect variable name.");
@@ -223,7 +250,7 @@ class Parser {
return statements;
}
- private Stmt statement(T, A...)(A a) {
+ private T statement(T, A...)(A a) {
if (!isAtEnd()
&& !match(TokenType.SEMICOLON)
&& !check(TokenType.RIGHT_BRACE)
@@ -251,6 +278,10 @@ class Parser {
return new Assign(variable.name, value);
}
+ if (auto variable = cast(Get) expr) {
+ return new Set(variable.object, variable.name, value);
+ }
+
error(equals, "Invalid assignment target.");
}
return expr;
@@ -316,6 +347,10 @@ class Parser {
while (true) {
if (match(TokenType.LEFT_PAREN)) {
expr = finishCall(expr);
+ } else if (match(TokenType.DOT)) {
+ TokenI name = consume(TokenType.IDENTIFIER,
+ "Expect property name after '.'.");
+ expr = new Get(expr, name);
} else {
break;
}
@@ -328,6 +363,13 @@ class Parser {
if (match(TokenType.FUN)) {
return fun();
}
+ return classExpr();
+ }
+
+ private Expr classExpr() {
+ if (match(TokenType.CLASS)) {
+ return _class();
+ }
return primary();
}
@@ -358,10 +400,10 @@ class Parser {
return new Literal(Variant(null));
if (match(IDENTIFIER))
return new Variable(previous());
-
- if (match(NUMBER, STRING)) {
+ if (match(NUMBER, STRING))
return new Literal(previous().literal);
- }
+ if (match(THIS))
+ return new This(previous());
if (match(LEFT_PAREN)) {
Expr expr = expression();
diff --git a/source/resolver.d b/source/resolver.d
@@ -6,6 +6,7 @@ import token;
import app;
import std.format;
import std.typecons : Tuple;
+import tokentype;
class Resolver : StmtVisitor, ExprVisitor {
@@ -15,6 +16,7 @@ class Resolver : StmtVisitor, ExprVisitor {
private SList!(VarRef[string]) scopes;
private FunctionType currentFunction = FunctionType.NONE;
private LoopType currentLoop = LoopType.NONE;
+ private ClassType currentClass = ClassType.NONE;
private enum VarState {
DECLARED,
@@ -24,7 +26,9 @@ class Resolver : StmtVisitor, ExprVisitor {
private enum FunctionType {
NONE,
- FUN
+ FUN,
+ METHOD,
+ INITIALIZER
}
private enum LoopType {
@@ -32,6 +36,11 @@ class Resolver : StmtVisitor, ExprVisitor {
WHILE
}
+ private enum ClassType {
+ NONE,
+ CLASS
+ }
+
this(Interpreter interpreter) {
this.interpreter = interpreter;
this.scopes = SList!(VarRef[string])();
@@ -50,12 +59,17 @@ class Resolver : StmtVisitor, ExprVisitor {
private void endScope() {
foreach(sco; scopes.front().byPair) {
if(sco.value.state != VarState.REFERENCED) {
- Lox.error(sco.value.line, format("Variable '%s' declared but never referenced", sco.key));
+ Lox.warning(mkToken(sco.key, sco.value),
+ format("Variable declared but never referenced"));
}
}
scopes.removeFront();
}
+ private TokenI mkToken(string name, VarRef vref) {
+ return TokenI(TokenType.IDENTIFIER, name, null, vref.line);
+ }
+
private void declare(TokenI name) {
if(scopes.empty()) return;
@@ -73,8 +87,12 @@ class Resolver : StmtVisitor, ExprVisitor {
private void resolveLocal(Expr expr, TokenI name) {
foreach(i, sco; scopes[].enumerate()) {
- if(name.lexeme in sco) {
+ if(auto local = name.lexeme in sco) {
+ if ((*local).state == VarState.DECLARED) {
+ Lox.error(name, "Attempt to reference undefined local variable");
+ }
interpreter.resolve(expr, i);
+ (*local).state = VarState.REFERENCED;
}
}
}
@@ -126,6 +144,9 @@ class Resolver : StmtVisitor, ExprVisitor {
Lox.error(_return.keyword, "Can't return from top-level code.");
}
if (_return.value !is null) resolve(_return.value);
+ else if (currentFunction == FunctionType.INITIALIZER) {
+ Lox.error(_return.keyword, "Can't return value from initializer method");
+ }
}
void visit(Ternary _ternary) {
@@ -150,16 +171,7 @@ class Resolver : StmtVisitor, ExprVisitor {
}
void visit(Variable expr) {
- if (!scopes.empty() &&
- scopes.front().get(expr.name.lexeme, VarRef(VarState.DEFINED, 0)).state == VarState.DECLARED) {
- Lox.error(expr.name, "Attempt to resolve undefined local variable.");
- }
-
resolveLocal(expr, expr.name);
-
- if(!scopes.empty() && expr.name.lexeme in scopes.front()) {
- scopes.front()[expr.name.lexeme].state = VarState.REFERENCED;
- }
}
void visit(Assign expr) {
resolve(expr.value);
@@ -183,8 +195,12 @@ class Resolver : StmtVisitor, ExprVisitor {
}
void visit(Function expr) {
+ resolveFunction(expr, FunctionType.FUN);
+ }
+
+ private void resolveFunction(Function expr, FunctionType type) {
FunctionType enclosingFunction = currentFunction;
- currentFunction = FunctionType.FUN;
+ currentFunction = type;
beginScope();
foreach (param; expr.params) {
declare(param);
@@ -194,4 +210,41 @@ class Resolver : StmtVisitor, ExprVisitor {
endScope();
currentFunction = enclosingFunction;
}
+
+ void visit(Class cl) {
+ ClassType enclosingClass = currentClass;
+ currentClass = ClassType.CLASS;
+ beginScope();
+ foreach(method; cl.methods) {
+ if (auto fun = cast(Function) method.initializer) {
+ scopes.front()["this"] = VarRef(VarState.REFERENCED, 0);
+ FunctionType declaration = method.name.lexeme == "init" ?
+ FunctionType.INITIALIZER : FunctionType.METHOD;
+ resolveFunction(fun, declaration);
+ } else {
+ resolve(method.initializer);
+ }
+ }
+ endScope();
+ currentClass = enclosingClass;
+ }
+
+ void visit(Get get) {
+ resolve(get.object);
+ }
+
+ void visit(Set set) {
+ resolve(set.value);
+ resolve(set.object);
+ }
+
+ void visit(This th) {
+ if (currentClass == ClassType.NONE
+ || currentFunction == FunctionType.FUN
+ || currentFunction == FunctionType.NONE) {
+ Lox.error(th.keyword, format("'This' used in invalid function type: %s", currentFunction));
+ return;
+ }
+ resolveLocal(th, th.keyword);
+ }
}
diff --git a/source/scanner.d b/source/scanner.d
@@ -10,7 +10,7 @@ class Scanner {
private string source;
private int start = 0;
private int current = 0;
- private int line = 0;
+ private int line = 1;
private Array!TokenI tokens;