From b0d934707ba17d51396605871f4455877e772091 Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 02:24:09 +0000 Subject: [PATCH 01/10] Added LoxPrintMultiple version --- dub.sdl | 2 +- src/jlox/interpreter.d | 8 ++++++-- src/jlox/parser.d | 15 ++++++++++++--- src/jlox/stmt.d | 5 ++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/dub.sdl b/dub.sdl index 58a534e..ce86bdd 100644 --- a/dub.sdl +++ b/dub.sdl @@ -8,7 +8,7 @@ dependency "taggedalgebraic" version="~>0.11.23" targetType "executable" buildRequirements "requireBoundsCheck" "requireContracts" -versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" +versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" "LoxPrintMultiple" configuration "jlox" { sourcePaths "src/jlox" "src/common" diff --git a/src/jlox/interpreter.d b/src/jlox/interpreter.d index d248eda..157d0a5 100644 --- a/src/jlox/interpreter.d +++ b/src/jlox/interpreter.d @@ -116,8 +116,12 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { execute(stmt.elseBranch); } void visit(Stmt.Print stmt){ - TValue value = evaluate(stmt.expression); - writeln(tvalueToString(value)); + version(LoxPrintMultiple){ + writeln(stmt.expressions.map!(x => evaluate(x)).map!tvalueToString.join("\t")); + } else { + TValue value = evaluate(stmt.expression); + writeln(tvalueToString(value)); + } } void visit(Stmt.Return stmt){ TValue value = stmt.value !is null ? evaluate(stmt.value) : TValue.nil(tvalueNil); diff --git a/src/jlox/parser.d b/src/jlox/parser.d index 6ebc002..f0d1133 100644 --- a/src/jlox/parser.d +++ b/src/jlox/parser.d @@ -81,9 +81,18 @@ class Parser{ } private Stmt printStatement(){ - Expr value = expression(); - consume(TokenType.SEMICOLON, "Expect ';' after value."); - return new Stmt.Print(value); + version(LoxPrintMultiple){ + Expr[] values; + do { + values ~= expression(); + } while(match(TokenType.COMMA)); + consume(TokenType.SEMICOLON, "Expect ';' after values."); + return new Stmt.Print(values); + } else { + Expr value = expression(); + consume(TokenType.SEMICOLON, "Expect ';' after value."); + return new Stmt.Print(value); + } } private Stmt returnStatement(){ Token keyword = previous(); diff --git a/src/jlox/stmt.d b/src/jlox/stmt.d index ad701c8..767f58e 100644 --- a/src/jlox/stmt.d +++ b/src/jlox/stmt.d @@ -50,7 +50,10 @@ abstract class Stmt{ mixin defCtorAndAccept; } static class Print : typeof(this){ - Expr expression; + version(LoxPrintMultiple) + Expr[] expressions; + else + Expr expression; mixin defCtorAndAccept; } static class Return : typeof(this){ From a1acefab0ea05d1bc7437b0f454c54216a9a9152 Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 02:58:54 +0000 Subject: [PATCH 02/10] Improve tests --- test/all.d | 5 +++-- test/fib_closure.lox | 18 ++++++++++++++++++ test/{fib21.lox => fib_for.lox} | 0 test/{fib10.lox => fib_recursive.lox} | 0 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 test/fib_closure.lox rename test/{fib21.lox => fib_for.lox} (100%) rename test/{fib10.lox => fib_recursive.lox} (100%) diff --git a/test/all.d b/test/all.d index 5b79f0e..71de62f 100755 --- a/test/all.d +++ b/test/all.d @@ -15,8 +15,9 @@ void main(){ } return r; } - assert([ "./lox", "test/fib21.lox" ].execute.output == fib(6765)); - assert([ "./lox", "test/fib10.lox" ].execute.output == fib(34)); assert([ "./lox", "test/closure.lox" ].execute.output == "1\n2\n"); + assert([ "./lox", "test/fib_for.lox" ].execute.output == fib(6765)); + assert([ "./lox", "test/fib_recursive.lox" ].execute.output == fib(34)); + assert([ "./lox", "test/fib_closure.lox" ].execute.output == fib(34)); } diff --git a/test/fib_closure.lox b/test/fib_closure.lox new file mode 100644 index 0000000..42c8fe8 --- /dev/null +++ b/test/fib_closure.lox @@ -0,0 +1,18 @@ + +fun fibonacci(){ + var a = 0; + var b = 1; + fun f(){ + var next = a; + a = b; + b = next + b; + return next; + } + return f; +} + +var nextFibonacci = fibonacci(); + +for(var i = 0; i < 10; i = i + 1) + print nextFibonacci(); + diff --git a/test/fib21.lox b/test/fib_for.lox similarity index 100% rename from test/fib21.lox rename to test/fib_for.lox diff --git a/test/fib10.lox b/test/fib_recursive.lox similarity index 100% rename from test/fib10.lox rename to test/fib_recursive.lox From 52a7b73a9eb6232019ad6db4d461c273e999de6d Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 16:04:04 +0000 Subject: [PATCH 03/10] Resolving and Binding 11 --- src/common/util.d | 1 + src/jlox/environment.d | 12 +++ src/jlox/expr.d | 2 +- src/jlox/interpreter.d | 18 +++- src/jlox/main.d | 15 +++- src/jlox/resolver.d | 143 +++++++++++++++++++++++++++++++ test/all.d | 56 ++++++++---- test/err/already_defined.lox | 6 ++ test/err/global_scope_return.lox | 3 + test/err/invalid_syntax.lox | 3 + test/err/self_ref_vardecl.lox | 3 + test/err/undefined_var.lox | 3 + test/scope.lox | 19 ++++ test/test.lox | 1 + 14 files changed, 263 insertions(+), 22 deletions(-) create mode 100644 src/jlox/resolver.d create mode 100644 test/err/already_defined.lox create mode 100644 test/err/global_scope_return.lox create mode 100644 test/err/invalid_syntax.lox create mode 100644 test/err/self_ref_vardecl.lox create mode 100644 test/err/undefined_var.lox create mode 100644 test/scope.lox create mode 100644 test/test.lox diff --git a/src/common/util.d b/src/common/util.d index 14e0917..f8dc60d 100644 --- a/src/common/util.d +++ b/src/common/util.d @@ -6,3 +6,4 @@ template defaultCtor(){ this.tupleof[i] = a; } } + diff --git a/src/jlox/environment.d b/src/jlox/environment.d index 06023dc..baee5fe 100644 --- a/src/jlox/environment.d +++ b/src/jlox/environment.d @@ -20,6 +20,15 @@ class Environment{ return enclosing.get(name); throw new RuntimeError(name, "Undefined variable '" ~ name.lexeme ~ "'."); } + Environment ancestor(uint distance){ + Environment environment = this; + for(uint i = 0; i < distance; i++) + environment = environment.enclosing; + return environment; + } + TValue 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; @@ -27,5 +36,8 @@ class Environment{ return enclosing.assign(name, value); throw new RuntimeError(name, "Undefined variable '" ~ name.lexeme ~ "'."); } + void assignAt(uint distance, Token name, TValue value){ + ancestor(distance).values[name.lexeme] = value; + } } diff --git a/src/jlox/expr.d b/src/jlox/expr.d index 2aea4b1..ed313f6 100644 --- a/src/jlox/expr.d +++ b/src/jlox/expr.d @@ -21,7 +21,7 @@ abstract class Expr{ R visit(Unary expr); R visit(Variable expr); } - private alias rTypes = AliasSeq!(string, TValue); + private alias rTypes = AliasSeq!(string, TValue, void); static foreach(T; rTypes) abstract T accept(Visitor!T visitor); private template defCtorAndAccept(){ diff --git a/src/jlox/interpreter.d b/src/jlox/interpreter.d index 157d0a5..9af9dd3 100644 --- a/src/jlox/interpreter.d +++ b/src/jlox/interpreter.d @@ -97,7 +97,10 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { return; throw new RuntimeError(operator, "Operand must be a number."); } - + private uint[Expr] locals; + void resolve(Expr expr, uint depth){ + locals[expr] = depth; + } void visit(Stmt.Block stmt){ executeBlock(stmt.statements, new Environment(environment)); @@ -212,11 +215,20 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { return func.call(this, arguments.array); } TValue visit(Expr.Variable expr){ - return environment.get(expr.name); + return lookUpVariable(expr.name, expr); + } + private TValue 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); - environment.assign(expr.name, value); + if(const distance = expr in locals) + environment.assignAt(*distance, expr.name, value); + else + globals.assign(expr.name, value); return value; } } diff --git a/src/jlox/main.d b/src/jlox/main.d index 51e0e33..71a7541 100644 --- a/src/jlox/main.d +++ b/src/jlox/main.d @@ -14,6 +14,11 @@ import jlox.parser; import jlox.interpreter; import jlox.expr; import jlox.stmt; +import jlox.resolver; + +enum RetVal{ + success = 0, other = 1, runtime = 2 +} private class MainException : Exception{ this(){ @@ -52,7 +57,11 @@ static class Lox{ Parser parser = new Parser(tokens); Stmt[] statements = parser.parse(); + if(hadError) + return; + Resolver resolver = new Resolver(interpreter); + resolver.resolve(statements); if(hadError) return; @@ -85,7 +94,7 @@ int main(string[] argv){ try{ if(args.arg("path") && args.option("command")){ stderr.writeln("Cant have both path and --cmd"); - return 3; + return RetVal.other; } if(auto cmd = args.option("command")) Lox.run(cmd); @@ -94,8 +103,8 @@ int main(string[] argv){ else Lox.runPrompt(); } catch(MainException rte){ - return Lox.hadError ? 1 : 2; + return Lox.hadError ? RetVal.other : RetVal.runtime; } - return 0; + return RetVal.success; } diff --git a/src/jlox/resolver.d b/src/jlox/resolver.d new file mode 100644 index 0000000..0b02a35 --- /dev/null +++ b/src/jlox/resolver.d @@ -0,0 +1,143 @@ +module jlox.resolver; + +import std.container.slist; +import std.stdio; +import std.range : enumerate; + +import jlox.main; +import jlox.token; +import jlox.stmt; +import jlox.expr; +import jlox.interpreter; +import common.util; + +private enum FunctionType { NONE, FUNCTION } + +class Resolver : Stmt.Visitor!void, Expr.Visitor!void { + private Interpreter interpreter; + this(Interpreter interpreter){ + this.interpreter = interpreter; + } + private FunctionType currentFunction = FunctionType.NONE; + private SList!(bool[string]) scopes; + private void resolve(Stmt stmt) => stmt.accept(this); + private void resolve(Expr expr) => expr.accept(this); + void resolve(Stmt[] statements){ + foreach(statement; statements) + resolve(statement); + } + private void resolveLocal(Expr expr, Token name){ + foreach(i, scp; scopes[].enumerate){ + if(name.lexeme in scp){ + interpreter.resolve(expr, cast(uint)i); + return; + } + } + } + private void resolveFunction(Stmt.Function func, FunctionType type){ + FunctionType enclosingFunction = currentFunction; + currentFunction = type; + scope(exit) + currentFunction = enclosingFunction; + beginScope(); + foreach(param; func.params){ + declare(param); + define(param); + } + resolve(func.body); + endScope(); + } + private void endScope() => scopes.removeFront(); + private void beginScope(){ + bool[string] s; + scopes.insertFront(s); + } + private void declare(Token name){ + if(scopes.empty) + return; + bool[string] scp = scopes.front; + if(name.lexeme in scp) + Lox.error(name, "Already a variable with this name in this scope."); + scp[name.lexeme] = false; + } + private void define(Token name){ + if(scopes.empty) + return; + scopes.front[name.lexeme] = true; + } + + void visit(Stmt.Block stmt){ + beginScope(); + resolve(stmt.statements); + endScope(); + } + void visit(Stmt.Var stmt){ + declare(stmt.name); + if(stmt.initialiser !is null) + resolve(stmt.initialiser); + define(stmt.name); + } + void visit(Stmt.Function stmt){ + declare(stmt.name); + define(stmt.name); + resolveFunction(stmt, FunctionType.FUNCTION); + } + void visit(Expr.Variable expr){ + if(!scopes.empty && scopes.front.get(expr.name.lexeme, true) == false) + Lox.error(expr.name, "Can't read local variable in its own initializer."); + resolveLocal(expr, expr.name); + } + void visit(Expr.Assign expr){ + resolve(expr.value); + resolveLocal(expr, expr.name); + } + + void visit(Stmt.Expression stmt){ + resolve(stmt.expression); + } + void visit(Stmt.If stmt){ + resolve(stmt.condition); + resolve(stmt.thenBranch); + if(stmt.elseBranch !is null) + resolve(stmt.elseBranch); + } + void visit(Stmt.Print stmt){ + version(LoxPrintMultiple){ + foreach(expr; stmt.expressions) + resolve(expr); + } else { + resolve(stmt.expression); + } + } + 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) + resolve(stmt.value); + } + void visit(Stmt.While stmt){ + resolve(stmt.condition); + resolve(stmt.body); + } + void visit(Expr.Binary expr){ + resolve(expr.left); + resolve(expr.right); + } + void visit(Expr.Call expr){ + resolve(expr.callee); + foreach(argument; expr.arguments) + resolve(argument); + } + void visit(Expr.Grouping expr){ + resolve(expr.expression); + } + void visit(Expr.Literal expr){} + void visit(Expr.Logical expr){ + resolve(expr.left); + resolve(expr.right); + } + void visit(Expr.Unary expr){ + resolve(expr.right); + } +} + diff --git a/test/all.d b/test/all.d index 71de62f..2145c73 100755 --- a/test/all.d +++ b/test/all.d @@ -1,23 +1,49 @@ #!/bin/env rdmd import std.process; +import std.concurrency; import std.conv; +import std.string, std.format; +import std.algorithm, std.range; void main(){ - string fib(uint n){ - string r = ""; - double a = 0; - double temp; - for(double b = 1; a <= n; b = temp + b){ - r ~= a.to!string ~ "\n"; - temp = a; - a = b; - } - return r; - } - assert([ "./lox", "test/closure.lox" ].execute.output == "1\n2\n"); - assert([ "./lox", "test/fib_for.lox" ].execute.output == fib(6765)); - assert([ "./lox", "test/fib_recursive.lox" ].execute.output == fib(34)); - assert([ "./lox", "test/fib_closure.lox" ].execute.output == fib(34)); + 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)); }); + + 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"); }); +} + +enum RetVal{ + success = 0, other = 1, runtime = 2 +} + +string fib(uint n){ + string r = ""; + double a = 0; + double temp; + for(double b = 1; a <= n; b = temp + b){ + r ~= a.to!string ~ "\n"; + temp = a; + a = b; + } + 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 shouldFail(string file, int code = 1, string msg = null){ + auto c = file.run; + assert(c.status == code, "Expected %s to fail with code %d but got %d".format(file, code, c.status)); + assert(!msg || c.output.toLower.indexOf(msg.toLower) >= 0, "ShouldFail %s failed\n-- Got --\n%s\n-- Expected --\n%s".format(file, c.output, msg)); + assert(c.output.indexOf("_Dmain") == -1, "ShouldFail %s got D exception\n%s".format(file, c.output)); } diff --git a/test/err/already_defined.lox b/test/err/already_defined.lox new file mode 100644 index 0000000..6d142d4 --- /dev/null +++ b/test/err/already_defined.lox @@ -0,0 +1,6 @@ + +fun bad(){ + var a = "first"; + var a = "second"; +} + diff --git a/test/err/global_scope_return.lox b/test/err/global_scope_return.lox new file mode 100644 index 0000000..c0658c2 --- /dev/null +++ b/test/err/global_scope_return.lox @@ -0,0 +1,3 @@ + +return ":)"; + diff --git a/test/err/invalid_syntax.lox b/test/err/invalid_syntax.lox new file mode 100644 index 0000000..24823b1 --- /dev/null +++ b/test/err/invalid_syntax.lox @@ -0,0 +1,3 @@ + +foo + diff --git a/test/err/self_ref_vardecl.lox b/test/err/self_ref_vardecl.lox new file mode 100644 index 0000000..e58a127 --- /dev/null +++ b/test/err/self_ref_vardecl.lox @@ -0,0 +1,3 @@ + +var x = x + 1; + diff --git a/test/err/undefined_var.lox b/test/err/undefined_var.lox new file mode 100644 index 0000000..b3162ca --- /dev/null +++ b/test/err/undefined_var.lox @@ -0,0 +1,3 @@ + +print hello; + diff --git a/test/scope.lox b/test/scope.lox new file mode 100644 index 0000000..4b9eadc --- /dev/null +++ b/test/scope.lox @@ -0,0 +1,19 @@ + +var a = "global"; + +fun f(){ + print a; + var a = "first"; + print a; + { + print a; + var a = "second"; + print a; + } + print a; +} + +f(); +print ""; +f(); + diff --git a/test/test.lox b/test/test.lox new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/test.lox @@ -0,0 +1 @@ + From d8ac625429420fd7bc9df105c246a159756a7464 Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 22:22:04 +0000 Subject: [PATCH 04/10] 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 @@ + From 848c846e09838fc5361e38341b4719e2228740ac Mon Sep 17 00:00:00 2001 From: nazrin Date: Mon, 2 Jun 2025 23:44:23 +0000 Subject: [PATCH 05/10] Inheritance 13 --- .gitignore | 1 + src/jlox/expr.d | 6 +++++ src/jlox/interpreter.d | 23 ++++++++++++++++- src/jlox/loxclass.d | 5 +++- src/jlox/parser.d | 13 +++++++++- src/jlox/resolver.d | 21 +++++++++++++++- src/jlox/stmt.d | 1 + test/all.d | 5 +++- test/err/super_outside_class.lox | 3 +++ test/err/super_without_superclass.lox | 8 ++++++ test/super.lox | 36 +++++++++++++++++++++++++++ test/test.lox | 2 -- 12 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 test/err/super_outside_class.lox create mode 100644 test/err/super_without_superclass.lox create mode 100644 test/super.lox delete mode 100644 test/test.lox diff --git a/.gitignore b/.gitignore index a8ba56d..5a66296 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ lox-test-* *.lst .msc/ +test/test.lox diff --git a/src/jlox/expr.d b/src/jlox/expr.d index 2c68f8b..f95a4ff 100644 --- a/src/jlox/expr.d +++ b/src/jlox/expr.d @@ -18,6 +18,7 @@ abstract class Expr{ R visit(Literal expr); R visit(Logical expr); R visit(Set expr); + R visit(Super expr); R visit(This expr); R visit(Unary expr); R visit(Variable expr); @@ -73,6 +74,11 @@ abstract class Expr{ Expr value; mixin defCtorAndAccept; } + static class Super : typeof(this){ + Token keyword; + Token method; + mixin defCtorAndAccept; + } static class This : typeof(this){ Token keyword; mixin defCtorAndAccept; diff --git a/src/jlox/interpreter.d b/src/jlox/interpreter.d index 319c62c..15870bc 100644 --- a/src/jlox/interpreter.d +++ b/src/jlox/interpreter.d @@ -1,6 +1,7 @@ module jlox.interpreter; import std.conv; +import std.exception : enforce; import std.stdio; import std.algorithm; import std.array; @@ -109,13 +110,24 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!LoxValue { executeBlock(stmt.statements, new Environment(environment)); } void visit(Stmt.Class stmt){ + LoxValue superclass; + if(stmt.superclass !is null){ + superclass = evaluate(stmt.superclass); + enforce(cast(LoxClass)superclass, new RuntimeError(stmt.superclass.name, "Superclass must be a class.")); + } environment.define(stmt.name.lexeme, new LoxNil()); + if(stmt.superclass !is null){ + environment = new Environment(environment); + environment.define("super", superclass); + } 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); + LoxClass cls = new LoxClass(stmt.name.lexeme, cast(LoxClass)superclass, methods); + if(superclass !is null) + environment = environment.enclosing; environment.assign(stmt.name, cls); } void visit(Stmt.Expression stmt){ @@ -188,6 +200,15 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!LoxValue { (cast(LoxInstance)object).set(expr.name, value); return value; } + LoxValue visit(Expr.Super expr){ + uint distance = locals[expr]; + LoxClass superclass = cast(LoxClass)environment.getAt(distance, "super"); + LoxInstance object = cast(LoxInstance)environment.getAt(distance - 1, "this"); + LoxFunction method = superclass.findMethod(expr.method.lexeme); + if(method is null) + throw new RuntimeError(expr.method, "Undefined property '" ~ expr.method.lexeme ~ "'."); + return method.bind(object); + } LoxValue visit(Expr.This expr){ return lookUpVariable(expr.keyword, expr); } diff --git a/src/jlox/loxclass.d b/src/jlox/loxclass.d index da8a202..44be38a 100644 --- a/src/jlox/loxclass.d +++ b/src/jlox/loxclass.d @@ -8,12 +8,15 @@ import common.util; class LoxClass : LoxCallable{ package const string name; + private const LoxClass superclass; private const LoxFunction[string] methods; mixin defaultCtor; - LoxFunction findMethod(string name){ + LoxFunction findMethod(string name) const{ if(auto method = name in methods) return cast(LoxFunction)*method; + if(superclass !is null) + return superclass.findMethod(name); return null; } diff --git a/src/jlox/parser.d b/src/jlox/parser.d index eea1900..a314992 100644 --- a/src/jlox/parser.d +++ b/src/jlox/parser.d @@ -312,6 +312,12 @@ class Parser{ consume(TokenType.RIGHT_PAREN, "Expect ')' after expression."); return new Expr.Grouping(expr); } + if(match(TokenType.SUPER)){ + Token keyword = previous(); + consume(TokenType.DOT, "Expect '.' after 'super'."); + Token method = consume(TokenType.IDENTIFIER, "Expect superclass method name."); + return new Expr.Super(keyword, method); + } throw error(peek(), "Expect expression."); } private Stmt varDeclaration(){ @@ -324,12 +330,17 @@ class Parser{ } private Stmt classDeclaration() { Token name = consume(TokenType.IDENTIFIER, "Expect class name."); + Expr.Variable superclass; + if(match(TokenType.LESS)){ + consume(TokenType.IDENTIFIER, "Expect superlcass name."); + superclass = new Expr.Variable(previous); + } 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); + return new Stmt.Class(name, superclass, methods); } private Stmt declaration(){ try { diff --git a/src/jlox/resolver.d b/src/jlox/resolver.d index b5cc879..c9dbf81 100644 --- a/src/jlox/resolver.d +++ b/src/jlox/resolver.d @@ -14,7 +14,7 @@ import common.util; class Resolver : Stmt.Visitor!void, Expr.Visitor!void { private enum FunctionType{ NONE, FUNCTION, INITIALISER, METHOD } - private enum ClassType{ NONE, CLASS } + private enum ClassType{ NONE, CLASS, SUBCLASS } private Interpreter interpreter; this(Interpreter interpreter){ this.interpreter = interpreter; @@ -80,6 +80,16 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { currentClass = enclosingClass; declare(stmt.name); define(stmt.name); + if(stmt.superclass !is null){ + currentClass = ClassType.SUBCLASS; + if(stmt.name.lexeme == stmt.superclass.name.lexeme) + Lox.error(stmt.superclass.name, "A class can't inherit from itself."); + resolve(stmt.superclass); + } + if(stmt.superclass !is null){ + beginScope(); + scopes.front["super"] = true; + } beginScope(); scopes.front["this"] = true; foreach(method; stmt.methods){ @@ -89,6 +99,8 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { resolveFunction(method, declaration); } endScope(); + if(stmt.superclass !is null) + endScope(); } void visit(Stmt.Var stmt){ declare(stmt.name); @@ -165,6 +177,13 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void { resolve(expr.value); resolve(expr.object); } + void visit(Expr.Super expr){ + if(currentClass == ClassType.NONE) + Lox.error(expr.keyword, "Can't use 'super' outside of a class."); + else if (currentClass != ClassType.SUBCLASS) + Lox.error(expr.keyword, "Can't use 'super' in a class with no superclass."); + resolveLocal(expr, expr.keyword); + } void visit(Expr.This expr){ if(currentClass == ClassType.NONE){ Lox.error(expr.keyword, "Can't use 'this' outside of a class."); diff --git a/src/jlox/stmt.d b/src/jlox/stmt.d index 3d42392..64d35ae 100644 --- a/src/jlox/stmt.d +++ b/src/jlox/stmt.d @@ -36,6 +36,7 @@ abstract class Stmt{ } static class Class : typeof(this){ Token name; + Expr.Variable superclass; Function[] methods; mixin defCtorAndAccept; } diff --git a/test/all.d b/test/all.d index e891ff1..a7c3542 100755 --- a/test/all.d +++ b/test/all.d @@ -16,12 +16,15 @@ void main(){ "./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"); + "./test/super.lox".match("Fry until golden brown.\nPipe full of custard and coat with chocolate.\nA method\n"); + "./test/err/invalid_syntax.lox".shouldFail(RetVal.other); "./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"); + "./test/err/super_outside_class.lox".shouldFail(RetVal.other, "Can't use 'super' outside of a class"); + "./test/err/super_without_superclass.lox".shouldFail(RetVal.other, "Can't use 'super' in a class with no superclass"); } enum RetVal{ diff --git a/test/err/super_outside_class.lox b/test/err/super_outside_class.lox new file mode 100644 index 0000000..4a14239 --- /dev/null +++ b/test/err/super_outside_class.lox @@ -0,0 +1,3 @@ + +super.notEvenInAClass(); + diff --git a/test/err/super_without_superclass.lox b/test/err/super_without_superclass.lox new file mode 100644 index 0000000..d6b4cdc --- /dev/null +++ b/test/err/super_without_superclass.lox @@ -0,0 +1,8 @@ + +class Eclair{ + cook(){ + super.cook(); + print "Pipe full of crème pâtissière."; + } +} + diff --git a/test/super.lox b/test/super.lox new file mode 100644 index 0000000..dec5f8d --- /dev/null +++ b/test/super.lox @@ -0,0 +1,36 @@ + +class Doughnut{ + cook(){ + print "Fry until golden brown."; + } +} + +class BostonCream < Doughnut{ + cook(){ + super.cook(); + print "Pipe full of custard and coat with chocolate."; + } +} + +BostonCream().cook(); + + +class A{ + method(){ + print "A method"; + } +} + +class B < A{ + method(){ + print "B method"; + } + test(){ + super.method(); + } +} + +class C < B{} + +C().test(); + diff --git a/test/test.lox b/test/test.lox deleted file mode 100644 index 139597f..0000000 --- a/test/test.lox +++ /dev/null @@ -1,2 +0,0 @@ - - From 7fa01b4fb9c2c93d893620dd5cb0d2da46de4256 Mon Sep 17 00:00:00 2001 From: nazrin Date: Tue, 3 Jun 2025 01:54:47 +0000 Subject: [PATCH 06/10] Chunks of Bytecode 14 --- dub.sdl | 4 ++++ src/clox/chunk.d | 26 ++++++++++++++++++++++++++ src/clox/dbg.d | 42 ++++++++++++++++++++++++++++++++++++++++++ src/clox/main.d | 20 ++++++++++++++++++++ src/clox/value.d | 10 ++++++++++ 5 files changed, 102 insertions(+) create mode 100644 src/clox/chunk.d create mode 100644 src/clox/dbg.d create mode 100644 src/clox/main.d create mode 100644 src/clox/value.d diff --git a/dub.sdl b/dub.sdl index 8920a38..8248879 100644 --- a/dub.sdl +++ b/dub.sdl @@ -9,6 +9,10 @@ buildRequirements "requireBoundsCheck" "requireContracts" versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" "LoxPrintMultiple" +sourcePaths +configuration "clox" { + sourcePaths "src/clox" "src/common" +} configuration "jlox" { sourcePaths "src/jlox" "src/common" } diff --git a/src/clox/chunk.d b/src/clox/chunk.d new file mode 100644 index 0000000..d6199eb --- /dev/null +++ b/src/clox/chunk.d @@ -0,0 +1,26 @@ +module clox.chunk; + +import std.container.array; +import std.stdio; + +import clox.value; + +enum OpCode : ubyte{ + OP_CONSTANT, + OP_RETURN, +} + +struct Chunk{ + Array!ubyte code; + Array!uint lines; + Array!Value constants; + ubyte addConstant(in Value value) @nogc nothrow { + constants ~= value; + return cast(ubyte)((constants.length) - 1); + } + void write(ubyte b, uint line = 0) @nogc nothrow { + code ~= b; + lines ~= line; + } +} + diff --git a/src/clox/dbg.d b/src/clox/dbg.d new file mode 100644 index 0000000..9895399 --- /dev/null +++ b/src/clox/dbg.d @@ -0,0 +1,42 @@ +module clox.dbg; + +import std.stdio; + +import clox.chunk; +import clox.value; + +private ulong simpleInstruction(string name, ulong offset){ + writeln(name); + return offset + 1; +} +private ulong constantInstruction(string name, in ref Chunk chunk, ulong offset){ + ubyte constant = chunk.code[offset + 1]; + writef("%-16s %4d '", name, constant); + printValue(chunk.constants[constant]); + writeln("'"); + return offset + 2; +} + +void disassembleChunk(in ref Chunk chunk, string name = "chunk"){ + writefln("== %s ==", name); + for(ulong offset = 0; offset < chunk.code.length;) + offset = disassembleInstruction(chunk, offset); +} +ulong disassembleInstruction(in ref Chunk chunk, const ulong offset){ + writef(" %04d ", offset); + if(offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1]) + write(" | "); + else + writef(" %4d ", chunk.lines[offset]); + ubyte instruction = chunk.code[offset]; + with(OpCode) switch(instruction){ + case OP_CONSTANT: + return constantInstruction("OP_CONSTANT", chunk, offset); + case OP_RETURN: + return simpleInstruction("OP_RETURN", offset); + default: + writefln("Unknown opcode %d", instruction); + return offset + 1; + } +} + diff --git a/src/clox/main.d b/src/clox/main.d new file mode 100644 index 0000000..cc07501 --- /dev/null +++ b/src/clox/main.d @@ -0,0 +1,20 @@ +module clox.main; + +import std.stdio; + +import clox.chunk; +import clox.dbg; + +int main(string[] argv){ + Chunk chunk; + + ubyte constant = chunk.addConstant(1.2); + chunk.write(OpCode.OP_CONSTANT); + chunk.write(constant); + + chunk.write(OpCode.OP_RETURN); + + disassembleChunk(chunk); + return 0; +} + diff --git a/src/clox/value.d b/src/clox/value.d new file mode 100644 index 0000000..c9b298c --- /dev/null +++ b/src/clox/value.d @@ -0,0 +1,10 @@ +module clox.value; + +import std.stdio; + +alias Value = double; + +void printValue(Value value){ + writef("%g", value); +} + From aba643a88e80fcc4df135cc223371d27f2808b29 Mon Sep 17 00:00:00 2001 From: nazrin Date: Tue, 3 Jun 2025 19:04:25 +0000 Subject: [PATCH 07/10] A Virtual Machine 15 --- dub.sdl | 7 +++--- src/clox/chunk.d | 6 +++-- src/clox/dbg.d | 42 +++++++++++++++++++--------------- src/clox/main.d | 24 +++++++++++++++++--- src/clox/util.d | 25 ++++++++++++++++++++ src/clox/value.d | 4 ++-- src/clox/vm.d | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 139 insertions(+), 28 deletions(-) create mode 100644 src/clox/util.d create mode 100644 src/clox/vm.d diff --git a/dub.sdl b/dub.sdl index 8248879..9d1f085 100644 --- a/dub.sdl +++ b/dub.sdl @@ -5,15 +5,16 @@ copyright "Copyright © 2025, tanya" license "MPL-2.0" dependency "commandr" version="~>1.1.0" targetType "executable" -buildRequirements "requireBoundsCheck" "requireContracts" - -versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" "LoxPrintMultiple" sourcePaths configuration "clox" { + debugVersions "traceExec" + buildRequirements "requireBoundsCheck" "requireContracts" sourcePaths "src/clox" "src/common" } configuration "jlox" { + versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" "LoxPrintMultiple" + buildRequirements "requireBoundsCheck" "requireContracts" sourcePaths "src/jlox" "src/common" } diff --git a/src/clox/chunk.d b/src/clox/chunk.d index d6199eb..20ce7ef 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -6,8 +6,10 @@ import std.stdio; import clox.value; enum OpCode : ubyte{ - OP_CONSTANT, - OP_RETURN, + Constant, + Add, Subtract, Multiply, Divide, + Negate, + Return, } struct Chunk{ diff --git a/src/clox/dbg.d b/src/clox/dbg.d index 9895399..fe63856 100644 --- a/src/clox/dbg.d +++ b/src/clox/dbg.d @@ -1,41 +1,47 @@ module clox.dbg; import std.stdio; +import std.conv; +import std.uni; import clox.chunk; import clox.value; -private ulong simpleInstruction(string name, ulong offset){ - writeln(name); +debug private ulong simpleInstruction(string name, ulong offset) @nogc nothrow{ + debug writeln(name); return offset + 1; } -private ulong constantInstruction(string name, in ref Chunk chunk, ulong offset){ +debug private ulong constantInstruction(string name, Chunk* chunk, ulong offset) @nogc nothrow{ ubyte constant = chunk.code[offset + 1]; - writef("%-16s %4d '", name, constant); - printValue(chunk.constants[constant]); - writeln("'"); + debug writef("%-16s %4d '", name, constant); + debug printValue(chunk.constants[constant]); + debug writeln("'"); return offset + 2; } -void disassembleChunk(in ref Chunk chunk, string name = "chunk"){ - writefln("== %s ==", name); +debug void disassembleChunk(Chunk* chunk, string name = "chunk") @nogc nothrow{ + debug writefln("== %s ==", name); for(ulong offset = 0; offset < chunk.code.length;) offset = disassembleInstruction(chunk, offset); } -ulong disassembleInstruction(in ref Chunk chunk, const ulong offset){ - writef(" %04d ", offset); - if(offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1]) - write(" | "); - else - writef(" %4d ", chunk.lines[offset]); +debug ulong disassembleInstruction(Chunk* chunk, const ulong offset) @nogc nothrow{ + debug writef(" %04d ", offset); + if(offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1]){ + debug write(" | "); + } else { + debug writef(" %4d ", chunk.lines[offset]); + } ubyte instruction = chunk.code[offset]; with(OpCode) switch(instruction){ - case OP_CONSTANT: + case Constant: return constantInstruction("OP_CONSTANT", chunk, offset); - case OP_RETURN: - return simpleInstruction("OP_RETURN", offset); + static foreach(k; [ Return, Negate, Add, Subtract, Multiply, Divide ]){ + case k: + static name = "OP_" ~ (k.to!string).toUpper; + return simpleInstruction(name, offset); + } default: - writefln("Unknown opcode %d", instruction); + debug writefln("Unknown opcode %d", instruction); return offset + 1; } } diff --git a/src/clox/main.d b/src/clox/main.d index cc07501..f5dd3be 100644 --- a/src/clox/main.d +++ b/src/clox/main.d @@ -4,17 +4,35 @@ import std.stdio; import clox.chunk; import clox.dbg; +import clox.vm; int main(string[] argv){ Chunk chunk; ubyte constant = chunk.addConstant(1.2); - chunk.write(OpCode.OP_CONSTANT); + chunk.write(OpCode.Constant); chunk.write(constant); - chunk.write(OpCode.OP_RETURN); + constant = chunk.addConstant(3.4); + chunk.write(OpCode.Constant); + chunk.write(constant); + + chunk.write(OpCode.Add); + + constant = chunk.addConstant(5.6); + chunk.write(OpCode.Constant); + chunk.write(constant); + + chunk.write(OpCode.Divide); + + chunk.write(OpCode.Negate); + + chunk.write(OpCode.Return); + + VM vm = VM(0); + vm.interpret(&chunk); + - disassembleChunk(chunk); return 0; } diff --git a/src/clox/util.d b/src/clox/util.d new file mode 100644 index 0000000..081e6b9 --- /dev/null +++ b/src/clox/util.d @@ -0,0 +1,25 @@ +module clox.util; + +struct Stack(T, size_t N){ + T* top; + T[N] data; + invariant{ assert(top <= data.ptr + N); assert(top >= data.ptr); } + this(int _) @nogc nothrow{ + top = data.ptr; + } + void push(T value) @nogc nothrow{ + assert(top < data.ptr + N); + debug assert(*top is T.init); + *(top++) = value; + } + T pop() @nogc nothrow{ + assert(top > data.ptr); + T t = *(--top); + debug *(top) = T.init; + return t; + } + const(T)[] live() @nogc nothrow const{ + return data[0 .. (top - data.ptr)]; + } +} + diff --git a/src/clox/value.d b/src/clox/value.d index c9b298c..eb74daa 100644 --- a/src/clox/value.d +++ b/src/clox/value.d @@ -4,7 +4,7 @@ import std.stdio; alias Value = double; -void printValue(Value value){ - writef("%g", value); +debug void printValue(Value value) @nogc nothrow{ + debug writef("%g", value); } diff --git a/src/clox/vm.d b/src/clox/vm.d new file mode 100644 index 0000000..b9c9cfe --- /dev/null +++ b/src/clox/vm.d @@ -0,0 +1,59 @@ +module clox.vm; + +import std.stdio; + +import clox.chunk; +import clox.value; +import clox.dbg; +import clox.util; + +enum stackMax = 256; + +struct VM{ + const(ubyte)* ip; + Stack!(Value, stackMax) stack; + Chunk* chunk; + enum InterpretResult{ Ok, CompileError, RunetimeError } + this(int _) @nogc nothrow { + stack = typeof(stack)(0); + } + InterpretResult interpret(Chunk* chunk) @nogc nothrow { + this.chunk = chunk; + ip = &chunk.code[0]; + return run(); + } + private InterpretResult run() @nogc nothrow { + auto readByte() => *ip++; + auto readIns() => cast(OpCode)readByte(); + auto readConstant() => chunk.constants[readByte()]; + while(true){ + debug(traceExec){ + writeln(" ", stack.live); + debug disassembleInstruction(chunk, ip - &chunk.code[0]); + } + OpCode instruction = readIns(); + with(OpCode) opSwitch: final switch(instruction){ + case Constant: + Value constant = readConstant(); + stack.push(constant); + break; + static foreach(k, op; [ Add: "+", Subtract: "-", Multiply: "*", Divide: "/" ]){ + case k: + Value b = stack.pop(); + Value a = stack.pop(); + stack.push(mixin("a", op, "b")); + break opSwitch; + } + case Negate: + stack.push(-stack.pop()); + break; + case Return: + debug printValue(stack.pop()); + debug writeln(); + return InterpretResult.Ok; + } + } + assert(0); + } +} + From 8fb449825de5707ae3bb21eab9aed24115f9863e Mon Sep 17 00:00:00 2001 From: nazrin Date: Tue, 3 Jun 2025 22:57:02 +0000 Subject: [PATCH 08/10] Scanning on Demand 16 --- src/clox/compiler.d | 9 +++ src/clox/main.d | 62 ++++++++++-------- src/clox/scanner.d | 153 ++++++++++++++++++++++++++++++++++++++++++++ src/clox/vm.d | 7 +- src/common/util.d | 4 ++ src/jlox/scanner.d | 6 +- 6 files changed, 209 insertions(+), 32 deletions(-) create mode 100644 src/clox/compiler.d create mode 100644 src/clox/scanner.d diff --git a/src/clox/compiler.d b/src/clox/compiler.d new file mode 100644 index 0000000..c1df16b --- /dev/null +++ b/src/clox/compiler.d @@ -0,0 +1,9 @@ +module clox.compiler; + +import std.stdio; + +import clox.scanner; + +void compile(string source) @nogc nothrow { +} + diff --git a/src/clox/main.d b/src/clox/main.d index f5dd3be..44ec26d 100644 --- a/src/clox/main.d +++ b/src/clox/main.d @@ -1,38 +1,46 @@ module clox.main; import std.stdio; +import std.file; import clox.chunk; import clox.dbg; import clox.vm; -int main(string[] argv){ - Chunk chunk; +extern(C) int isatty(int); - ubyte constant = chunk.addConstant(1.2); - chunk.write(OpCode.Constant); - chunk.write(constant); - - constant = chunk.addConstant(3.4); - chunk.write(OpCode.Constant); - chunk.write(constant); - - chunk.write(OpCode.Add); - - constant = chunk.addConstant(5.6); - chunk.write(OpCode.Constant); - chunk.write(constant); - - chunk.write(OpCode.Divide); - - chunk.write(OpCode.Negate); - - chunk.write(OpCode.Return); - - VM vm = VM(0); - vm.interpret(&chunk); - - - return 0; +struct Lox{ + VM vm; + this(int _){ + vm = VM(0); + } + int runFile(string path){ + string source = path.readText(); + VM.InterpretResult result = vm.interpret(source); + final switch(result){ + case VM.InterpretResult.CompileError: return 65; + case VM.InterpretResult.RuntimeError: return 70; + case VM.InterpretResult.Ok: return 0; + } + } + int runPrompt(){ + while(true){ + write("> "); + string line = stdin.readln(); + if(!line){ + writeln(); + return 0; + } + vm.interpret(line); + } + } +} + +int main(string[] argv){ + Lox lox = Lox(0); + if(isatty(stdin.fileno)) + return lox.runPrompt(); + else + return lox.runFile("/dev/stdin"); } diff --git a/src/clox/scanner.d b/src/clox/scanner.d new file mode 100644 index 0000000..1d95000 --- /dev/null +++ b/src/clox/scanner.d @@ -0,0 +1,153 @@ +module clox.scanner; + +import std.stdio; +import std.ascii; + +import common.util; + +struct Token{ + enum Type : ubyte { + LeftParen, RightParen, // Single-character tokens. + LeftBrace, RightBrace, + Comma, Dot, Minus, Plus, + Semicolon, Slash, Star, + Bang, BangEqual, // One or two character tokens. + Equal, EqualEqual, + Greater, GreaterEqual, + Less, LessEqual, + Identifier, String, Number, // Literals. + And, Class, Else, False, // Keywords. + For, Fun, If, Nil, Or, + Print, Return, Super, This, + True, Var, While, + Error, EOF // Special + } + Type type; + int line; + string lexeme; + static Token error(string msg) => Token(Token.Type.Error, 0, msg); +} + +struct Scanner{ + string start; + string current; + int line = 1; + this(string source){ + start = current = source; + } + bool isAtEnd() const => current.length == 0; + private char peek() const => current[0]; + private char peekNext() const => current.length >= 2 ? current[1] : '\0'; + private Token makeToken(Token.Type type) const{ + Token token; + token.type = type; + token.lexeme = start[0 .. current.ptr - start.ptr]; + return token; + } + private char advance(){ + char c = current[0]; + current = current[1 .. $]; + return c; + } + private bool match(char needle){ + if(isAtEnd || current[0] != needle) + return false; + current = current[1 .. $]; + return true; + } + private void skipWhitespace(){ + while(!isAtEnd){ + char c = peek(); + if(!c) + return; + if(c == '/' && peekNext() == '/'){ + while(!isAtEnd && peek() != '\n') + advance(); + continue; + } + if(!c.isWhite) + return; + if(c == '\n') + line++; + current = current[1 .. $]; + } + } + private Token parseString(){ + while(peek() != '"' && !isAtEnd){ + if(peek() == '\n') + line++; + advance(); + } + if(isAtEnd) + return Token.error("Unterminated string."); + advance(); + return makeToken(Token.Type.String); + } + private Token parseNumber(){ + while(peek().isDigit) + advance(); + if(peek() == '.' && peekNext().isDigit){ + advance(); + while(peek().isDigit) + advance(); + } + return makeToken(Token.Type.Number); + } + private Token parseIdentifier(){ + while(peek().isAlphaNum_) + advance(); + Token token = makeToken(Token.Type.Identifier); + switch(token.lexeme){ + case "and": token.type = Token.Type.And; break; + case "class": token.type = Token.Type.Class; break; + case "else": token.type = Token.Type.Else; break; + case "if": token.type = Token.Type.If; break; + case "nil": token.type = Token.Type.Nil; break; + case "or": token.type = Token.Type.Or; break; + case "print": token.type = Token.Type.Print; break; + case "return": token.type = Token.Type.Return; break; + case "super": token.type = Token.Type.Super; break; + case "var": token.type = Token.Type.Var; break; + case "while": token.type = Token.Type.While; break; + case "false": token.type = Token.Type.False; break; + case "for": token.type = Token.Type.For; break; + case "fun": token.type = Token.Type.Fun; break; + case "this": token.type = Token.Type.This; break; + case "true": token.type = Token.Type.True; break; + default: break; + } + return token; + } + Token scan(){ + skipWhitespace(); + start = current; + if(isAtEnd) + return Token(Token.Type.EOF); + char c = advance(); + if(c.isAlpha_) + return parseIdentifier(); + switch(c){ + case '(': return makeToken(Token.Type.LeftParen); + case ')': return makeToken(Token.Type.RightParen); + case '{': return makeToken(Token.Type.LeftBrace); + case '}': return makeToken(Token.Type.RightBrace); + case ';': return makeToken(Token.Type.Semicolon); + case ',': return makeToken(Token.Type.Comma); + case '.': return makeToken(Token.Type.Dot); + case '-': return makeToken(Token.Type.Minus); + case '+': return makeToken(Token.Type.Plus); + case '/': return makeToken(Token.Type.Slash); + case '*': return makeToken(Token.Type.Star); + case '!': return makeToken(match('=') ? Token.Type.BangEqual : Token.Type.Bang); + case '=': return makeToken(match('=') ? Token.Type.EqualEqual : Token.Type.Equal); + case '<': return makeToken(match('=') ? Token.Type.LessEqual : Token.Type.Less); + case '>': return makeToken(match('=') ? Token.Type.GreaterEqual : Token.Type.Greater); + case '"': return parseString(); + default: break; + } + if(c.isDigit) + return parseNumber(); + return Token.error("Unexpected character '" ~ c ~ "'."); + } +} + diff --git a/src/clox/vm.d b/src/clox/vm.d index b9c9cfe..151ec6d 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -6,6 +6,7 @@ import clox.chunk; import clox.value; import clox.dbg; import clox.util; +import clox.compiler; enum stackMax = 256; @@ -13,10 +14,14 @@ struct VM{ const(ubyte)* ip; Stack!(Value, stackMax) stack; Chunk* chunk; - enum InterpretResult{ Ok, CompileError, RunetimeError } + enum InterpretResult{ Ok, CompileError, RuntimeError } this(int _) @nogc nothrow { stack = typeof(stack)(0); } + InterpretResult interpret(string source) @nogc nothrow { + compile(source); + return InterpretResult.Ok; + } InterpretResult interpret(Chunk* chunk) @nogc nothrow { this.chunk = chunk; ip = &chunk.code[0]; diff --git a/src/common/util.d b/src/common/util.d index f8dc60d..0efa053 100644 --- a/src/common/util.d +++ b/src/common/util.d @@ -7,3 +7,7 @@ template defaultCtor(){ } } +import std.ascii : isAlpha, isAlphaNum; +bool isAlpha_(dchar c) => c.isAlpha || c == '_'; +bool isAlphaNum_(dchar c) => c.isAlphaNum || c == '_'; + diff --git a/src/jlox/scanner.d b/src/jlox/scanner.d index 8e78a08..8da932f 100644 --- a/src/jlox/scanner.d +++ b/src/jlox/scanner.d @@ -6,11 +6,9 @@ import std.conv; import jlox.token; import jlox.tokentype; import jlox.main; +import common.util; -private bool isAlpha_(dchar c) => c.isAlpha || c == '_'; -private bool isAlphaNum_(dchar c) => c.isAlphaNum || c == '_'; - -class Scanner { +class Scanner{ private string source; private Token[] tokens; private uint start = 0; From 41404633da957bf63e4fe674e1b5d24f351dd491 Mon Sep 17 00:00:00 2001 From: nazrin Date: Wed, 4 Jun 2025 19:49:53 +0000 Subject: [PATCH 09/10] Compiling Expressions 17 --- dub.sdl | 12 ++-- dub.selections.json | 4 +- src/clox/chunk.d | 19 +++++-- src/clox/compiler.d | 25 ++++++++- src/clox/container/int24.d | 26 +++++++++ src/clox/container/rle.d | 62 +++++++++++++++++++++ src/clox/container/stack.d | 26 +++++++++ src/clox/container/varint.d | 73 ++++++++++++++++++++++++ src/clox/dbg.d | 38 ++++++++----- src/clox/emitter.d | 51 +++++++++++++++++ src/clox/main.d | 2 +- src/clox/parser.d | 66 ++++++++++++++++++++++ src/clox/parserules.d | 108 ++++++++++++++++++++++++++++++++++++ src/clox/scanner.d | 12 ++-- src/clox/util.d | 25 +-------- src/clox/value.d | 4 +- src/clox/vm.d | 22 ++++++-- src/common/util.d | 35 +++++++++++- 18 files changed, 546 insertions(+), 64 deletions(-) create mode 100644 src/clox/container/int24.d create mode 100644 src/clox/container/rle.d create mode 100644 src/clox/container/stack.d create mode 100644 src/clox/container/varint.d create mode 100644 src/clox/emitter.d create mode 100644 src/clox/parser.d create mode 100644 src/clox/parserules.d diff --git a/dub.sdl b/dub.sdl index 9d1f085..90eb9b4 100644 --- a/dub.sdl +++ b/dub.sdl @@ -4,17 +4,19 @@ authors "tanya" copyright "Copyright © 2025, tanya" license "MPL-2.0" dependency "commandr" version="~>1.1.0" +dependency "colored" version="~>0.0.33" targetType "executable" - sourcePaths configuration "clox" { - debugVersions "traceExec" - buildRequirements "requireBoundsCheck" "requireContracts" + /* debugVersions "traceExec" */ + debugVersions "printCode" + targetType "executable" sourcePaths "src/clox" "src/common" + buildRequirements "requireBoundsCheck" "requireContracts" } configuration "jlox" { + targetType "executable" + sourcePaths "src/jlox" "src/common" versions "LoxConcatNonStrings" "LoxExtraNativeFuncs" "LoxPrintMultiple" buildRequirements "requireBoundsCheck" "requireContracts" - sourcePaths "src/jlox" "src/common" } - diff --git a/dub.selections.json b/dub.selections.json index 6b90314..ead0009 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -1,7 +1,9 @@ { "fileVersion": 1, "versions": { + "colored": "0.0.33", "commandr": "1.1.0", - "taggedalgebraic": "0.11.23" + "taggedalgebraic": "0.11.23", + "unit-threaded": "2.2.3" } } diff --git a/src/clox/chunk.d b/src/clox/chunk.d index 20ce7ef..833e8b1 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -2,8 +2,11 @@ module clox.chunk; import std.container.array; import std.stdio; +import std.algorithm.searching; import clox.value; +import clox.container.rle; +import clox.container.int24; enum OpCode : ubyte{ Constant, @@ -14,15 +17,23 @@ enum OpCode : ubyte{ struct Chunk{ Array!ubyte code; - Array!uint lines; + Rle!(Uint24, ubyte) lines; Array!Value constants; - ubyte addConstant(in Value value) @nogc nothrow { + uint addConstant(in Value value) @nogc nothrow { + long index = constants[].countUntil(value); + if(index >= 0) + return cast(uint)index; constants ~= value; - return cast(ubyte)((constants.length) - 1); + return cast(uint)((constants.length) - 1); } void write(ubyte b, uint line = 0) @nogc nothrow { + ubyte[1] data = [ b ]; + write(data, line); + } + void write(ubyte[] b, uint line = 0) @nogc nothrow { code ~= b; - lines ~= line; + foreach(i; 0 .. b.length) + lines ~= Uint24(line); // TODO could be done without a loop } } diff --git a/src/clox/compiler.d b/src/clox/compiler.d index c1df16b..1107ca5 100644 --- a/src/clox/compiler.d +++ b/src/clox/compiler.d @@ -2,8 +2,29 @@ module clox.compiler; import std.stdio; -import clox.scanner; +import clox.scanner, clox.parser, clox.emitter; +import clox.chunk; +import clox.value; +import clox.util; +import clox.parserules; +import clox.dbg; -void compile(string source) @nogc nothrow { +struct Compiler{ + Scanner scanner; + Parser parser; + Emitter emitter; + bool compile(string source, Chunk* chunk){ + scanner = Scanner(source); + parser = Parser(&this); + emitter = Emitter(&this, chunk); + + parser.advance(); + parser.expression(); + parser.consume(Token.Type.EOF, "Expect end of expression."); + debug writeln(*chunk); + emitter.endCompiler(); + return !parser.hadError; + } } + diff --git a/src/clox/container/int24.d b/src/clox/container/int24.d new file mode 100644 index 0000000..406a933 --- /dev/null +++ b/src/clox/container/int24.d @@ -0,0 +1,26 @@ +module clox.container.int24; + +struct Uint24{ + nothrow: @nogc: @safe: + ubyte[3] data; + static Uint24 opCall(uint n){ + import std.bitmanip : nativeToLittleEndian; + Uint24 u3; + assert(n <= 16_777_215); + ubyte[uint.sizeof] d = nativeToLittleEndian!uint(n); + u3.data[0 .. 3] = d[0 .. 3]; + return u3; + } +} +uint toUint(Uint24 u3) @nogc nothrow @safe { + import std.bitmanip : littleEndianToNative; + ubyte[4] temp; + temp[0 .. 3] = u3.data; + return littleEndianToNative!uint(temp); +} +unittest{ + static assert(Uint24.sizeof == 3); + assert(Uint24(5).toUint == 5); + assert(Uint24(16_777_215).toUint == 16_777_215); +} + diff --git a/src/clox/container/rle.d b/src/clox/container/rle.d new file mode 100644 index 0000000..f6e117a --- /dev/null +++ b/src/clox/container/rle.d @@ -0,0 +1,62 @@ +module clox.container.rle; + +import std.traits; +import std.container.array; + +struct Rle(T, L = ubyte) if(isUnsigned!L){ + nothrow: @nogc: + align(1) struct Count{ + T item; + L num; + } + size_t total; + Array!Count data; + private void pushNew(T item){ + data ~= Count(item, 0); + } + size_t push(T item){ + if(data.length){ + Count* d = &data[(data.length)-1]; + if(d.item == item && d.num < L.max){ + d.num++; + return total++; + } + } + pushNew(item); + return total++; + } + T opOpAssign(string op: "~")(T rhs){ + push(rhs); + return rhs; + } + T opIndex(size_t n) const @safe{ + assert(n < total); + size_t c; + for(size_t i; i < n;){ + i += data[c].num + 1; + if(i <= n) + c++; + } + return data[c].item; + } +} +unittest{ + import clox.container.int24; + auto rl = Rle!(Uint24, ubyte)(); + static assert(rl.Count.sizeof == 4); + foreach(i; 0..300){ + size_t index = rl.push(Uint24(5)); + assert(rl[index].toUint == 5); + } + assert(rl[299].toUint == 5); + foreach(i; 0..30){ + size_t index = rl.push(Uint24(0)); + assert(rl[index].toUint == 0); + } + assert(rl[0].toUint == 5); + foreach(i; 0..300){ + size_t index = rl.push(Uint24(16_777_215)); + assert(rl[index].toUint == 16_777_215); + } +} + diff --git a/src/clox/container/stack.d b/src/clox/container/stack.d new file mode 100644 index 0000000..9dabed1 --- /dev/null +++ b/src/clox/container/stack.d @@ -0,0 +1,26 @@ +module clox.container.stack; + +struct Stack(T, size_t N){ + @nogc: nothrow: + T* top; + T[N] data; + invariant{ assert(top <= data.ptr + N); assert(top >= data.ptr); } + this(int _) @safe{ + top = data.ptr; + } + void push(T value){ + assert(top < data.ptr + N); + debug assert(*top is T.init); + *(top++) = value; + } + T pop(){ + assert(top > data.ptr); + T t = *(--top); + debug *(top) = T.init; + return t; + } + const(T)[] live() const @safe{ + return data[0 .. (top - data.ptr)]; + } +} + diff --git a/src/clox/container/varint.d b/src/clox/container/varint.d new file mode 100644 index 0000000..d1d467b --- /dev/null +++ b/src/clox/container/varint.d @@ -0,0 +1,73 @@ +module clox.container.varint; + +struct VarUint{ + import std.bitmanip; + nothrow: @nogc: + uint i; + ubyte len; + ubyte[4] data; + this(long l) @safe { + if(l < 0b1000_0000){ + len = 1; + data[0] = (cast(ubyte)l); + return; + } + if(l < 0b0100_0000__0000_0000){ + len = 2; + data[0 .. 2] = nativeToBigEndian(cast(ushort)l); + data[0] |= 0b1000_0000; + return; + } + if(l < 0b0010_0000__0000_0000__0000_0000__0000_0000){ + len = 4; + data[0 .. 4] = nativeToBigEndian(cast(uint)l); + data[0] |= 0b1100_0000; + return; + } + assert(0); + } + static VarUint read(const(ubyte)[] data) @safe { + VarUint v; + ubyte a = data[0]; + if((data[0] & 0b1000_0000) == 0){ + v.i = a; + v.len = 1; + return v; + } + if((a & 0b0100_0000) == 0){ + ubyte[2] d = data[0 .. 2]; + d[0] &= 0b0111_1111; + v.i = bigEndianToNative!ushort(d); + v.len = 2; + return v; + } + if((a & 0b0010_0000) == 0){ + ubyte[4] d = data[0 .. 4]; + d[0] &= 0b0011_1111; + v.i = bigEndianToNative!uint(d); + v.len = 4; + return v; + } + assert(0); + } + ubyte[] bytes() @nogc nothrow { + return data[0 .. len]; + } +} +unittest{ + import std.range; + assert(VarUint(5).bytes.length == 1); + assert(VarUint(127).bytes.length == 1); + assert(VarUint(128).bytes.length == 2); + assert(VarUint(536_870_911).bytes.length == 4); + foreach(ulong i; [ + 0, 1, 2, 5, + 150, 127, 128, + 536_870_911, + ushort.max * 100 + ]){ + auto vi = VarUint(i); + assert(i == VarUint.read(vi.bytes).i); + } +} + diff --git a/src/clox/dbg.d b/src/clox/dbg.d index fe63856..9d88850 100644 --- a/src/clox/dbg.d +++ b/src/clox/dbg.d @@ -3,33 +3,41 @@ module clox.dbg; import std.stdio; import std.conv; import std.uni; +import std.format; + +import colored; import clox.chunk; import clox.value; +import clox.util; +import clox.container.varint; +import clox.container.int24; -debug private ulong simpleInstruction(string name, ulong offset) @nogc nothrow{ - debug writeln(name); +private ulong simpleInstruction(string name, ulong offset){ + writeln(name.lightCyan); return offset + 1; } -debug private ulong constantInstruction(string name, Chunk* chunk, ulong offset) @nogc nothrow{ - ubyte constant = chunk.code[offset + 1]; - debug writef("%-16s %4d '", name, constant); - debug printValue(chunk.constants[constant]); - debug writeln("'"); - return offset + 2; +private ulong constantInstruction(string name, Chunk* chunk, ulong offset){ + /* ubyte constant = chunk.code[offset + 1]; */ + VarUint constant = VarUint.read(chunk.code.data[offset + 1 .. $]); + /* writeln(constant); */ + write("%-16s".format(name).cyan, " %4d ".format(constant.i).lightMagenta, "'"); + printValue(chunk.constants[constant.i]); + writeln("'"); + return offset + 1 + constant.len; } -debug void disassembleChunk(Chunk* chunk, string name = "chunk") @nogc nothrow{ - debug writefln("== %s ==", name); +void disassembleChunk(Chunk* chunk, string name = "chunk"){ + writefln("== %s ==", name); for(ulong offset = 0; offset < chunk.code.length;) offset = disassembleInstruction(chunk, offset); } -debug ulong disassembleInstruction(Chunk* chunk, const ulong offset) @nogc nothrow{ - debug writef(" %04d ", offset); +ulong disassembleInstruction(Chunk* chunk, const ulong offset){ + write(" %04d ".format(offset).lightGray); if(offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1]){ - debug write(" | "); + write(" | ".darkGray); } else { - debug writef(" %4d ", chunk.lines[offset]); + write(" %4d ".format(chunk.lines[offset].toUint).lightGray); } ubyte instruction = chunk.code[offset]; with(OpCode) switch(instruction){ @@ -41,7 +49,7 @@ debug ulong disassembleInstruction(Chunk* chunk, const ulong offset) @nogc nothr return simpleInstruction(name, offset); } default: - debug writefln("Unknown opcode %d", instruction); + writefln("Unknown opcode %d", instruction); return offset + 1; } } diff --git a/src/clox/emitter.d b/src/clox/emitter.d new file mode 100644 index 0000000..480f3c6 --- /dev/null +++ b/src/clox/emitter.d @@ -0,0 +1,51 @@ +module clox.emitter; + +import clox.compiler; +import clox.chunk; +import clox.value; +import clox.util; +import clox.dbg; + +import clox.container.varint; + +struct Emitter{ + Compiler* compiler; + Chunk* chunk; + private uint line; + Chunk* currentChunk(){ + return chunk; + } + void emit(Args...)(Args args){ + static foreach(v; args){{ + static if(is(typeof(v) == OpCode)){ + auto bytes = v; + } else static if(is(typeof(v) == uint)){ + auto bytes = VarUint(v).bytes; + } else { + static assert(0); + } + currentChunk.write(bytes, line); + }} + } + void emitConstant(Value value){ + emit(OpCode.Constant, makeConstant(value)); + } + void emitReturn(){ + emit(OpCode.Return); + } + void endCompiler(){ + emitReturn(); + debug(printCode){ + if(!compiler.parser.hadError) + disassembleChunk(currentChunk()); + } + } + uint makeConstant(Value value){ + uint constant = chunk.addConstant(value); + return constant; + } + void setLine(uint l){ + this.line = l; + } +} + diff --git a/src/clox/main.d b/src/clox/main.d index 44ec26d..19e0fe7 100644 --- a/src/clox/main.d +++ b/src/clox/main.d @@ -25,7 +25,7 @@ struct Lox{ } int runPrompt(){ while(true){ - write("> "); + write("lox> "); string line = stdin.readln(); if(!line){ writeln(); diff --git a/src/clox/parser.d b/src/clox/parser.d new file mode 100644 index 0000000..24bbdbd --- /dev/null +++ b/src/clox/parser.d @@ -0,0 +1,66 @@ +module clox.parser; + +import clox.compiler; +import clox.value; +import clox.scanner; +import clox.parserules; + +struct Parser{ + Compiler* compiler; + Token current, previous; + bool hadError, panicMode; + void errorAtCurrent(string message){ + errorAt(current, message); + } + void error(string message){ + errorAt(previous, message); + } + void errorAt(in ref Token token, string message){ + import core.stdc.stdio; + if(panicMode) + return; + panicMode = true; + fprintf(stderr, "[line %d] Error", token.line); + if(token.type == Token.Type.EOF){ + fprintf(stderr, " at end"); + } else if(token.type != Token.Type.Error){ + fprintf(stderr, " at '%.*s'", cast(int)token.lexeme.length, token.lexeme.ptr); + } + fprintf(stderr, ": %.*s\n", cast(int)message.length, message.ptr); + hadError = true; + } + auto consume(Token.Type type, string msg){ + if(current.type == type){ + advance(); + return; + } + errorAtCurrent(msg); + } + void advance(){ + previous = current; + while(true){ + current = compiler.scanner.scan(); + if(current.type != Token.Type.Error) + break; + errorAtCurrent(current.lexeme); + } + } + + void expression(){ + parsePrecedence(Precedence.Assignment); + } + void parsePrecedence(Precedence precedence){ + advance(); + ParseFn prefixRule = ParseRule.get(previous.type).prefix; + if(prefixRule == null){ + error("Expect expression."); + return; + } + prefixRule(compiler); + while(precedence <= ParseRule.get(current.type).precedence){ + advance(); + ParseFn infixRule = ParseRule.get(previous.type).infix; + infixRule(compiler); + } + } +} diff --git a/src/clox/parserules.d b/src/clox/parserules.d new file mode 100644 index 0000000..3f8e315 --- /dev/null +++ b/src/clox/parserules.d @@ -0,0 +1,108 @@ +module clox.parserules; + +import clox.compiler; +import clox.chunk; +import clox.scanner; + +alias ParseFn = void function(Compiler* compiler); + +void number(Compiler* compiler){ + import core.stdc.stdlib : strtod; + Token token = compiler.parser.previous; + double value = strtod(token.lexeme.ptr, null); + compiler.emitter.setLine(token.line); + compiler.emitter.emitConstant(value); +} +void grouping(Compiler* compiler){ + compiler.parser.expression(); + compiler.parser.consume(Token.Type.RightParen, "Expect ')' after expression."); +} +void unary(Compiler* compiler){ + Token operator = compiler.parser.previous; + compiler.parser.parsePrecedence(Precedence.Unary); + compiler.emitter.setLine(operator.line); + switch(operator.type){ + case Token.Type.Minus: compiler.emitter.emit(OpCode.Negate); break; + default: assert(0); + } +} +void binary(Compiler* compiler){ + Token operator = compiler.parser.previous; + immutable(ParseRule)* rule = ParseRule.get(operator.type); + compiler.parser.parsePrecedence(cast(Precedence)(rule.precedence + 1)); + compiler.emitter.setLine(operator.line); + switch(operator.type){ + case Token.Type.Plus: compiler.emitter.emit(OpCode.Add); break; + case Token.Type.Minus: compiler.emitter.emit(OpCode.Subtract); break; + case Token.Type.Star: compiler.emitter.emit(OpCode.Multiply); break; + case Token.Type.Slash: compiler.emitter.emit(OpCode.Divide); break; + default: assert(0); + } +} + +struct ParseRule{ + ParseFn prefix; + ParseFn infix; + Precedence precedence; + static immutable(ParseRule)* get(Token.Type type) @nogc nothrow{ + return &rules[type]; + } +} + +enum Precedence{ + None, + Assignment, // = + Or, // or + And, // and + Equality, // == != + Comparison, // < > <= >= + Term, // + - + Factor, // * / + Unary, // ! - + Call, // . () + Primary +} + +immutable ParseRule[Token.Type.max+1] rules = [ + Token.Type.LeftParen : ParseRule(&grouping, null, Precedence.None), + Token.Type.RightParen : ParseRule(null, null, Precedence.None), + Token.Type.LeftBrace : ParseRule(null, null, Precedence.None), + Token.Type.RightBrace : ParseRule(null, null, Precedence.None), + Token.Type.Comma : ParseRule(null, null, Precedence.None), + Token.Type.Dot : ParseRule(null, null, Precedence.None), + Token.Type.Minus : ParseRule(&unary, &binary, Precedence.Term), + Token.Type.Plus : ParseRule(null, &binary, Precedence.Term), + Token.Type.Semicolon : ParseRule(null, null, Precedence.None), + Token.Type.Slash : ParseRule(null, &binary, Precedence.Factor), + Token.Type.Star : ParseRule(null, &binary, Precedence.Factor), + Token.Type.Bang : ParseRule(null, null, Precedence.None), + Token.Type.BangEqual : ParseRule(null, null, Precedence.None), + Token.Type.Equal : ParseRule(null, null, Precedence.None), + Token.Type.EqualEqual : ParseRule(null, null, Precedence.None), + Token.Type.Greater : ParseRule(null, null, Precedence.None), + Token.Type.GreaterEqual : ParseRule(null, null, Precedence.None), + Token.Type.Less : ParseRule(null, null, Precedence.None), + Token.Type.LessEqual : ParseRule(null, null, Precedence.None), + Token.Type.Identifier : ParseRule(null, null, Precedence.None), + Token.Type.String : ParseRule(null, null, Precedence.None), + Token.Type.Number : ParseRule(&number, null, Precedence.None), + Token.Type.And : ParseRule(null, null, Precedence.None), + Token.Type.Class : ParseRule(null, null, Precedence.None), + Token.Type.Else : ParseRule(null, null, Precedence.None), + Token.Type.False : ParseRule(null, null, Precedence.None), + Token.Type.For : ParseRule(null, null, Precedence.None), + Token.Type.Fun : ParseRule(null, null, Precedence.None), + Token.Type.If : ParseRule(null, null, Precedence.None), + Token.Type.Nil : ParseRule(null, null, Precedence.None), + Token.Type.Or : ParseRule(null, null, Precedence.None), + Token.Type.Print : ParseRule(null, null, Precedence.None), + Token.Type.Return : ParseRule(null, null, Precedence.None), + Token.Type.Super : ParseRule(null, null, Precedence.None), + Token.Type.This : ParseRule(null, null, Precedence.None), + Token.Type.True : ParseRule(null, null, Precedence.None), + Token.Type.Var : ParseRule(null, null, Precedence.None), + Token.Type.While : ParseRule(null, null, Precedence.None), + Token.Type.Error : ParseRule(null, null, Precedence.None), + Token.Type.EOF : ParseRule(null, null, Precedence.None), +]; + diff --git a/src/clox/scanner.d b/src/clox/scanner.d index 1d95000..d916993 100644 --- a/src/clox/scanner.d +++ b/src/clox/scanner.d @@ -7,6 +7,7 @@ import common.util; struct Token{ enum Type : ubyte { + None, Error, EOF, // Special LeftParen, RightParen, // Single-character tokens. LeftBrace, RightBrace, Comma, Dot, Minus, Plus, @@ -20,19 +21,20 @@ struct Token{ For, Fun, If, Nil, Or, Print, Return, Super, This, True, Var, While, - Error, EOF // Special } Type type; int line; string lexeme; - static Token error(string msg) => Token(Token.Type.Error, 0, msg); + static Token error(string msg) nothrow @nogc => Token(Token.Type.Error, 0, msg); } struct Scanner{ + nothrow: + @nogc: string start; string current; int line = 1; - this(string source){ + this(string source) @nogc nothrow{ start = current = source; } bool isAtEnd() const => current.length == 0; @@ -42,6 +44,7 @@ struct Scanner{ Token token; token.type = type; token.lexeme = start[0 .. current.ptr - start.ptr]; + token.line = line; return token; } private char advance(){ @@ -67,6 +70,7 @@ struct Scanner{ } if(!c.isWhite) return; + /* debug writeln(c == '\n'); */ if(c == '\n') line++; current = current[1 .. $]; @@ -147,7 +151,7 @@ struct Scanner{ } if(c.isDigit) return parseNumber(); - return Token.error("Unexpected character '" ~ c ~ "'."); + return Token.error("Unexpected character."); } } diff --git a/src/clox/util.d b/src/clox/util.d index 081e6b9..45c0945 100644 --- a/src/clox/util.d +++ b/src/clox/util.d @@ -1,25 +1,6 @@ module clox.util; -struct Stack(T, size_t N){ - T* top; - T[N] data; - invariant{ assert(top <= data.ptr + N); assert(top >= data.ptr); } - this(int _) @nogc nothrow{ - top = data.ptr; - } - void push(T value) @nogc nothrow{ - assert(top < data.ptr + N); - debug assert(*top is T.init); - *(top++) = value; - } - T pop() @nogc nothrow{ - assert(top > data.ptr); - T t = *(--top); - debug *(top) = T.init; - return t; - } - const(T)[] live() @nogc nothrow const{ - return data[0 .. (top - data.ptr)]; - } -} +import std.stdio; +import std.traits : isUnsigned; +import std.container.array; diff --git a/src/clox/value.d b/src/clox/value.d index eb74daa..c9b298c 100644 --- a/src/clox/value.d +++ b/src/clox/value.d @@ -4,7 +4,7 @@ import std.stdio; alias Value = double; -debug void printValue(Value value) @nogc nothrow{ - debug writef("%g", value); +void printValue(Value value){ + writef("%g", value); } diff --git a/src/clox/vm.d b/src/clox/vm.d index 151ec6d..67a5090 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -7,6 +7,8 @@ import clox.value; import clox.dbg; import clox.util; import clox.compiler; +import clox.container.stack; +import clox.container.varint; enum stackMax = 256; @@ -18,11 +20,15 @@ struct VM{ this(int _) @nogc nothrow { stack = typeof(stack)(0); } - InterpretResult interpret(string source) @nogc nothrow { - compile(source); - return InterpretResult.Ok; + InterpretResult interpret(string source){ + Chunk c = Chunk(); + Compiler compiler; + if(!compiler.compile(source, &c)) + return InterpretResult.CompileError; + chunk = &c; + return interpret(chunk); } - InterpretResult interpret(Chunk* chunk) @nogc nothrow { + InterpretResult interpret(Chunk* chunk){ this.chunk = chunk; ip = &chunk.code[0]; return run(); @@ -30,11 +36,15 @@ struct VM{ private InterpretResult run() @nogc nothrow { auto readByte() => *ip++; auto readIns() => cast(OpCode)readByte(); - auto readConstant() => chunk.constants[readByte()]; + Value readConstant(){ + VarUint constant = VarUint.read(ip[0 .. 4]); + ip += constant.len; + return chunk.constants[constant.i]; + } while(true){ debug(traceExec){ writeln(" ", stack.live); - debug disassembleInstruction(chunk, ip - &chunk.code[0]); + disassembleInstruction(chunk, ip - &chunk.code[0]); } OpCode instruction = readIns(); with(OpCode) opSwitch: final switch(instruction){ diff --git a/src/common/util.d b/src/common/util.d index 0efa053..a6876ba 100644 --- a/src/common/util.d +++ b/src/common/util.d @@ -8,6 +8,37 @@ template defaultCtor(){ } import std.ascii : isAlpha, isAlphaNum; -bool isAlpha_(dchar c) => c.isAlpha || c == '_'; -bool isAlphaNum_(dchar c) => c.isAlphaNum || c == '_'; +bool isAlpha_(dchar c) @nogc nothrow @safe => c.isAlpha || c == '_'; +bool isAlphaNum_(dchar c) @nogc nothrow @safe => c.isAlphaNum || c == '_'; + +T pop(T)(ref T[] arr){ + T v = arr.last; + arr.length--; + return v; +} +T shift(T)(ref T[] arr){ + T v = arr.first; + arr = arr[1 .. $]; + return v; +} +ref T sole(T)(T[] arr) @nogc @safe pure{ + assert(arr.length == 1, "Not sole"); + return arr[0]; +} +ref T first(T)(T[] arr) @nogc @safe pure{ + assert(arr.length >= 1, "No first"); + return arr[0]; +} +ref T second(T)(T[] arr) @nogc @safe pure{ + assert(arr.length >= 2, "No second"); + return arr[1]; +} +ref T third(T)(T[] arr) @nogc @safe pure{ + assert(arr.length >= 3, "No third"); + return arr[2]; +} +ref T last(T)(T[] arr) @nogc @safe pure{ + assert(arr.length >= 1, "No last"); + return arr[$-1]; +} From 10c44eab2ecc3a492fe0de24d47eb584295cd99c Mon Sep 17 00:00:00 2001 From: nazrin Date: Thu, 5 Jun 2025 00:55:26 +0000 Subject: [PATCH 10/10] Types of Values 18 --- src/clox/chunk.d | 36 ++++++++++++++++++++-- src/clox/container/vartype.d | 59 ++++++++++++++++++++++++++++++++++++ src/clox/dbg.d | 18 ++++++++--- src/clox/emitter.d | 2 +- src/clox/parserules.d | 50 +++++++++++++++++++++--------- src/clox/value.d | 47 ++++++++++++++++++++++++++-- src/clox/vm.d | 46 ++++++++++++++++++++++++++-- 7 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 src/clox/container/vartype.d diff --git a/src/clox/chunk.d b/src/clox/chunk.d index 833e8b1..e41bcaf 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -8,12 +8,42 @@ import clox.value; import clox.container.rle; import clox.container.int24; +enum SimpleOp; +enum LogicOp; +enum ValueOp; +enum ArithOp; +enum CompOp; enum OpCode : ubyte{ Constant, - Add, Subtract, Multiply, Divide, - Negate, - Return, + @ValueOp Nil, + @ValueOp True, + @ValueOp False, + + @(SimpleOp, CompOp) Equal, + @(SimpleOp, CompOp) Greater, + @(SimpleOp, CompOp) Less, + @(SimpleOp, CompOp) NotEqual, + @(SimpleOp, CompOp) GreaterEqual, + @(SimpleOp, CompOp) LessEqual, + + @(SimpleOp, ArithOp) Add, + @(SimpleOp, ArithOp) Subtract, + @(SimpleOp, ArithOp) Multiply, + @(SimpleOp, ArithOp) Divide, + + @(SimpleOp, LogicOp) Not, + @(SimpleOp, LogicOp) Negate, + @(SimpleOp) Return, } +import std.traits: hasUDA; +bool isSimpleOp(alias op)() => hasUDA!(op, SimpleOp); +bool isValueOp(alias op)() => hasUDA!(op, ValueOp); +bool isLogicOp(alias op)() => hasUDA!(op, LogicOp); +bool isCompOp(alias op)() => hasUDA!(op, CompOp); +bool isArithOp(alias op)() => hasUDA!(op, ArithOp); +bool isSize1Op(alias op)() => isSimpleOp!op || isValueOp!op; +static assert( isSimpleOp!(OpCode.Equal) ); +static assert( !isSimpleOp!(OpCode.Constant) ); struct Chunk{ Array!ubyte code; diff --git a/src/clox/container/vartype.d b/src/clox/container/vartype.d new file mode 100644 index 0000000..1a27cc9 --- /dev/null +++ b/src/clox/container/vartype.d @@ -0,0 +1,59 @@ +module clox.container.vartype; + +import std.stdio; +import std.algorithm; +import std.array; +import std.uni; +import std.conv; + +struct VarType(S) if(is(S == union)){ + private enum members = __traits(derivedMembers, S); + mixin("enum Type{ None, ", [members].map!asCapitalized.join(", "), "}"); + private S value; + private Type _type; + Type type() const @safe @nogc nothrow => _type; + private void check(Type t) const @safe nothrow @nogc{ + assert(this.type == t, "Tried to get wrong type"); + } + private template funcs(string G, string T){ + mixin("bool is", G, "() const nothrow @nogc @safe => this.type == this.Type.", G, ";"); + mixin("auto get", G, "() const nothrow @nogc { check(this.Type.", G, "); return value.", T, "; }"); + mixin("void set", G, "(typeof(S.", T, ") v = typeof(S.", T, ").init){ this._type = this.Type.", G, "; this.value.", T, " = v; }"); + mixin("static auto ", T, "(typeof(S.", T, ") v){ typeof(this) vt; vt.set", G, "(v); return vt; }"); + mixin("static auto ", T, "(){ typeof(this) vt; vt.set", G, "(); return vt; }"); + } + static foreach(s; members){ + mixin funcs!(s.asCapitalized.to!string, s); + } + string toString() const{ + final switch(_type){ + static foreach(s; members){ + mixin("case Type.", s.asCapitalized.to!string, ": return _type.to!string ~ ':' ~ get", s.asCapitalized.to!string, ".to!string ;"); + } + case Type.None: return "None"; + } + } +} + +unittest{ + import std.exception, std.stdio; + union Test{ + uint u; + int i; + double d; + } + auto i = VarType!Test.i(-5); + assert(i.getI == -5); + assert(i.type == i.Type.I); + assert(i.isI); + assert(!i.isD); + i.setD(0.5); + assert(i.getD == 0.5); + assert(i.type == i.Type.D); + assert(i.isD); + assert(!i.isU); + + auto i2 = VarType!Test.i(); + assert(i2.isI); +} + diff --git a/src/clox/dbg.d b/src/clox/dbg.d index 9d88850..570efc5 100644 --- a/src/clox/dbg.d +++ b/src/clox/dbg.d @@ -13,8 +13,17 @@ import clox.util; import clox.container.varint; import clox.container.int24; -private ulong simpleInstruction(string name, ulong offset){ - writeln(name.lightCyan); +private ulong simpleInstruction(alias op)(string name, ulong offset){ + static if(isValueOp!op) + writeln(name.cyan); + else static if(isLogicOp!op) + writeln(name.lightRed); + else static if(isCompOp!op) + writeln(name.red); + else static if(isArithOp!op) + writeln(name.yellow); + else + writeln(name.lightCyan); return offset + 1; } private ulong constantInstruction(string name, Chunk* chunk, ulong offset){ @@ -41,12 +50,13 @@ ulong disassembleInstruction(Chunk* chunk, const ulong offset){ } ubyte instruction = chunk.code[offset]; with(OpCode) switch(instruction){ + import std.meta, std.traits; case Constant: return constantInstruction("OP_CONSTANT", chunk, offset); - static foreach(k; [ Return, Negate, Add, Subtract, Multiply, Divide ]){ + static foreach(k; Filter!(isSize1Op, EnumMembers!OpCode)){ case k: static name = "OP_" ~ (k.to!string).toUpper; - return simpleInstruction(name, offset); + return simpleInstruction!k(name, offset); } default: writefln("Unknown opcode %d", instruction); diff --git a/src/clox/emitter.d b/src/clox/emitter.d index 480f3c6..116f504 100644 --- a/src/clox/emitter.d +++ b/src/clox/emitter.d @@ -11,7 +11,7 @@ import clox.container.varint; struct Emitter{ Compiler* compiler; Chunk* chunk; - private uint line; + private uint line = 1; Chunk* currentChunk(){ return chunk; } diff --git a/src/clox/parserules.d b/src/clox/parserules.d index 3f8e315..a8bff9e 100644 --- a/src/clox/parserules.d +++ b/src/clox/parserules.d @@ -3,30 +3,32 @@ module clox.parserules; import clox.compiler; import clox.chunk; import clox.scanner; +import clox.value; alias ParseFn = void function(Compiler* compiler); -void number(Compiler* compiler){ +private void number(Compiler* compiler){ import core.stdc.stdlib : strtod; Token token = compiler.parser.previous; double value = strtod(token.lexeme.ptr, null); compiler.emitter.setLine(token.line); - compiler.emitter.emitConstant(value); + compiler.emitter.emitConstant(Value.num(value)); } -void grouping(Compiler* compiler){ +private void grouping(Compiler* compiler){ compiler.parser.expression(); compiler.parser.consume(Token.Type.RightParen, "Expect ')' after expression."); } -void unary(Compiler* compiler){ +private void unary(Compiler* compiler){ Token operator = compiler.parser.previous; compiler.parser.parsePrecedence(Precedence.Unary); compiler.emitter.setLine(operator.line); switch(operator.type){ case Token.Type.Minus: compiler.emitter.emit(OpCode.Negate); break; + case Token.Type.Bang: compiler.emitter.emit(OpCode.Not); break; default: assert(0); } } -void binary(Compiler* compiler){ +private void binary(Compiler* compiler){ Token operator = compiler.parser.previous; immutable(ParseRule)* rule = ParseRule.get(operator.type); compiler.parser.parsePrecedence(cast(Precedence)(rule.precedence + 1)); @@ -36,6 +38,24 @@ void binary(Compiler* compiler){ case Token.Type.Minus: compiler.emitter.emit(OpCode.Subtract); break; case Token.Type.Star: compiler.emitter.emit(OpCode.Multiply); break; case Token.Type.Slash: compiler.emitter.emit(OpCode.Divide); break; + + case Token.Type.BangEqual: compiler.emitter.emit(OpCode.NotEqual); break; + case Token.Type.EqualEqual: compiler.emitter.emit(OpCode.Equal); break; + + case Token.Type.Greater: compiler.emitter.emit(OpCode.Greater); break; + case Token.Type.GreaterEqual: compiler.emitter.emit(OpCode.GreaterEqual); break; + + case Token.Type.Less: compiler.emitter.emit(OpCode.Less); break; + case Token.Type.LessEqual: compiler.emitter.emit(OpCode.LessEqual); break; + + default: assert(0); + } +} +private void literal(Compiler* compiler){ + switch(compiler.parser.previous.type){ + case Token.Type.True: compiler.emitter.emit(OpCode.True); break; + case Token.Type.False: compiler.emitter.emit(OpCode.False); break; + case Token.Type.Nil: compiler.emitter.emit(OpCode.Nil); break; default: assert(0); } } @@ -75,31 +95,31 @@ immutable ParseRule[Token.Type.max+1] rules = [ Token.Type.Semicolon : ParseRule(null, null, Precedence.None), Token.Type.Slash : ParseRule(null, &binary, Precedence.Factor), Token.Type.Star : ParseRule(null, &binary, Precedence.Factor), - Token.Type.Bang : ParseRule(null, null, Precedence.None), - Token.Type.BangEqual : ParseRule(null, null, Precedence.None), + Token.Type.Bang : ParseRule(&unary, null, Precedence.None), + Token.Type.BangEqual : ParseRule(null, &binary, Precedence.Equality), Token.Type.Equal : ParseRule(null, null, Precedence.None), - Token.Type.EqualEqual : ParseRule(null, null, Precedence.None), - Token.Type.Greater : ParseRule(null, null, Precedence.None), - Token.Type.GreaterEqual : ParseRule(null, null, Precedence.None), - Token.Type.Less : ParseRule(null, null, Precedence.None), - Token.Type.LessEqual : ParseRule(null, null, Precedence.None), + Token.Type.EqualEqual : ParseRule(null, &binary, Precedence.Equality), + Token.Type.Greater : ParseRule(null, &binary, Precedence.Comparison), + Token.Type.GreaterEqual : ParseRule(null, &binary, Precedence.Comparison), + Token.Type.Less : ParseRule(null, &binary, Precedence.Comparison), + Token.Type.LessEqual : ParseRule(null, &binary, Precedence.Comparison), Token.Type.Identifier : ParseRule(null, null, Precedence.None), Token.Type.String : ParseRule(null, null, Precedence.None), Token.Type.Number : ParseRule(&number, null, Precedence.None), Token.Type.And : ParseRule(null, null, Precedence.None), Token.Type.Class : ParseRule(null, null, Precedence.None), Token.Type.Else : ParseRule(null, null, Precedence.None), - Token.Type.False : ParseRule(null, null, Precedence.None), + Token.Type.False : ParseRule(&literal, null, Precedence.None), Token.Type.For : ParseRule(null, null, Precedence.None), Token.Type.Fun : ParseRule(null, null, Precedence.None), Token.Type.If : ParseRule(null, null, Precedence.None), - Token.Type.Nil : ParseRule(null, null, Precedence.None), + Token.Type.Nil : ParseRule(&literal, null, Precedence.None), Token.Type.Or : ParseRule(null, null, Precedence.None), Token.Type.Print : ParseRule(null, null, Precedence.None), Token.Type.Return : ParseRule(null, null, Precedence.None), Token.Type.Super : ParseRule(null, null, Precedence.None), Token.Type.This : ParseRule(null, null, Precedence.None), - Token.Type.True : ParseRule(null, null, Precedence.None), + Token.Type.True : ParseRule(&literal, null, Precedence.None), Token.Type.Var : ParseRule(null, null, Precedence.None), Token.Type.While : ParseRule(null, null, Precedence.None), Token.Type.Error : ParseRule(null, null, Precedence.None), diff --git a/src/clox/value.d b/src/clox/value.d index c9b298c..1914616 100644 --- a/src/clox/value.d +++ b/src/clox/value.d @@ -2,9 +2,52 @@ module clox.value; import std.stdio; -alias Value = double; +import clox.container.vartype; + +/* struct Value{ */ +/* alias T = VarType!u; */ +/* private union U{ */ +/* bool bln; */ +/* bool nil; */ +/* double num; */ +/* } */ +/* T v; */ +/* alias v this; */ +/* } */ + private union U{ + bool bln; + bool nil; + double num; + } +alias Value = VarType!U; void printValue(Value value){ - writef("%g", value); + final switch(value.type){ + case value.Type.Bln: writef("%s", value.getBln); break; + case value.Type.Num: writef("%g", value.getNum); break; + case value.Type.Nil: writef("nil"); break; + case value.Type.None: assert(0); + } +} +bool isTruthy(Value value) nothrow @nogc { + final switch(value.type){ + case value.Type.Bln: return value.getBln; + case value.Type.Num: return true; + case value.Type.Nil: return false; + case value.Type.None: assert(0); + } +} +bool isFalsey(Value value) nothrow @nogc { + return !isTruthy(value); +} +bool compare(string op)(Value a, Value b){ + if(a.type != b.type) + return false; + final switch(a.type){ + case a.Type.Bln: return mixin("a.getBln", op, "b.getBln"); + case a.Type.Num: return mixin("a.getNum", op, "b.getNum"); + case a.Type.Nil: return true; + case a.Type.None: assert(0); + } } diff --git a/src/clox/vm.d b/src/clox/vm.d index 67a5090..485a626 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -9,6 +9,7 @@ import clox.util; import clox.compiler; import clox.container.stack; import clox.container.varint; +import clox.container.int24; enum stackMax = 256; @@ -33,7 +34,16 @@ struct VM{ ip = &chunk.code[0]; return run(); } - private InterpretResult run() @nogc nothrow { + private void runtimeError(Args...)(string format, Args args) nothrow { + size_t instruction = ip - (&chunk.code[0]) - 1; + uint line = chunk.lines[instruction].toUint; + try{ + stderr.writef("[line %d] ", line); + stderr.writefln(format, args); + } catch(Exception){} + /* stack.reset(); */ + } + private InterpretResult run() nothrow { auto readByte() => *ip++; auto readIns() => cast(OpCode)readByte(); Value readConstant(){ @@ -41,6 +51,9 @@ struct VM{ ip += constant.len; return chunk.constants[constant.i]; } + Value peek(int distance = 0){ + return stack.top[-1 - distance]; + } while(true){ debug(traceExec){ writeln(" ", stack.live); @@ -52,15 +65,42 @@ struct VM{ Value constant = readConstant(); stack.push(constant); break; + case True: + stack.push(Value.bln(true)); + break; + case False: + stack.push(Value.bln(false)); + break; + case Nil: + stack.push(Value.nil()); + break; static foreach(k, op; [ Add: "+", Subtract: "-", Multiply: "*", Divide: "/" ]){ + case k: + if(!peek(0).isNum || !peek(1).isNum){ + runtimeError("Operands must be numbers."); + return InterpretResult.RuntimeError; + } + double b = stack.pop().getNum; + double a = stack.pop().getNum; + stack.push(Value.num(mixin("a", op, "b"))); + break opSwitch; + } + static foreach(k, op; [ NotEqual: "!=", Equal: "==", Greater: ">", GreaterEqual: ">=", Less: "<", LessEqual: "<=" ]){ case k: Value b = stack.pop(); Value a = stack.pop(); - stack.push(mixin("a", op, "b")); + stack.push(Value.bln(compare!op(a, b))); break opSwitch; } + case Not: + stack.push(Value.bln(stack.pop().isFalsey)); + break; case Negate: - stack.push(-stack.pop()); + if(!peek(0).isNum){ + runtimeError("Operand must be a number."); + return InterpretResult.RuntimeError; + } + stack.push(Value.num(-stack.pop().getNum)); break; case Return: debug printValue(stack.pop());