From d8ac625429420fd7bc9df105c246a159756a7464 Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 22:22:04 +0000 Subject: [PATCH] Classes 12 --- dub.sdl | 1 - src/jlox/environment.d | 20 ++--- src/jlox/expr.d | 52 +++++------- src/jlox/interpreter.d | 178 +++++++++++++++++++++++------------------ src/jlox/loxclass.d | 34 ++++++++ src/jlox/loxfunction.d | 21 ++++- src/jlox/loxinstance.d | 27 +++++++ src/jlox/main.d | 19 +++-- src/jlox/parser.d | 40 ++++++--- src/jlox/resolver.d | 42 +++++++++- src/jlox/scanner.d | 6 +- src/jlox/stmt.d | 6 ++ src/jlox/token.d | 76 +++++++++++------- test/all.d | 30 ++++--- test/class.lox | 21 +++++ test/ops.lox | 17 ++++ test/shortcircuit.lox | 12 +++ test/test.lox | 1 + 18 files changed, 417 insertions(+), 186 deletions(-) create mode 100644 src/jlox/loxclass.d create mode 100644 src/jlox/loxinstance.d create mode 100644 test/class.lox create mode 100644 test/ops.lox create mode 100644 test/shortcircuit.lox diff --git a/dub.sdl b/dub.sdl index ce86bdd..8920a38 100644 --- a/dub.sdl +++ b/dub.sdl @@ -4,7 +4,6 @@ authors "tanya" copyright "Copyright © 2025, tanya" license "MPL-2.0" dependency "commandr" version="~>1.1.0" -dependency "taggedalgebraic" version="~>0.11.23" targetType "executable" buildRequirements "requireBoundsCheck" "requireContracts" diff --git a/src/jlox/environment.d b/src/jlox/environment.d index baee5fe..b1d32bd 100644 --- a/src/jlox/environment.d +++ b/src/jlox/environment.d @@ -6,15 +6,15 @@ import common.util; class Environment{ Environment enclosing; - private TValue[string] values; + private LoxValue[string] values; mixin defaultCtor; - void define(string name, TValue value){ + void define(string name, LoxValue value){ values[name] = value; } - TValue get(Token name){ - if(TValue* value = name.lexeme in values) + LoxValue get(Token name){ + if(LoxValue* value = name.lexeme in values) return *value; if(enclosing) return enclosing.get(name); @@ -26,17 +26,19 @@ class Environment{ environment = environment.enclosing; return environment; } - TValue getAt(uint distance, string name){ + LoxValue getAt(uint distance, string name){ return ancestor(distance).values[name]; } - void assign(Token name, TValue value){ - if(name.lexeme in values) - return values[name.lexeme] = value; + void assign(Token name, LoxValue value){ + if(name.lexeme in values){ + values[name.lexeme] = value; + return; + } if(enclosing) return enclosing.assign(name, value); throw new RuntimeError(name, "Undefined variable '" ~ name.lexeme ~ "'."); } - void assignAt(uint distance, Token name, TValue value){ + void assignAt(uint distance, Token name, LoxValue value){ ancestor(distance).values[name.lexeme] = value; } } diff --git a/src/jlox/expr.d b/src/jlox/expr.d index ed313f6..2c68f8b 100644 --- a/src/jlox/expr.d +++ b/src/jlox/expr.d @@ -4,8 +4,6 @@ import std.conv; import std.stdio; import std.meta : AliasSeq; -import taggedalgebraic; - import jlox.token; import jlox.tokentype; import common.util; @@ -15,13 +13,16 @@ abstract class Expr{ R visit(Assign expr); R visit(Binary expr); R visit(Call expr); + R visit(Get expr); R visit(Grouping expr); R visit(Literal expr); R visit(Logical expr); + R visit(Set expr); + R visit(This expr); R visit(Unary expr); R visit(Variable expr); } - private alias rTypes = AliasSeq!(string, TValue, void); + private alias rTypes = AliasSeq!(string, LoxValue, void); static foreach(T; rTypes) abstract T accept(Visitor!T visitor); private template defCtorAndAccept(){ @@ -47,12 +48,17 @@ abstract class Expr{ Expr[] arguments; mixin defCtorAndAccept; } + static class Get : typeof(this){ + Expr object; + Token name; + mixin defCtorAndAccept; + } static class Grouping : typeof(this){ Expr expression; mixin defCtorAndAccept; } static class Literal : typeof(this){ - TValue value; + LoxValue value; mixin defCtorAndAccept; } static class Logical : typeof(this){ @@ -61,6 +67,16 @@ abstract class Expr{ Expr right; mixin defCtorAndAccept; } + static class Set : typeof(this){ + Expr object; + Token name; + Expr value; + mixin defCtorAndAccept; + } + static class This : typeof(this){ + Token keyword; + mixin defCtorAndAccept; + } static class Unary : typeof(this){ Token operator; Expr right; @@ -72,31 +88,3 @@ abstract class Expr{ } } -/* class AstPrinter : Expr.Visitor!string{ */ -/* string print(Expr expr){ */ -/* return expr.accept(this); */ -/* } */ -/* string parenthesize(Args...)(string name, Args args){ */ -/* string s = "(" ~ name; */ -/* static foreach(expr; args){ */ -/* s ~= " "; */ -/* s ~= expr.accept(this); */ -/* } */ -/* return s ~ ")"; */ -/* } */ -/* string visit(Expr.Binary expr){ */ -/* return parenthesize(expr.operator.lexeme, expr.left, expr.right); */ -/* } */ -/* string visit(Expr.Grouping expr){ */ -/* return parenthesize("group", expr.expression); */ -/* } */ -/* string visit(Expr.Literal expr){ */ -/* if(expr.value.isNil) */ -/* return "nil"; */ -/* return expr.value.to!string; */ -/* } */ -/* string visit(Expr.Unary expr){ */ -/* return parenthesize(expr.operator.lexeme, expr.right); */ -/* } */ -/* } */ - diff --git a/src/jlox/interpreter.d b/src/jlox/interpreter.d index 9af9dd3..319c62c 100644 --- a/src/jlox/interpreter.d +++ b/src/jlox/interpreter.d @@ -11,10 +11,12 @@ import jlox.expr; import jlox.stmt; import jlox.token; import jlox.tokentype; -import jlox.token : TValue; +import jlox.token : LoxValue; import jlox.main; import jlox.environment; import jlox.loxfunction; +import jlox.loxclass; +import jlox.loxinstance; class RuntimeError : Exception{ const Token token; @@ -24,14 +26,14 @@ class RuntimeError : Exception{ } } class Return : Exception{ - const TValue value; - this(TValue value){ + const LoxValue value; + this(LoxValue value){ super(null); this.value = value; } } -class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { +class Interpreter : Stmt.Visitor!void, Expr.Visitor!LoxValue { Environment globals = new Environment(); private Environment environment; this(){ @@ -39,20 +41,24 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { import std.datetime.stopwatch; auto sw = StopWatch(AutoStart.yes); - globals.define("clock", TValue.cal(new class LoxCallable{ + globals.define("clock", new class LoxCallable{ int arity() => 0; - TValue call(Interpreter interpreter, TValue[] arguments) => TValue.dbl(sw.peek.total!"usecs" / (1000.0 * 1000.0)); - })); + LoxValue call(Interpreter interpreter, LoxValue[] arguments) => new LoxNum(sw.peek.total!"usecs" / (1000.0 * 1000.0)); + override string toString() const => ""; + }); version(LoxExtraNativeFuncs){ - globals.define("sleep", TValue.cal(new class LoxCallable{ + globals.define("sleep", new class LoxCallable{ int arity() => 1; import core.thread.osthread; - TValue call(Interpreter interpreter, TValue[] arguments){ - Thread.sleep(dur!"usecs"(cast(long)(arguments[0].dblValue * 1000 * 1000))); - return TValue.nil(tvalueNil); + LoxValue call(Interpreter interpreter, LoxValue[] arguments){ + if(cast(LoxNum)arguments[0] is null) + throw new RuntimeError(null, "Expected number to sleep/1"); + Thread.sleep(dur!"usecs"(cast(long)(cast(LoxNum)arguments[0] * 1000 * 1000))); + return new LoxNil(); } - })); + override string toString() const => ""; + }); } } void interpret(Stmt[] statements){ @@ -76,24 +82,21 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { this.environment = previous; } } - private TValue evaluate(Expr expr){ + private LoxValue evaluate(Expr expr){ return expr.accept(this); } - private bool isTruthy(TValue val){ - switch(val.kind){ - case TValue.Kind.nil: - return false; - case TValue.Kind.bln: - return val.blnValue; - default: - return true; - } + private bool isTruthy(LoxValue val){ + if(cast(LoxNil)val) + return false; + if(auto b = cast(LoxBool)val) + return b; + return true; } - private bool isEqual(TValue a, TValue b){ - return a == b; + private bool isEqual(LoxValue a, LoxValue b){ + return a.equals(b); } - private void checkNumberOperand(Token operator, TValue operand){ - if(operand.kind == TValue.Kind.dbl) + private void checkNumberOperand(Token operator, LoxValue operand){ + if(cast(LoxNum)operand) return; throw new RuntimeError(operator, "Operand must be a number."); } @@ -105,12 +108,22 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { void visit(Stmt.Block stmt){ executeBlock(stmt.statements, new Environment(environment)); } + void visit(Stmt.Class stmt){ + environment.define(stmt.name.lexeme, new LoxNil()); + LoxFunction[string] methods; + foreach(Stmt.Function method; stmt.methods){ + LoxFunction func = new LoxFunction(method, environment, method.name.lexeme == "init"); + methods[method.name.lexeme] = func; + } + LoxClass cls = new LoxClass(stmt.name.lexeme, methods); + environment.assign(stmt.name, cls); + } void visit(Stmt.Expression stmt){ evaluate(stmt.expression); } void visit(Stmt.Function stmt){ - LoxFunction func = new LoxFunction(stmt, environment); - environment.define(stmt.name.lexeme, TValue.cal(func)); + LoxFunction func = new LoxFunction(stmt, environment, false); + environment.define(stmt.name.lexeme, func); } void visit(Stmt.If stmt){ if(isTruthy(evaluate(stmt.condition))) @@ -120,44 +133,44 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { } void visit(Stmt.Print stmt){ version(LoxPrintMultiple){ - writeln(stmt.expressions.map!(x => evaluate(x)).map!tvalueToString.join("\t")); + writeln(stmt.expressions.map!(x => evaluate(x)).map!(x => x.toString).join('\t')); } else { - TValue value = evaluate(stmt.expression); - writeln(tvalueToString(value)); + LoxValue value = evaluate(stmt.expression); + writeln(value.toString); } } void visit(Stmt.Return stmt){ - TValue value = stmt.value !is null ? evaluate(stmt.value) : TValue.nil(tvalueNil); + LoxValue value = stmt.value !is null ? evaluate(stmt.value) : new LoxNil(); throw new Return(value); } void visit(Stmt.Var stmt){ - environment.define(stmt.name.lexeme, stmt.initialiser is null ? TValue.nil(tvalueNil) : evaluate(stmt.initialiser)); + environment.define(stmt.name.lexeme, stmt.initialiser is null ? new LoxNil() : evaluate(stmt.initialiser)); } void visit(Stmt.While stmt){ while(isTruthy(evaluate(stmt.condition))) execute(stmt.body); } - TValue visit(Expr.Literal expr){ + LoxValue visit(Expr.Literal expr){ return expr.value; } - TValue visit(Expr.Grouping expr){ + LoxValue visit(Expr.Grouping expr){ return evaluate(expr.expression); } - TValue visit(Expr.Unary expr){ - TValue right = evaluate(expr.right); + LoxValue visit(Expr.Unary expr){ + LoxValue right = evaluate(expr.right); switch(expr.operator.type){ case TokenType.MINUS: checkNumberOperand(expr.operator, right); - return TValue.dbl(-right.dblValue); + return new LoxNum(-cast(LoxNum)right); case TokenType.BANG: - return TValue.bln(!isTruthy(right)); + return new LoxBool(!isTruthy(right)); default: assert(0); } } - TValue visit(Expr.Logical expr){ - TValue left = evaluate(expr.left); + LoxValue visit(Expr.Logical expr){ + LoxValue left = evaluate(expr.left); if(expr.operator.type == TokenType.OR){ if(isTruthy(left)) return left; @@ -167,64 +180,71 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { } return evaluate(expr.right); } - TValue visit(Expr.Binary expr){ - TValue left = evaluate(expr.left); - TValue right = evaluate(expr.right); - static string m(TokenType t, string op, string v, string vv){ - return q{case %s: - checkNumberOperand(expr.operator, left); - checkNumberOperand(expr.operator, right); - return TValue.%s( left.%s %s right.%s ); - }.format(t, v, vv, op, vv); - } + LoxValue visit(Expr.Set expr){ + LoxValue object = evaluate(expr.object); + if(!(cast(LoxInstance)object)) + throw new RuntimeError(expr.name, "Only instances have fields."); + LoxValue value = evaluate(expr.value); + (cast(LoxInstance)object).set(expr.name, value); + return value; + } + LoxValue visit(Expr.This expr){ + return lookUpVariable(expr.keyword, expr); + } + LoxValue visit(Expr.Binary expr){ + LoxValue left = evaluate(expr.left); + LoxValue right = evaluate(expr.right); with(TokenType) switch(expr.operator.type){ - static foreach(t, op; [ MINUS: "-", SLASH: "/", STAR: "*" ]) - mixin(ctEval!(m(t, op, "dbl", "dblValue"))); - - case PLUS: - if(left.isDbl && right.isDbl) - return TValue.dbl(left.dblValue + right.dblValue); - else if(left.isStr && right.isStr) - return TValue.str(left.strValue ~ right.strValue); - version(LoxConcatNonStrings){ - if(left.isStr || right.isStr) - return TValue.str(tvalueToString(left) ~ tvalueToString(right)); - } - checkNumberOperand(expr.operator, left); - checkNumberOperand(expr.operator, right); - - static foreach(t, op; [ GREATER: ">", GREATER_EQUAL: ">=", LESS: "<", LESS_EQUAL: "<=" ]) - mixin(ctEval!(m(t, op, "bln", "dblValue"))); + static foreach(t, op; [ PLUS: "+", MINUS: "-", SLASH: "/", STAR: "*", GREATER: ">", GREATER_EQUAL: ">=", LESS: "<", LESS_EQUAL: "<=" ]){ + case t: + static if(t == PLUS){ + if(cast(LoxStr)left && cast(LoxStr)right) + return new LoxStr(cast(LoxStr)left ~ cast(LoxStr)right); + version(LoxConcatNonStrings){ + if(cast(LoxStr)left || cast(LoxStr)right) + return new LoxStr(left.toString ~ right.toString); + } + } + checkNumberOperand(expr.operator, left); + checkNumberOperand(expr.operator, right); + return (cast(LoxNum)left).opBinary!op(cast(LoxNum)right); + } case BANG_EQUAL: - return TValue.bln(!isEqual(left, right)); + return new LoxBool(!isEqual(left, right)); case EQUAL_EQUAL: - return TValue.bln(isEqual(left, right)); + return new LoxBool(isEqual(left, right)); default: - assert(0); + assert(0); } } - TValue visit(Expr.Call expr){ - TValue callee = evaluate(expr.callee); - if(!callee.isCal) + LoxValue visit(Expr.Call expr){ + LoxValue callee = evaluate(expr.callee); + if(!cast(LoxCallable)callee) throw new RuntimeError(expr.paren, "Can only call functions and classes."); auto arguments = expr.arguments.map!(a => evaluate(a)); - LoxCallable func = callee.calValue; + LoxCallable func = cast(LoxCallable)callee; if(arguments.length != func.arity()) throw new RuntimeError(expr.paren, "Expected " ~ func.arity().to!string ~ " arguments but got " ~ arguments.length.to!string ~ "."); return func.call(this, arguments.array); } - TValue visit(Expr.Variable expr){ + LoxValue visit(Expr.Get expr){ + LoxValue object = evaluate(expr.object); + if(auto ins = cast(LoxInstance)object) + return ins.get(expr.name); + throw new RuntimeError(expr.name, "Only instances have properties."); + } + LoxValue visit(Expr.Variable expr){ return lookUpVariable(expr.name, expr); } - private TValue lookUpVariable(Token name, Expr expr){ + private LoxValue lookUpVariable(Token name, Expr expr){ if(const distance = expr in locals) return environment.getAt(*distance, name.lexeme); else return globals.get(name); } - TValue visit(Expr.Assign expr){ - TValue value = evaluate(expr.value); + LoxValue visit(Expr.Assign expr){ + LoxValue value = evaluate(expr.value); if(const distance = expr in locals) environment.assignAt(*distance, expr.name, value); else diff --git a/src/jlox/loxclass.d b/src/jlox/loxclass.d new file mode 100644 index 0000000..da8a202 --- /dev/null +++ b/src/jlox/loxclass.d @@ -0,0 +1,34 @@ +module jlox.loxclass; + +import jlox.token; +import jlox.interpreter; +import jlox.loxinstance; +import jlox.loxfunction; +import common.util; + +class LoxClass : LoxCallable{ + package const string name; + private const LoxFunction[string] methods; + mixin defaultCtor; + + LoxFunction findMethod(string name){ + if(auto method = name in methods) + return cast(LoxFunction)*method; + return null; + } + + int arity(){ + if(auto initialiser = findMethod("init")) + return initialiser.arity(); + return 0; + } + LoxValue call(Interpreter interpreter, LoxValue[] arguments){ + LoxInstance instance = new LoxInstance(this); + LoxFunction initialiser = findMethod("init"); + if(initialiser !is null) + initialiser.bind(instance).call(interpreter, arguments); + return instance; + } + override string toString() const => "class"; +} + diff --git a/src/jlox/loxfunction.d b/src/jlox/loxfunction.d index 85bbb32..504369e 100644 --- a/src/jlox/loxfunction.d +++ b/src/jlox/loxfunction.d @@ -2,31 +2,44 @@ module jlox.loxfunction; import std.conv; +import common.util; import jlox.token; import jlox.stmt; import jlox.interpreter; import jlox.environment; -import common.util; +import jlox.loxinstance; class LoxFunction : LoxCallable{ private Stmt.Function declaration; private Environment closure; + private const bool isInitialiser; mixin defaultCtor; invariant{ assert(declaration && closure); } + + LoxFunction bind(LoxInstance instance){ + Environment environment = new Environment(closure); + environment.define("this", instance); + return new LoxFunction(declaration, environment, isInitialiser); + } int arity() => declaration.params.length.to!int; - TValue call(Interpreter interpreter, TValue[] arguments){ + LoxValue call(Interpreter interpreter, LoxValue[] arguments){ Environment environment = new Environment(closure); foreach(i; 0 .. declaration.params.length) environment.define(declaration.params[i].lexeme, arguments[i]); try{ interpreter.executeBlock(declaration.body, environment); } catch(Return returnValue){ - return returnValue.value; + if(isInitialiser) + return closure.getAt(0, "this"); + return cast(LoxValue)returnValue.value; } - return TValue.nil(tvalueNil); + if(isInitialiser) + return closure.getAt(0, "this"); + return new LoxNil(); } + override string toString() const => "function"; } diff --git a/src/jlox/loxinstance.d b/src/jlox/loxinstance.d new file mode 100644 index 0000000..f30d858 --- /dev/null +++ b/src/jlox/loxinstance.d @@ -0,0 +1,27 @@ +module jlox.loxinstance; + +import std.format : format; + +import jlox.token; +import jlox.interpreter; +import jlox.loxclass; +import common.util; + +class LoxInstance : LoxValue{ + private LoxClass cls; + private LoxValue[string] fields; + mixin defaultCtor; + override string toString() const => "".format(cls.name); + + LoxValue get(Token name){ + if(auto v = name.lexeme in fields) + return *v; + if(auto method = cls.findMethod(name.lexeme)) + return method.bind(this); + throw new RuntimeError(name, "Undefined property '" ~ name.lexeme ~ "'."); + } + void set(Token name, LoxValue value){ + fields[name.lexeme] = value; + } +} + diff --git a/src/jlox/main.d b/src/jlox/main.d index 71a7541..17f176a 100644 --- a/src/jlox/main.d +++ b/src/jlox/main.d @@ -47,7 +47,10 @@ static class Lox{ hadError = true; } static void runtimeError(RuntimeError error){ - stderr.writeln(error.msg ~ "\n[line " ~ error.token.line.to!string ~ "]"); + if(error.token) + stderr.writeln(error.msg ~ "\n[line " ~ error.token.line.to!string ~ "]"); + else + stderr.writeln(error.msg); hadRuntimeError = true; } @@ -86,6 +89,8 @@ static class Lox{ } } +extern(C) int isatty(int); + int main(string[] argv){ auto args = new Program("lox") .add(new Argument("path").optional.acceptsFiles) @@ -96,12 +101,16 @@ int main(string[] argv){ stderr.writeln("Cant have both path and --cmd"); return RetVal.other; } - if(auto cmd = args.option("command")) + if(auto cmd = args.option("command")){ Lox.run(cmd); - else if(auto path = args.arg("path")) + } else if(auto path = args.arg("path")){ Lox.runFile(path); - else - Lox.runPrompt(); + } else { + if(isatty(stdin.fileno)) + Lox.runPrompt(); + else + Lox.runFile("/dev/stdin"); + } } catch(MainException rte){ return Lox.hadError ? RetVal.other : RetVal.runtime; } diff --git a/src/jlox/parser.d b/src/jlox/parser.d index f0d1133..eea1900 100644 --- a/src/jlox/parser.d +++ b/src/jlox/parser.d @@ -83,7 +83,7 @@ class Parser{ private Stmt printStatement(){ version(LoxPrintMultiple){ Expr[] values; - do { + if(!check(TokenType.SEMICOLON)) do { values ~= expression(); } while(match(TokenType.COMMA)); consume(TokenType.SEMICOLON, "Expect ';' after values."); @@ -144,7 +144,7 @@ class Parser{ else initialiser = expressionStatement(); - Expr condition = check(TokenType.SEMICOLON) ? new Expr.Literal(TValue.bln(true)) : expression(); + Expr condition = check(TokenType.SEMICOLON) ? new Expr.Literal(new LoxBool(true)) : expression(); consume(TokenType.SEMICOLON, "Expect ';' after loop condition."); Expr increment; @@ -197,8 +197,11 @@ class Parser{ if(match(TokenType.EQUAL)){ Token equals = previous(); Expr value = assignment(); - if(auto v = cast(Expr.Variable)expr) + if(auto v = cast(Expr.Variable)expr){ return new Expr.Assign(v.name, value); + } else if(auto get = cast(Expr.Get)expr){ + return new Expr.Set(get.object, get.name, value); + } error(equals, "Invalid assignment target."); } return expr; @@ -280,10 +283,14 @@ class Parser{ } Expr expr = primary(); while(true){ - if(match(TokenType.LEFT_PAREN)) + if(match(TokenType.LEFT_PAREN)){ expr = finishCall(expr); - else + } else if(match(TokenType.DOT)){ + Token name = consume(TokenType.IDENTIFIER, "Expect property name after '.'."); + expr = new Expr.Get(expr, name); + } else { break; + } } return expr; } @@ -291,13 +298,15 @@ class Parser{ if(match(TokenType.IDENTIFIER)) return new Expr.Variable(previous()); if(match(TokenType.FALSE)) - return new Expr.Literal(TValue.bln(false)); + return new Expr.Literal(new LoxBool(false)); if(match(TokenType.TRUE)) - return new Expr.Literal(TValue.bln(true)); + return new Expr.Literal(new LoxBool(true)); if(match(TokenType.NIL)) - return new Expr.Literal(tvalueNil); + return new Expr.Literal(new LoxNil()); if(match(TokenType.NUMBER, TokenType.STRING)) return new Expr.Literal(previous().literal); + if(match(TokenType.THIS)) + return new Expr.This(previous()); if(match(TokenType.LEFT_PAREN)){ Expr expr = expression(); consume(TokenType.RIGHT_PAREN, "Expect ')' after expression."); @@ -313,12 +322,23 @@ class Parser{ consume(TokenType.SEMICOLON, "Expect ';' after variable declaration."); return new Stmt.Var(name, initialiser); } + private Stmt classDeclaration() { + Token name = consume(TokenType.IDENTIFIER, "Expect class name."); + consume(TokenType.LEFT_BRACE, "Expect '{' before class body."); + Stmt.Function[] methods; + while(!check(TokenType.RIGHT_BRACE) && !isAtEnd) + methods ~= fun("method"); + consume(TokenType.RIGHT_BRACE, "Expect '}' after class body."); + return new Stmt.Class(name, methods); + } private Stmt declaration(){ try { - if(match(TokenType.FUN)) - return fun("function"); if(match(TokenType.VAR)) return varDeclaration(); + if(match(TokenType.FUN)) + return fun("function"); + if(match(TokenType.CLASS)) + return classDeclaration(); return statement(); } catch(ParseError error){ synchronise(); diff --git a/src/jlox/resolver.d b/src/jlox/resolver.d index 0b02a35..b5cc879 100644 --- a/src/jlox/resolver.d +++ b/src/jlox/resolver.d @@ -2,6 +2,7 @@ module jlox.resolver; import std.container.slist; import std.stdio; +import std.algorithm; import std.range : enumerate; import jlox.main; @@ -11,14 +12,15 @@ import jlox.expr; import jlox.interpreter; import common.util; -private enum FunctionType { NONE, FUNCTION } - class Resolver : Stmt.Visitor!void, Expr.Visitor!void { + private enum FunctionType{ NONE, FUNCTION, INITIALISER, METHOD } + private enum ClassType{ NONE, CLASS } private Interpreter interpreter; this(Interpreter interpreter){ this.interpreter = interpreter; } private FunctionType currentFunction = FunctionType.NONE; + private ClassType currentClass = ClassType.NONE; private SList!(bool[string]) scopes; private void resolve(Stmt stmt) => stmt.accept(this); private void resolve(Expr expr) => expr.accept(this); @@ -71,6 +73,23 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { resolve(stmt.statements); endScope(); } + void visit(Stmt.Class stmt){ + ClassType enclosingClass = currentClass; + currentClass = ClassType.CLASS; + scope(exit) + currentClass = enclosingClass; + declare(stmt.name); + define(stmt.name); + beginScope(); + scopes.front["this"] = true; + foreach(method; stmt.methods){ + FunctionType declaration = FunctionType.METHOD; + if(method.name.lexeme == "init") + declaration = FunctionType.INITIALISER; + resolveFunction(method, declaration); + } + endScope(); + } void visit(Stmt.Var stmt){ declare(stmt.name); if(stmt.initialiser !is null) @@ -112,8 +131,11 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { void visit(Stmt.Return stmt){ if(currentFunction == FunctionType.NONE) Lox.error(stmt.keyword, "Can't return from top-level code."); - if(stmt.value !is null) + if(stmt.value !is null){ + if(currentFunction == FunctionType.INITIALISER) + Lox.error(stmt.keyword, "Can't return a value from an initializer."); resolve(stmt.value); + } } void visit(Stmt.While stmt){ resolve(stmt.condition); @@ -128,6 +150,9 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { foreach(argument; expr.arguments) resolve(argument); } + void visit(Expr.Get expr){ + resolve(expr.object); + } void visit(Expr.Grouping expr){ resolve(expr.expression); } @@ -136,6 +161,17 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { resolve(expr.left); resolve(expr.right); } + void visit(Expr.Set expr){ + resolve(expr.value); + resolve(expr.object); + } + void visit(Expr.This expr){ + if(currentClass == ClassType.NONE){ + Lox.error(expr.keyword, "Can't use 'this' outside of a class."); + return; + } + resolveLocal(expr, expr.keyword); + } void visit(Expr.Unary expr){ resolve(expr.right); } diff --git a/src/jlox/scanner.d b/src/jlox/scanner.d index 5d0a412..8e78a08 100644 --- a/src/jlox/scanner.d +++ b/src/jlox/scanner.d @@ -30,7 +30,7 @@ class Scanner { current++; return true; } - private void addToken(TokenType type, TValue literal = TValue.nil(tvalueNil)){ + private void addToken(TokenType type, LoxValue literal = new LoxNil()){ string text = source[start .. current]; tokens ~= new Token(type, text, literal, line); } @@ -54,7 +54,7 @@ class Scanner { } advance(); string value = source[start + 1 .. current -1]; - addToken(TokenType.STRING, TValue.str(value)); + addToken(TokenType.STRING, new LoxStr(value)); } private void number(){ while(peek().isDigit) @@ -64,7 +64,7 @@ class Scanner { while(peek().isDigit) advance(); } - addToken(TokenType.NUMBER, TValue.dbl(source[start .. current].to!double)); + addToken(TokenType.NUMBER, new LoxNum(source[start .. current].to!double)); } private TokenType keywords(string word){ with(TokenType) switch(word){ diff --git a/src/jlox/stmt.d b/src/jlox/stmt.d index 767f58e..3d42392 100644 --- a/src/jlox/stmt.d +++ b/src/jlox/stmt.d @@ -12,6 +12,7 @@ import jlox.expr; abstract class Stmt{ interface Visitor(R){ R visit(Block expr); + R visit(Class expr); R visit(Expression expr); R visit(Function expr); R visit(If expr); @@ -33,6 +34,11 @@ abstract class Stmt{ Stmt[] statements; mixin defCtorAndAccept; } + static class Class : typeof(this){ + Token name; + Function[] methods; + mixin defCtorAndAccept; + } static class Expression : typeof(this){ Expr expression; mixin defCtorAndAccept; diff --git a/src/jlox/token.d b/src/jlox/token.d index fb2c117..dcbf5a3 100644 --- a/src/jlox/token.d +++ b/src/jlox/token.d @@ -1,48 +1,70 @@ module jlox.token; +import std.stdio; import std.conv; +import std.format : format; -import taggedalgebraic; - +import common.util; import jlox.tokentype; import jlox.interpreter; +import jlox.loxclass; +import jlox.loxinstance; +import jlox.loxfunction; -interface LoxCallable{ +abstract interface LoxValue{ + string toString() const; + final bool equals(LoxValue v){ + if(this.classinfo != v.classinfo) + return false; + else if(auto s = cast(LoxStr)this) + return s.str == (cast(LoxStr)v).str; + else if(auto n = cast(LoxNum)this) + return n.num == (cast(LoxNum)v).num; + else if(auto b = cast(LoxBool)this) + return b.bln == (cast(LoxBool)v).bln; + return this == v; + }; +} +interface LoxCallable : LoxValue{ int arity(); - TValue call(Interpreter interpreter, TValue[] arguments); + LoxValue call(Interpreter interpreter, LoxValue[] arguments); } -private struct Value{ - string str; - double dbl; - bool bln; - LoxCallable cal; - struct Nil{} - Nil nil; +class LoxStr : LoxValue{ + immutable string str; + alias str this; + mixin defaultCtor; + override string toString() const => str; } -alias TValue = TaggedUnion!Value; -immutable tvalueNil = Value.Nil(); -string tvalueToString(TValue val){ - final switch(val.kind){ - case TValue.Kind.str: - return val.strValue; - case TValue.Kind.dbl: - return val.dblValue.to!string; - case TValue.Kind.bln: - return val.blnValue ? "true" : "false"; - case TValue.Kind.cal: - return ""; - case TValue.Kind.nil: - return "nil"; +class LoxBool : LoxValue{ + immutable bool bln; + alias bln this; + mixin defaultCtor; + override string toString() const => bln.to!string; +} +class LoxNum : LoxValue{ + immutable double num; + alias num this; + mixin defaultCtor; + override string toString() const => num.to!string; + auto opBinary(string op)(const LoxNum rhs) const{ + mixin("auto v = num", op, "rhs.num;"); + static if(is(typeof(v) == bool)) + return new LoxBool(v); + else + return new LoxNum(v); } } +class LoxNil : LoxValue{ + override string toString() const => ""; +} class Token{ TokenType type; string lexeme; - TValue literal; + LoxValue literal; int line; - this(TokenType type, string lexeme, TValue literal, int line) { + this(TokenType type, string lexeme, LoxValue literal, int line) { this.type = type; this.lexeme = lexeme; this.literal = literal; diff --git a/test/all.d b/test/all.d index 2145c73..e891ff1 100755 --- a/test/all.d +++ b/test/all.d @@ -1,5 +1,6 @@ #!/bin/env rdmd +import std.stdio; import std.process; import std.concurrency; import std.conv; @@ -7,18 +8,20 @@ import std.string, std.format; import std.algorithm, std.range; void main(){ - auto scheduler = new ThreadScheduler(); - scheduler.spawn((){ "./test/closure.lox".run.output.match("1\n2\n"); }); - scheduler.spawn((){ "./test/scope.lox".run.output.match("global first first second first ".replace(' ', '\n').repeat(2).join("\n")); }); - scheduler.spawn((){ "./test/fib_for.lox".run.output.match(fib(6765)); }); - scheduler.spawn((){ "./test/fib_recursive.lox".run.output.match(fib(34)); }); - scheduler.spawn((){ "./test/fib_closure.lox".run.output.match(fib(34)); }); + "./test/ops.lox".match("1\n2\n3\n4\n5\n6\n7\ntrue\nfalse\ntrue\ntrue\nhello, world\n"); + "./test/shortcircuit.lox".match("true\nAAAA!\nAAAA!\nAAAA?\n"); + "./test/closure.lox".match("1\n2\n"); + "./test/scope.lox".match("global first first second first ".replace(' ', '\n').repeat(2).join("\n")); + "./test/fib_for.lox".match(fib(6765)); + "./test/fib_recursive.lox".match(fib(34)); + "./test/fib_closure.lox".match(fib(34)); + "./test/class.lox".match("The German chocolate cake is delicious!\n"); - scheduler.spawn((){ "./test/err/already_defined.lox".shouldFail(RetVal.other, "Already a variable with this name"); }); - scheduler.spawn((){ "./test/err/undefined_var.lox".shouldFail(RetVal.runtime, "Undefined variable"); }); - scheduler.spawn((){ "./test/err/self_ref_vardecl.lox".shouldFail(RetVal.runtime, "Undefined variable"); }); - scheduler.spawn((){ "./test/err/invalid_syntax.lox".shouldFail(RetVal.other); }); - scheduler.spawn((){ "./test/err/global_scope_return.lox".shouldFail(RetVal.other, "Can't return from top-level code"); }); + "./test/err/already_defined.lox".shouldFail(RetVal.other, "Already a variable with this name"); + "./test/err/undefined_var.lox".shouldFail(RetVal.runtime, "Undefined variable"); + "./test/err/self_ref_vardecl.lox".shouldFail(RetVal.runtime, "Undefined variable"); + "./test/err/invalid_syntax.lox".shouldFail(RetVal.other); + "./test/err/global_scope_return.lox".shouldFail(RetVal.other, "Can't return from top-level code"); } enum RetVal{ @@ -37,8 +40,9 @@ string fib(uint n){ return r; } auto run(string file) => [ "./lox", file ].execute; -void match(string res, string correct){ - assert(res == correct, "Match failed\n-- Got --\n%s\n-- Expected --\n%s".format(res, correct)); +void match(string file, string correct){ + auto res = file.run.output; + assert(res == correct, "Match %s failed\n-- Got --\n%s\n-- Expected --\n%s".format(file, res, correct)); } void shouldFail(string file, int code = 1, string msg = null){ auto c = file.run; diff --git a/test/class.lox b/test/class.lox new file mode 100644 index 0000000..87bfb6a --- /dev/null +++ b/test/class.lox @@ -0,0 +1,21 @@ + +class Cake{ + init(goodness){ + this.adjective = this.rate(goodness); + } + rate(goodness){ + if(goodness) + return "delicious"; + else + return "horrendous"; + } + taste(){ + print "The " + this.flavor + " cake is " + this.adjective + "!"; + } +} + +var cake = Cake(true); +cake.flavor = "German chocolate"; +var t = cake.taste; +t(); + diff --git a/test/ops.lox b/test/ops.lox new file mode 100644 index 0000000..30da068 --- /dev/null +++ b/test/ops.lox @@ -0,0 +1,17 @@ + +print 2 - 1; +print 1 + 1; +print 6 / 2; +print 2 * 2; +print 2 * 3 - 1; +print 1 + 2 + 3; +print 1 + 2 * 3; + +print 2 <= 3 or false; +print 1 > 2; + +print 1 == 1; +print 1 != 2; + +print "hello, " + "world"; + diff --git a/test/shortcircuit.lox b/test/shortcircuit.lox new file mode 100644 index 0000000..7e03749 --- /dev/null +++ b/test/shortcircuit.lox @@ -0,0 +1,12 @@ + +fun scream(extra){ + extra = extra or "!"; + print "AAAA" + extra; +} + +print nil or true; + +scream(false); +scream(nil); +scream("?"); + diff --git a/test/test.lox b/test/test.lox index 8b13789..139597f 100644 --- a/test/test.lox +++ b/test/test.lox @@ -1 +1,2 @@ +