Classes 12
This commit is contained in:
parent
52a7b73a9e
commit
d8ac625429
18 changed files with 417 additions and 186 deletions
1
dub.sdl
1
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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); */
|
||||
/* } */
|
||||
/* } */
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => "<native clock>";
|
||||
});
|
||||
|
||||
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 => "<native sleep>";
|
||||
});
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
|
|||
34
src/jlox/loxclass.d
Normal file
34
src/jlox/loxclass.d
Normal file
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
27
src/jlox/loxinstance.d
Normal file
27
src/jlox/loxinstance.d
Normal file
|
|
@ -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 => "<instance %s>".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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 "<function>";
|
||||
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 => "<nil>";
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
30
test/all.d
30
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;
|
||||
|
|
|
|||
21
test/class.lox
Normal file
21
test/class.lox
Normal file
|
|
@ -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();
|
||||
|
||||
17
test/ops.lox
Normal file
17
test/ops.lox
Normal file
|
|
@ -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";
|
||||
|
||||
12
test/shortcircuit.lox
Normal file
12
test/shortcircuit.lox
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
fun scream(extra){
|
||||
extra = extra or "!";
|
||||
print "AAAA" + extra;
|
||||
}
|
||||
|
||||
print nil or true;
|
||||
|
||||
scream(false);
|
||||
scream(nil);
|
||||
scream("?");
|
||||
|
||||
|
|
@ -1 +1,2 @@
|
|||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue