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 @@ +