Classes 12

This commit is contained in:
nazrin 2025-06-02 22:22:04 +00:00
parent 52a7b73a9e
commit d8ac625429
18 changed files with 417 additions and 186 deletions

View file

@ -4,7 +4,6 @@ authors "tanya"
copyright "Copyright © 2025, tanya" copyright "Copyright © 2025, tanya"
license "MPL-2.0" license "MPL-2.0"
dependency "commandr" version="~>1.1.0" dependency "commandr" version="~>1.1.0"
dependency "taggedalgebraic" version="~>0.11.23"
targetType "executable" targetType "executable"
buildRequirements "requireBoundsCheck" "requireContracts" buildRequirements "requireBoundsCheck" "requireContracts"

View file

@ -6,15 +6,15 @@ import common.util;
class Environment{ class Environment{
Environment enclosing; Environment enclosing;
private TValue[string] values; private LoxValue[string] values;
mixin defaultCtor; mixin defaultCtor;
void define(string name, TValue value){ void define(string name, LoxValue value){
values[name] = value; values[name] = value;
} }
TValue get(Token name){ LoxValue get(Token name){
if(TValue* value = name.lexeme in values) if(LoxValue* value = name.lexeme in values)
return *value; return *value;
if(enclosing) if(enclosing)
return enclosing.get(name); return enclosing.get(name);
@ -26,17 +26,19 @@ class Environment{
environment = environment.enclosing; environment = environment.enclosing;
return environment; return environment;
} }
TValue getAt(uint distance, string name){ LoxValue getAt(uint distance, string name){
return ancestor(distance).values[name]; return ancestor(distance).values[name];
} }
void assign(Token name, TValue value){ void assign(Token name, LoxValue value){
if(name.lexeme in values) if(name.lexeme in values){
return values[name.lexeme] = value; values[name.lexeme] = value;
return;
}
if(enclosing) if(enclosing)
return enclosing.assign(name, value); return enclosing.assign(name, value);
throw new RuntimeError(name, "Undefined variable '" ~ name.lexeme ~ "'."); 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; ancestor(distance).values[name.lexeme] = value;
} }
} }

View file

@ -4,8 +4,6 @@ import std.conv;
import std.stdio; import std.stdio;
import std.meta : AliasSeq; import std.meta : AliasSeq;
import taggedalgebraic;
import jlox.token; import jlox.token;
import jlox.tokentype; import jlox.tokentype;
import common.util; import common.util;
@ -15,13 +13,16 @@ abstract class Expr{
R visit(Assign expr); R visit(Assign expr);
R visit(Binary expr); R visit(Binary expr);
R visit(Call expr); R visit(Call expr);
R visit(Get expr);
R visit(Grouping expr); R visit(Grouping expr);
R visit(Literal expr); R visit(Literal expr);
R visit(Logical expr); R visit(Logical expr);
R visit(Set expr);
R visit(This expr);
R visit(Unary expr); R visit(Unary expr);
R visit(Variable expr); R visit(Variable expr);
} }
private alias rTypes = AliasSeq!(string, TValue, void); private alias rTypes = AliasSeq!(string, LoxValue, void);
static foreach(T; rTypes) static foreach(T; rTypes)
abstract T accept(Visitor!T visitor); abstract T accept(Visitor!T visitor);
private template defCtorAndAccept(){ private template defCtorAndAccept(){
@ -47,12 +48,17 @@ abstract class Expr{
Expr[] arguments; Expr[] arguments;
mixin defCtorAndAccept; mixin defCtorAndAccept;
} }
static class Get : typeof(this){
Expr object;
Token name;
mixin defCtorAndAccept;
}
static class Grouping : typeof(this){ static class Grouping : typeof(this){
Expr expression; Expr expression;
mixin defCtorAndAccept; mixin defCtorAndAccept;
} }
static class Literal : typeof(this){ static class Literal : typeof(this){
TValue value; LoxValue value;
mixin defCtorAndAccept; mixin defCtorAndAccept;
} }
static class Logical : typeof(this){ static class Logical : typeof(this){
@ -61,6 +67,16 @@ abstract class Expr{
Expr right; Expr right;
mixin defCtorAndAccept; 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){ static class Unary : typeof(this){
Token operator; Token operator;
Expr right; 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); */
/* } */
/* } */

View file

@ -11,10 +11,12 @@ import jlox.expr;
import jlox.stmt; import jlox.stmt;
import jlox.token; import jlox.token;
import jlox.tokentype; import jlox.tokentype;
import jlox.token : TValue; import jlox.token : LoxValue;
import jlox.main; import jlox.main;
import jlox.environment; import jlox.environment;
import jlox.loxfunction; import jlox.loxfunction;
import jlox.loxclass;
import jlox.loxinstance;
class RuntimeError : Exception{ class RuntimeError : Exception{
const Token token; const Token token;
@ -24,14 +26,14 @@ class RuntimeError : Exception{
} }
} }
class Return : Exception{ class Return : Exception{
const TValue value; const LoxValue value;
this(TValue value){ this(LoxValue value){
super(null); super(null);
this.value = value; this.value = value;
} }
} }
class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue { class Interpreter : Stmt.Visitor!void, Expr.Visitor!LoxValue {
Environment globals = new Environment(); Environment globals = new Environment();
private Environment environment; private Environment environment;
this(){ this(){
@ -39,20 +41,24 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue {
import std.datetime.stopwatch; import std.datetime.stopwatch;
auto sw = StopWatch(AutoStart.yes); auto sw = StopWatch(AutoStart.yes);
globals.define("clock", TValue.cal(new class LoxCallable{ globals.define("clock", new class LoxCallable{
int arity() => 0; 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){ version(LoxExtraNativeFuncs){
globals.define("sleep", TValue.cal(new class LoxCallable{ globals.define("sleep", new class LoxCallable{
int arity() => 1; int arity() => 1;
import core.thread.osthread; import core.thread.osthread;
TValue call(Interpreter interpreter, TValue[] arguments){ LoxValue call(Interpreter interpreter, LoxValue[] arguments){
Thread.sleep(dur!"usecs"(cast(long)(arguments[0].dblValue * 1000 * 1000))); if(cast(LoxNum)arguments[0] is null)
return TValue.nil(tvalueNil); 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){ void interpret(Stmt[] statements){
@ -76,24 +82,21 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue {
this.environment = previous; this.environment = previous;
} }
} }
private TValue evaluate(Expr expr){ private LoxValue evaluate(Expr expr){
return expr.accept(this); return expr.accept(this);
} }
private bool isTruthy(TValue val){ private bool isTruthy(LoxValue val){
switch(val.kind){ if(cast(LoxNil)val)
case TValue.Kind.nil: return false;
return false; if(auto b = cast(LoxBool)val)
case TValue.Kind.bln: return b;
return val.blnValue; return true;
default:
return true;
}
} }
private bool isEqual(TValue a, TValue b){ private bool isEqual(LoxValue a, LoxValue b){
return a == b; return a.equals(b);
} }
private void checkNumberOperand(Token operator, TValue operand){ private void checkNumberOperand(Token operator, LoxValue operand){
if(operand.kind == TValue.Kind.dbl) if(cast(LoxNum)operand)
return; return;
throw new RuntimeError(operator, "Operand must be a number."); 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){ void visit(Stmt.Block stmt){
executeBlock(stmt.statements, new Environment(environment)); 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){ void visit(Stmt.Expression stmt){
evaluate(stmt.expression); evaluate(stmt.expression);
} }
void visit(Stmt.Function stmt){ void visit(Stmt.Function stmt){
LoxFunction func = new LoxFunction(stmt, environment); LoxFunction func = new LoxFunction(stmt, environment, false);
environment.define(stmt.name.lexeme, TValue.cal(func)); environment.define(stmt.name.lexeme, func);
} }
void visit(Stmt.If stmt){ void visit(Stmt.If stmt){
if(isTruthy(evaluate(stmt.condition))) if(isTruthy(evaluate(stmt.condition)))
@ -120,44 +133,44 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue {
} }
void visit(Stmt.Print stmt){ void visit(Stmt.Print stmt){
version(LoxPrintMultiple){ 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 { } else {
TValue value = evaluate(stmt.expression); LoxValue value = evaluate(stmt.expression);
writeln(tvalueToString(value)); writeln(value.toString);
} }
} }
void visit(Stmt.Return stmt){ 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); throw new Return(value);
} }
void visit(Stmt.Var stmt){ 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){ void visit(Stmt.While stmt){
while(isTruthy(evaluate(stmt.condition))) while(isTruthy(evaluate(stmt.condition)))
execute(stmt.body); execute(stmt.body);
} }
TValue visit(Expr.Literal expr){ LoxValue visit(Expr.Literal expr){
return expr.value; return expr.value;
} }
TValue visit(Expr.Grouping expr){ LoxValue visit(Expr.Grouping expr){
return evaluate(expr.expression); return evaluate(expr.expression);
} }
TValue visit(Expr.Unary expr){ LoxValue visit(Expr.Unary expr){
TValue right = evaluate(expr.right); LoxValue right = evaluate(expr.right);
switch(expr.operator.type){ switch(expr.operator.type){
case TokenType.MINUS: case TokenType.MINUS:
checkNumberOperand(expr.operator, right); checkNumberOperand(expr.operator, right);
return TValue.dbl(-right.dblValue); return new LoxNum(-cast(LoxNum)right);
case TokenType.BANG: case TokenType.BANG:
return TValue.bln(!isTruthy(right)); return new LoxBool(!isTruthy(right));
default: default:
assert(0); assert(0);
} }
} }
TValue visit(Expr.Logical expr){ LoxValue visit(Expr.Logical expr){
TValue left = evaluate(expr.left); LoxValue left = evaluate(expr.left);
if(expr.operator.type == TokenType.OR){ if(expr.operator.type == TokenType.OR){
if(isTruthy(left)) if(isTruthy(left))
return left; return left;
@ -167,64 +180,71 @@ class Interpreter : Stmt.Visitor!void, Expr.Visitor!TValue {
} }
return evaluate(expr.right); return evaluate(expr.right);
} }
TValue visit(Expr.Binary expr){ LoxValue visit(Expr.Set expr){
TValue left = evaluate(expr.left); LoxValue object = evaluate(expr.object);
TValue right = evaluate(expr.right); if(!(cast(LoxInstance)object))
static string m(TokenType t, string op, string v, string vv){ throw new RuntimeError(expr.name, "Only instances have fields.");
return q{case %s: LoxValue value = evaluate(expr.value);
checkNumberOperand(expr.operator, left); (cast(LoxInstance)object).set(expr.name, value);
checkNumberOperand(expr.operator, right); return value;
return TValue.%s( left.%s %s right.%s ); }
}.format(t, v, vv, op, vv); 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){ with(TokenType) switch(expr.operator.type){
static foreach(t, op; [ MINUS: "-", SLASH: "/", STAR: "*" ]) static foreach(t, op; [ PLUS: "+", MINUS: "-", SLASH: "/", STAR: "*", GREATER: ">", GREATER_EQUAL: ">=", LESS: "<", LESS_EQUAL: "<=" ]){
mixin(ctEval!(m(t, op, "dbl", "dblValue"))); case t:
static if(t == PLUS){
case PLUS: if(cast(LoxStr)left && cast(LoxStr)right)
if(left.isDbl && right.isDbl) return new LoxStr(cast(LoxStr)left ~ cast(LoxStr)right);
return TValue.dbl(left.dblValue + right.dblValue); version(LoxConcatNonStrings){
else if(left.isStr && right.isStr) if(cast(LoxStr)left || cast(LoxStr)right)
return TValue.str(left.strValue ~ right.strValue); return new LoxStr(left.toString ~ right.toString);
version(LoxConcatNonStrings){ }
if(left.isStr || right.isStr) }
return TValue.str(tvalueToString(left) ~ tvalueToString(right)); checkNumberOperand(expr.operator, left);
} checkNumberOperand(expr.operator, right);
checkNumberOperand(expr.operator, left); return (cast(LoxNum)left).opBinary!op(cast(LoxNum)right);
checkNumberOperand(expr.operator, right); }
static foreach(t, op; [ GREATER: ">", GREATER_EQUAL: ">=", LESS: "<", LESS_EQUAL: "<=" ])
mixin(ctEval!(m(t, op, "bln", "dblValue")));
case BANG_EQUAL: case BANG_EQUAL:
return TValue.bln(!isEqual(left, right)); return new LoxBool(!isEqual(left, right));
case EQUAL_EQUAL: case EQUAL_EQUAL:
return TValue.bln(isEqual(left, right)); return new LoxBool(isEqual(left, right));
default: default:
assert(0); assert(0);
} }
} }
TValue visit(Expr.Call expr){ LoxValue visit(Expr.Call expr){
TValue callee = evaluate(expr.callee); LoxValue callee = evaluate(expr.callee);
if(!callee.isCal) if(!cast(LoxCallable)callee)
throw new RuntimeError(expr.paren, "Can only call functions and classes."); throw new RuntimeError(expr.paren, "Can only call functions and classes.");
auto arguments = expr.arguments.map!(a => evaluate(a)); auto arguments = expr.arguments.map!(a => evaluate(a));
LoxCallable func = callee.calValue; LoxCallable func = cast(LoxCallable)callee;
if(arguments.length != func.arity()) if(arguments.length != func.arity())
throw new RuntimeError(expr.paren, "Expected " ~ func.arity().to!string ~ " arguments but got " ~ arguments.length.to!string ~ "."); throw new RuntimeError(expr.paren, "Expected " ~ func.arity().to!string ~ " arguments but got " ~ arguments.length.to!string ~ ".");
return func.call(this, arguments.array); 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); 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) if(const distance = expr in locals)
return environment.getAt(*distance, name.lexeme); return environment.getAt(*distance, name.lexeme);
else else
return globals.get(name); return globals.get(name);
} }
TValue visit(Expr.Assign expr){ LoxValue visit(Expr.Assign expr){
TValue value = evaluate(expr.value); LoxValue value = evaluate(expr.value);
if(const distance = expr in locals) if(const distance = expr in locals)
environment.assignAt(*distance, expr.name, value); environment.assignAt(*distance, expr.name, value);
else else

34
src/jlox/loxclass.d Normal file
View 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";
}

View file

@ -2,31 +2,44 @@ module jlox.loxfunction;
import std.conv; import std.conv;
import common.util;
import jlox.token; import jlox.token;
import jlox.stmt; import jlox.stmt;
import jlox.interpreter; import jlox.interpreter;
import jlox.environment; import jlox.environment;
import common.util; import jlox.loxinstance;
class LoxFunction : LoxCallable{ class LoxFunction : LoxCallable{
private Stmt.Function declaration; private Stmt.Function declaration;
private Environment closure; private Environment closure;
private const bool isInitialiser;
mixin defaultCtor; mixin defaultCtor;
invariant{ invariant{
assert(declaration && closure); 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; int arity() => declaration.params.length.to!int;
TValue call(Interpreter interpreter, TValue[] arguments){ LoxValue call(Interpreter interpreter, LoxValue[] arguments){
Environment environment = new Environment(closure); Environment environment = new Environment(closure);
foreach(i; 0 .. declaration.params.length) foreach(i; 0 .. declaration.params.length)
environment.define(declaration.params[i].lexeme, arguments[i]); environment.define(declaration.params[i].lexeme, arguments[i]);
try{ try{
interpreter.executeBlock(declaration.body, environment); interpreter.executeBlock(declaration.body, environment);
} catch(Return returnValue){ } 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
View 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;
}
}

View file

@ -47,7 +47,10 @@ static class Lox{
hadError = true; hadError = true;
} }
static void runtimeError(RuntimeError error){ 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; hadRuntimeError = true;
} }
@ -86,6 +89,8 @@ static class Lox{
} }
} }
extern(C) int isatty(int);
int main(string[] argv){ int main(string[] argv){
auto args = new Program("lox") auto args = new Program("lox")
.add(new Argument("path").optional.acceptsFiles) .add(new Argument("path").optional.acceptsFiles)
@ -96,12 +101,16 @@ int main(string[] argv){
stderr.writeln("Cant have both path and --cmd"); stderr.writeln("Cant have both path and --cmd");
return RetVal.other; return RetVal.other;
} }
if(auto cmd = args.option("command")) if(auto cmd = args.option("command")){
Lox.run(cmd); Lox.run(cmd);
else if(auto path = args.arg("path")) } else if(auto path = args.arg("path")){
Lox.runFile(path); Lox.runFile(path);
else } else {
Lox.runPrompt(); if(isatty(stdin.fileno))
Lox.runPrompt();
else
Lox.runFile("/dev/stdin");
}
} catch(MainException rte){ } catch(MainException rte){
return Lox.hadError ? RetVal.other : RetVal.runtime; return Lox.hadError ? RetVal.other : RetVal.runtime;
} }

View file

@ -83,7 +83,7 @@ class Parser{
private Stmt printStatement(){ private Stmt printStatement(){
version(LoxPrintMultiple){ version(LoxPrintMultiple){
Expr[] values; Expr[] values;
do { if(!check(TokenType.SEMICOLON)) do {
values ~= expression(); values ~= expression();
} while(match(TokenType.COMMA)); } while(match(TokenType.COMMA));
consume(TokenType.SEMICOLON, "Expect ';' after values."); consume(TokenType.SEMICOLON, "Expect ';' after values.");
@ -144,7 +144,7 @@ class Parser{
else else
initialiser = expressionStatement(); 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."); consume(TokenType.SEMICOLON, "Expect ';' after loop condition.");
Expr increment; Expr increment;
@ -197,8 +197,11 @@ class Parser{
if(match(TokenType.EQUAL)){ if(match(TokenType.EQUAL)){
Token equals = previous(); Token equals = previous();
Expr value = assignment(); Expr value = assignment();
if(auto v = cast(Expr.Variable)expr) if(auto v = cast(Expr.Variable)expr){
return new Expr.Assign(v.name, value); 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."); error(equals, "Invalid assignment target.");
} }
return expr; return expr;
@ -280,10 +283,14 @@ class Parser{
} }
Expr expr = primary(); Expr expr = primary();
while(true){ while(true){
if(match(TokenType.LEFT_PAREN)) if(match(TokenType.LEFT_PAREN)){
expr = finishCall(expr); 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; break;
}
} }
return expr; return expr;
} }
@ -291,13 +298,15 @@ class Parser{
if(match(TokenType.IDENTIFIER)) if(match(TokenType.IDENTIFIER))
return new Expr.Variable(previous()); return new Expr.Variable(previous());
if(match(TokenType.FALSE)) if(match(TokenType.FALSE))
return new Expr.Literal(TValue.bln(false)); return new Expr.Literal(new LoxBool(false));
if(match(TokenType.TRUE)) if(match(TokenType.TRUE))
return new Expr.Literal(TValue.bln(true)); return new Expr.Literal(new LoxBool(true));
if(match(TokenType.NIL)) if(match(TokenType.NIL))
return new Expr.Literal(tvalueNil); return new Expr.Literal(new LoxNil());
if(match(TokenType.NUMBER, TokenType.STRING)) if(match(TokenType.NUMBER, TokenType.STRING))
return new Expr.Literal(previous().literal); return new Expr.Literal(previous().literal);
if(match(TokenType.THIS))
return new Expr.This(previous());
if(match(TokenType.LEFT_PAREN)){ if(match(TokenType.LEFT_PAREN)){
Expr expr = expression(); Expr expr = expression();
consume(TokenType.RIGHT_PAREN, "Expect ')' after expression."); consume(TokenType.RIGHT_PAREN, "Expect ')' after expression.");
@ -313,12 +322,23 @@ class Parser{
consume(TokenType.SEMICOLON, "Expect ';' after variable declaration."); consume(TokenType.SEMICOLON, "Expect ';' after variable declaration.");
return new Stmt.Var(name, initialiser); 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(){ private Stmt declaration(){
try { try {
if(match(TokenType.FUN))
return fun("function");
if(match(TokenType.VAR)) if(match(TokenType.VAR))
return varDeclaration(); return varDeclaration();
if(match(TokenType.FUN))
return fun("function");
if(match(TokenType.CLASS))
return classDeclaration();
return statement(); return statement();
} catch(ParseError error){ } catch(ParseError error){
synchronise(); synchronise();

View file

@ -2,6 +2,7 @@ module jlox.resolver;
import std.container.slist; import std.container.slist;
import std.stdio; import std.stdio;
import std.algorithm;
import std.range : enumerate; import std.range : enumerate;
import jlox.main; import jlox.main;
@ -11,14 +12,15 @@ import jlox.expr;
import jlox.interpreter; import jlox.interpreter;
import common.util; import common.util;
private enum FunctionType { NONE, FUNCTION }
class Resolver : Stmt.Visitor!void, Expr.Visitor!void { class Resolver : Stmt.Visitor!void, Expr.Visitor!void {
private enum FunctionType{ NONE, FUNCTION, INITIALISER, METHOD }
private enum ClassType{ NONE, CLASS }
private Interpreter interpreter; private Interpreter interpreter;
this(Interpreter interpreter){ this(Interpreter interpreter){
this.interpreter = interpreter; this.interpreter = interpreter;
} }
private FunctionType currentFunction = FunctionType.NONE; private FunctionType currentFunction = FunctionType.NONE;
private ClassType currentClass = ClassType.NONE;
private SList!(bool[string]) scopes; private SList!(bool[string]) scopes;
private void resolve(Stmt stmt) => stmt.accept(this); private void resolve(Stmt stmt) => stmt.accept(this);
private void resolve(Expr expr) => expr.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); resolve(stmt.statements);
endScope(); 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){ void visit(Stmt.Var stmt){
declare(stmt.name); declare(stmt.name);
if(stmt.initialiser !is null) if(stmt.initialiser !is null)
@ -112,8 +131,11 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void {
void visit(Stmt.Return stmt){ void visit(Stmt.Return stmt){
if(currentFunction == FunctionType.NONE) if(currentFunction == FunctionType.NONE)
Lox.error(stmt.keyword, "Can't return from top-level code."); 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); resolve(stmt.value);
}
} }
void visit(Stmt.While stmt){ void visit(Stmt.While stmt){
resolve(stmt.condition); resolve(stmt.condition);
@ -128,6 +150,9 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void {
foreach(argument; expr.arguments) foreach(argument; expr.arguments)
resolve(argument); resolve(argument);
} }
void visit(Expr.Get expr){
resolve(expr.object);
}
void visit(Expr.Grouping expr){ void visit(Expr.Grouping expr){
resolve(expr.expression); resolve(expr.expression);
} }
@ -136,6 +161,17 @@ class Resolver : Stmt.Visitor!void, Expr.Visitor!void {
resolve(expr.left); resolve(expr.left);
resolve(expr.right); 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){ void visit(Expr.Unary expr){
resolve(expr.right); resolve(expr.right);
} }

View file

@ -30,7 +30,7 @@ class Scanner {
current++; current++;
return true; 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]; string text = source[start .. current];
tokens ~= new Token(type, text, literal, line); tokens ~= new Token(type, text, literal, line);
} }
@ -54,7 +54,7 @@ class Scanner {
} }
advance(); advance();
string value = source[start + 1 .. current -1]; string value = source[start + 1 .. current -1];
addToken(TokenType.STRING, TValue.str(value)); addToken(TokenType.STRING, new LoxStr(value));
} }
private void number(){ private void number(){
while(peek().isDigit) while(peek().isDigit)
@ -64,7 +64,7 @@ class Scanner {
while(peek().isDigit) while(peek().isDigit)
advance(); 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){ private TokenType keywords(string word){
with(TokenType) switch(word){ with(TokenType) switch(word){

View file

@ -12,6 +12,7 @@ import jlox.expr;
abstract class Stmt{ abstract class Stmt{
interface Visitor(R){ interface Visitor(R){
R visit(Block expr); R visit(Block expr);
R visit(Class expr);
R visit(Expression expr); R visit(Expression expr);
R visit(Function expr); R visit(Function expr);
R visit(If expr); R visit(If expr);
@ -33,6 +34,11 @@ abstract class Stmt{
Stmt[] statements; Stmt[] statements;
mixin defCtorAndAccept; mixin defCtorAndAccept;
} }
static class Class : typeof(this){
Token name;
Function[] methods;
mixin defCtorAndAccept;
}
static class Expression : typeof(this){ static class Expression : typeof(this){
Expr expression; Expr expression;
mixin defCtorAndAccept; mixin defCtorAndAccept;

View file

@ -1,48 +1,70 @@
module jlox.token; module jlox.token;
import std.stdio;
import std.conv; import std.conv;
import std.format : format;
import taggedalgebraic; import common.util;
import jlox.tokentype; import jlox.tokentype;
import jlox.interpreter; 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(); int arity();
TValue call(Interpreter interpreter, TValue[] arguments); LoxValue call(Interpreter interpreter, LoxValue[] arguments);
} }
private struct Value{ class LoxStr : LoxValue{
string str; immutable string str;
double dbl; alias str this;
bool bln; mixin defaultCtor;
LoxCallable cal; override string toString() const => str;
struct Nil{}
Nil nil;
} }
alias TValue = TaggedUnion!Value; class LoxBool : LoxValue{
immutable tvalueNil = Value.Nil(); immutable bool bln;
string tvalueToString(TValue val){ alias bln this;
final switch(val.kind){ mixin defaultCtor;
case TValue.Kind.str: override string toString() const => bln.to!string;
return val.strValue; }
case TValue.Kind.dbl: class LoxNum : LoxValue{
return val.dblValue.to!string; immutable double num;
case TValue.Kind.bln: alias num this;
return val.blnValue ? "true" : "false"; mixin defaultCtor;
case TValue.Kind.cal: override string toString() const => num.to!string;
return "<function>"; auto opBinary(string op)(const LoxNum rhs) const{
case TValue.Kind.nil: mixin("auto v = num", op, "rhs.num;");
return "nil"; 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{ class Token{
TokenType type; TokenType type;
string lexeme; string lexeme;
TValue literal; LoxValue literal;
int line; int line;
this(TokenType type, string lexeme, TValue literal, int line) { this(TokenType type, string lexeme, LoxValue literal, int line) {
this.type = type; this.type = type;
this.lexeme = lexeme; this.lexeme = lexeme;
this.literal = literal; this.literal = literal;

View file

@ -1,5 +1,6 @@
#!/bin/env rdmd #!/bin/env rdmd
import std.stdio;
import std.process; import std.process;
import std.concurrency; import std.concurrency;
import std.conv; import std.conv;
@ -7,18 +8,20 @@ import std.string, std.format;
import std.algorithm, std.range; import std.algorithm, std.range;
void main(){ void main(){
auto scheduler = new ThreadScheduler(); "./test/ops.lox".match("1\n2\n3\n4\n5\n6\n7\ntrue\nfalse\ntrue\ntrue\nhello, world\n");
scheduler.spawn((){ "./test/closure.lox".run.output.match("1\n2\n"); }); "./test/shortcircuit.lox".match("true\nAAAA!\nAAAA!\nAAAA?\n");
scheduler.spawn((){ "./test/scope.lox".run.output.match("global first first second first ".replace(' ', '\n').repeat(2).join("\n")); }); "./test/closure.lox".match("1\n2\n");
scheduler.spawn((){ "./test/fib_for.lox".run.output.match(fib(6765)); }); "./test/scope.lox".match("global first first second first ".replace(' ', '\n').repeat(2).join("\n"));
scheduler.spawn((){ "./test/fib_recursive.lox".run.output.match(fib(34)); }); "./test/fib_for.lox".match(fib(6765));
scheduler.spawn((){ "./test/fib_closure.lox".run.output.match(fib(34)); }); "./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"); }); "./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"); }); "./test/err/undefined_var.lox".shouldFail(RetVal.runtime, "Undefined variable");
scheduler.spawn((){ "./test/err/self_ref_vardecl.lox".shouldFail(RetVal.runtime, "Undefined variable"); }); "./test/err/self_ref_vardecl.lox".shouldFail(RetVal.runtime, "Undefined variable");
scheduler.spawn((){ "./test/err/invalid_syntax.lox".shouldFail(RetVal.other); }); "./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/global_scope_return.lox".shouldFail(RetVal.other, "Can't return from top-level code");
} }
enum RetVal{ enum RetVal{
@ -37,8 +40,9 @@ string fib(uint n){
return r; return r;
} }
auto run(string file) => [ "./lox", file ].execute; auto run(string file) => [ "./lox", file ].execute;
void match(string res, string correct){ void match(string file, string correct){
assert(res == correct, "Match failed\n-- Got --\n%s\n-- Expected --\n%s".format(res, 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){ void shouldFail(string file, int code = 1, string msg = null){
auto c = file.run; auto c = file.run;

21
test/class.lox Normal file
View 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
View 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
View file

@ -0,0 +1,12 @@
fun scream(extra){
extra = extra or "!";
print "AAAA" + extra;
}
print nil or true;
scream(false);
scream(nil);
scream("?");

View file

@ -1 +1,2 @@