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"
license "MPL-2.0"
dependency "commandr" version="~>1.1.0"
dependency "taggedalgebraic" version="~>0.11.23"
targetType "executable"
buildRequirements "requireBoundsCheck" "requireContracts"

View file

@ -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;
}
}

View file

@ -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); */
/* } */
/* } */

View file

@ -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
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 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
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;
}
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;
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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){

View file

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

View file

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

View file

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