DzLox

:)
git clone https://git.sr.ht/~ashymad/DzLox
Log | Files | Refs | Submodules | LICENSE

commit 4902b342c1d5a728ff5219f3d9bb0074c9f1135a
parent fccd5ccafaeed8f7692998ca6ddf74e4598c93c1
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date:   Sat, 15 Oct 2022 02:05:30 +0200

Add classes

Diffstat:
Abacon.lox | 7+++++++
Msource/app.d | 14+++++++++++---
Msource/ast.d | 4++++
Msource/astprinter.d | 12++++++++++++
Asource/cls.d | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msource/fun.d | 28+++++++++++++++++++++++++---
Asource/instance.d | 39+++++++++++++++++++++++++++++++++++++++
Msource/interpreter.d | 34++++++++++++++++++++++++++++++++--
Msource/parser.d | 62++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msource/resolver.d | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msource/scanner.d | 2+-
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;