Methods and Initializers 28

This commit is contained in:
nazrin 2025-06-14 14:50:40 +00:00
parent d9dc02b92f
commit 28b0c71be1
12 changed files with 233 additions and 28 deletions

1
.gitignore vendored
View file

@ -18,4 +18,5 @@ lox-test-*
.msc/
test/test.lox
.ccls-cache/
.ldc2_cache/

View file

@ -56,10 +56,13 @@ enum OpCode : ubyte{
@(OpColour("000", "200", "100")) Print,
@OpCall @(OpColour("250", "200", "250")) Call,
@(OpColour("250", "200", "250")) Invoke,
@(OpColour("250", "200", "250")) Closure,
@(OpColour("250", "200", "050")) CloseUpvalue,
@(OpColour("250", "190", "200")) Return,
@OpConst @(OpColour("050", "190", "200")) Class,
@OpConst @(OpColour("100", "190", "200")) Method,
}
struct Chunk{

View file

@ -15,6 +15,7 @@ import clox.container.varint;
Parser parser;
Compiler* currentCompiler;
ClassCompiler* currentClass;
struct Upvalue{
int index;
@ -23,7 +24,7 @@ struct Upvalue{
struct Compiler{
Compiler* enclosing;
Obj.Function* func;
enum FunctionType{ None, Function, Script }
enum FunctionType{ None, Function, Script, Method, Initialiser }
FunctionType ftype = FunctionType.Script;
struct Local{
Token name;
@ -48,7 +49,11 @@ struct Compiler{
Local local = Local();
local.depth = 0;
local.name.lexeme = "<>";
if(ftype != FunctionType.Function){
local.name.lexeme = "this";
} else {
local.name.lexeme = "<>";
}
locals ~= local;
}
Obj.Function* compile(){
@ -80,7 +85,11 @@ struct Compiler{
currentCompiler.currentChunk.write(b, parser.previous.line);
}
void emitReturn(){
emit(OpCode.Nil, OpCode.Return);
if(ftype == FunctionType.Initialiser){
emit(OpCode.GetLocal, ubyte(0), OpCode.Return);
} else {
emit(OpCode.Nil, OpCode.Return);
}
}
void emitConstant(Value value){
int c = makeConstant(value);
@ -92,6 +101,10 @@ struct Compiler{
}
}
struct ClassCompiler{
ClassCompiler* enclosing;
}
void markCompilerRoots(){
Compiler* compiler = currentCompiler;
while(compiler !is null){

View file

@ -70,6 +70,19 @@ private long closureInstruction(Chunk* chunk, long offset){
printf(" ");
return offset ;
}
private long invokeInstruction(Chunk* chunk, long offset){
enum c = getUDAs!(OpCode.Invoke, OpColour)[0];
ubyte len;
int constant = VarUint.read(&chunk.code[offset + 1], len);
offset += len;
int argCount = VarUint.read(&chunk.code[offset + 1], len);
offset += len;
printf(ctEval!(colour!("%-14s", c.r, c.g, c.b) ~ colour!(" %4d ", Colour.Pink)).ptr, "Invoke".ptr, constant);
long pl = chunk.constants[constant].print();
foreach(i; 0 .. 16-pl)
printf(" ");
return offset + 1;
}
void printHeader(Chunk* chunk, const char* name = "execution"){
printf(" ============== %s ==============\n", name);
@ -109,6 +122,8 @@ long disassembleInstruction(Chunk* chunk, long offset){
case e: return callInstruction!e(e.stringof, chunk, offset);
} else static if(e == OpCode.Closure){
case e: return closureInstruction(chunk, offset);
} else static if(e == OpCode.Invoke){
case e: return invokeInstruction(chunk, offset);
} else {
case e: return simpleInstruction!e(e.stringof, offset);
}

View file

@ -50,7 +50,7 @@ void collectGarbage(){
vm.nextGC = conf.nextGCGrowthFunc(vm.bytesAllocated);
}
}
void mark(O)(O* o) if(is(O == Obj) || __traits(hasMember, O, "obj")){
void mark(O)(O* o) if(is(O == Obj) || __traits(getMember, O, "obj").offsetof == 0){
Obj* object = cast(Obj*)o;
if(object is null || object.isMarked)
return;
@ -85,6 +85,7 @@ private void markRoots(){
mark(upvalue);
}
mark(vm.globals);
mark(vm.initString);
markCompilerRoots();
}
private void blacken(Obj* object){
@ -110,6 +111,7 @@ private void blacken(Obj* object){
break;
case Obj.Type.Class:
Obj.Class* cls = object.asClass;
mark(cls.methods);
mark(cls.name);
break;
case Obj.Type.Instance:
@ -117,6 +119,11 @@ private void blacken(Obj* object){
mark(ins.cls);
mark(ins.fields);
break;
case Obj.Type.BoundMethod:
Obj.BoundMethod* bm = object.asBoundMethod;
mark(bm.receiver);
mark(bm.method);
break;
case Obj.Type.None: assert(0);
}
}

View file

@ -101,6 +101,7 @@ void freeObject(Obj* object){
break;
case Obj.Type.Class:
Obj.Class* cls = cast(Obj.Class*)object;
cls.methods.free();
FREE(cls);
break;
case Obj.Type.Instance:
@ -108,6 +109,10 @@ void freeObject(Obj* object){
ins.fields.free();
FREE(ins);
break;
case Obj.Type.BoundMethod:
Obj.BoundMethod* bm = cast(Obj.BoundMethod*)object;
FREE(bm);
break;
case Obj.Type.None: assert(0);
}
}

View file

@ -3,6 +3,7 @@ module clox.obj;
import core.stdc.stdio : printf;
import core.stdc.string : memcpy;
debug import std.stdio : writeln;
import std.traits;
import clox.chunk;
import clox.container.table;
@ -13,7 +14,7 @@ import clox.gc : collectGarbage;
struct Obj{
enum Type{
None, String, Function, Closure, Upvalue, NativeFunction, Class, Instance
None, String, Function, Closure, Upvalue, NativeFunction, Class, Instance, BoundMethod
}
Type type;
bool isMarked;
@ -23,14 +24,9 @@ struct Obj{
auto as(Type type)() const pure{
static if(type != Type.None)
assert(this.type == type);
static if(type == Type.String) return cast(String*)&this;
static if(type == Type.Function) return cast(Function*)&this;
static if(type == Type.NativeFunction) return cast(NativeFunction*)&this;
static if(type == Type.Closure) return cast(Closure*)&this;
static if(type == Type.Upvalue) return cast(Upvalue*)&this;
static if(type == Type.Class) return cast(Class*)&this;
static if(type == Type.Instance) return cast(Instance*)&this;
static if(type == Type.None) return &this;
static foreach(t; EnumMembers!Type)
static if(type == t) return mixin("cast(", t.stringof, "*)&this");
}
int print(const char* strFmt = `"%s"`) const{
@ -47,6 +43,9 @@ struct Obj{
return printf("<%s/%d>", closure.func.name.chars.ptr, closure.func.arity);
case Type.Class: return printf("<class %s>", asClass.name.chars.ptr);
case Type.Instance: return printf("<instance %s>", asInstance.cls.name.chars.ptr);
case Type.BoundMethod:
Closure* closure = asBoundMethod.method;
return printf("<%s/%d>", closure.func.name.chars.ptr, closure.func.arity);
case Type.Upvalue:
case Type.None: assert(0);
}
@ -59,6 +58,7 @@ struct Obj{
Upvalue* asUpvalue() const pure => as!(Type.Upvalue);
Class* asClass() const pure => as!(Type.Class);
Instance* asInstance() const pure => as!(Type.Instance);
BoundMethod* asBoundMethod() const pure => as!(Type.BoundMethod);
private static T* allocateObject(T)(){
collectGarbage();
@ -180,8 +180,10 @@ struct Obj{
static enum myType = Type.Class;
Obj obj;
String* name;
Table!(String*, Closure*) methods;
static Class* create(String* name){
Class* cls = allocateObject!(typeof(this))();
cls.methods = typeof(cls.methods)(4);
cls.name = name;
return cls;
}
@ -200,6 +202,19 @@ struct Obj{
}
}
struct BoundMethod{
static enum myType = Type.BoundMethod;
Obj obj;
Value receiver;
Closure* method;
static BoundMethod* create(Value receiver, Closure* method){
BoundMethod* bm = allocateObject!(typeof(this))();
bm.receiver = receiver;
bm.method = method;
return bm;
}
}
bool opEquals(in ref Obj rhs) const pure{
if(rhs.type != type)
return false;
@ -210,6 +225,7 @@ struct Obj{
case Type.Closure: return &this is &rhs;
case Type.Class: return &this is &rhs;
case Type.Instance: return &this is &rhs;
case Type.BoundMethod: return &this is &rhs;
case Type.Upvalue:
case Type.None: assert(0);
}
@ -222,6 +238,7 @@ struct Obj{
case Type.Closure: return false;
case Type.Class: return false;
case Type.Instance: return false;
case Type.BoundMethod: return false;
case Type.Upvalue:
case Type.None: assert(0);
}
@ -238,6 +255,7 @@ struct Obj{
case Type.Upvalue: return "Upvalue";
case Type.Class: return "Class";
case Type.Instance: return "Instance";
case Type.BoundMethod: return "BoundMethod";
case Type.None: assert(0);
}
}

View file

@ -66,19 +66,41 @@ struct Parser{
void funDeclaration(){
int global = parseVariable("Expect function name");
markInitialised();
func();
func(Compiler.FunctionType.Function);
defineVariable(global);
}
void classDeclaration(){
consume(Token.Type.Identifier, "Expect class name.");
int nameConstant = identifierConstant(parser.previous);
Token className = previous;
int nameConstant = identifierConstant(previous);
declareVariable();
currentCompiler.emit(OpCode.Class, VarUint(nameConstant).bytes);
defineVariable(nameConstant);
ClassCompiler classCompiler;
classCompiler.enclosing = currentClass;
currentClass = &classCompiler;
scope(exit)
currentClass = currentClass.enclosing;
namedVariable(className, false);
consume(Token.Type.LeftBrace, "Expect '{' before class body.");
while(!check(Token.Type.RightBrace) && !check(Token.Type.EOF)){
method();
}
consume(Token.Type.RightBrace, "Expect '}' after class body.");
currentCompiler.emit(OpCode.Pop);
}
void method(){
consume(Token.Type.Identifier, "Expect method name.");
int constant = identifierConstant(previous);
Compiler.FunctionType type = Compiler.FunctionType.Method;
if(previous.lexeme == "init")
type = Compiler.FunctionType.Initialiser;
func(type);
currentCompiler.emit(OpCode.Method, VarUint(constant).bytes);
}
void declaration(){
if(match(Token.Type.Var))
@ -89,7 +111,7 @@ struct Parser{
classDeclaration();
else
statement();
if(parser.panicMode)
if(panicMode)
synchronise();
}
void statement(){
@ -122,6 +144,8 @@ struct Parser{
if(match(Token.Type.Semicolon)){
currentCompiler.emitReturn();
} else {
if(currentCompiler.ftype == Compiler.FunctionType.Initialiser)
error("Can't return a value from an initializer.");
expression();
consume(Token.Type.Semicolon, "Expect ';' after return value.");
currentCompiler.emit(OpCode.Return);
@ -206,9 +230,9 @@ struct Parser{
declaration();
consume(Token.Type.RightBrace, "Expect '}' after block.");
}
void func(){
void func(Compiler.FunctionType type){
Compiler compiler;
compiler.initialise(Compiler.FunctionType.Function);
compiler.initialise(type);
scope(exit)
compiler.free();
beginScope();
@ -266,7 +290,7 @@ struct Parser{
void declareVariable(){
if(currentCompiler.scopeDepth == 0)
return;
const Token* name = &parser.previous;
const Token* name = &previous;
foreach_reverse(const ref local; currentCompiler.locals){
if(local.depth != -1 && local.depth < currentCompiler.scopeDepth)
break;

View file

@ -99,10 +99,20 @@ private void dot(Compiler* compiler, bool canAssign){
if(canAssign && parser.match(Token.Type.Equal)){
parser.expression();
compiler.emit(OpCode.SetProp, name.bytes);
} else if(parser.match(Token.Type.LeftParen)){
auto argCount = VarUint(parser.argumentList());
compiler.emit(OpCode.Invoke, name.bytes, argCount.bytes);
} else {
compiler.emit(OpCode.GetProp, name.bytes);
}
}
private void this_(Compiler* compiler, bool _){
if(currentClass == null){
parser.error("Can't use 'this' outside of a class.");
return;
}
variable(compiler, false);
}
struct ParseRule{
@ -163,7 +173,7 @@ immutable ParseRule[Token.Type.max+1] rules = [
Token.Type.Print : ParseRule(null, null, Precedence.None),
Token.Type.Return : ParseRule(null, null, Precedence.None),
Token.Type.Super : ParseRule(null, null, Precedence.None),
Token.Type.This : ParseRule(null, null, Precedence.None),
Token.Type.This : ParseRule(&this_, null, Precedence.None),
Token.Type.True : ParseRule(&literal, null, Precedence.None),
Token.Type.Var : ParseRule(null, null, Precedence.None),
Token.Type.While : ParseRule(null, null, Precedence.None),

View file

@ -32,6 +32,7 @@ struct VM{
debug(printCode) bool printCode;
debug(traceExec) bool traceExec;
bool isREPL, dontRun;
Obj.String* initString;
enum InterpretResult{ Ok, CompileError, RuntimeError }
struct CallFrame{
Obj.Closure* closure;
@ -43,6 +44,7 @@ struct VM{
globals = typeof(globals)(8);
greyStack.initialise();
stackTop = &stack[0];
initString = Obj.String.copy("init");
defineNative("clock", (int argCount, Value* args){
import core.stdc.time;
@ -84,6 +86,7 @@ struct VM{
fputs("\n", stderr);
printStackTrace();
}
private Value peek(int distance = 0) => stackTop[-1 - distance];
InterpretResult run(){
CallFrame* frame = &frames[frameCount - 1];
ref ip() => frame.ip;
@ -104,7 +107,6 @@ struct VM{
}
Value readConstant() => chunk.constants[readVarUint()];
Obj.String* readString() => readConstant().asObj.asString;
Value peek(int distance = 0) => stackTop[-1 - distance];
bool checkBinaryType(alias type)() => peek(0).isType(type) && peek(1).isType(type);
bool checkSameType()() => peek(0).type == peek(1).type;
int binaryOp(string op, alias check, string checkMsg, alias pre)(){
@ -209,8 +211,10 @@ struct VM{
push(*value);
break;
}
runtimeError("Undefined property '%s'.", name.chars.ptr);
return InterpretResult.RuntimeError;
if(!bindMethod(ins.cls, name)){
return InterpretResult.RuntimeError;
}
break;
case OpCode.SetProp:
if(!peek(1).isType(Obj.Type.Instance)){
runtimeError("Only instances have fields.");
@ -304,9 +308,19 @@ struct VM{
push(result);
frame = &vm.frames[vm.frameCount - 1];
break;
case OpCode.Invoke:
Obj.String* method = readString();
int argCount = readVarUint();
if(!invoke(method, argCount))
return InterpretResult.RuntimeError;
frame = &vm.frames[vm.frameCount - 1];
break;
case OpCode.Class:
push(Value.from(Obj.Class.create(readString())));
break;
case OpCode.Method:
defineMethod(readString());
break;
}
}
assert(0);
@ -366,9 +380,20 @@ struct VM{
vm.stackTop -= argCount + 1;
push(result);
return true;
case Obj.Type.BoundMethod:
Obj.BoundMethod* bound = obj.asBoundMethod;
stackTop[-argCount - 1] = bound.receiver;
return call(bound.method, argCount);
return true;
case Obj.Type.Class:
Obj.Class* cls = obj.asClass;
vm.stackTop[-argCount - 1] = Value.from(Obj.Instance.create(cls));
if(Obj.Closure** initialiser = cls.methods[vm.initString]){
return call(*initialiser, argCount);
} else if(argCount != 0){
runtimeError("Expected 0 arguments but got %d.", argCount);
return false;
}
return true;
case Obj.Type.Instance: break;
case Obj.Type.String: break;
@ -403,5 +428,40 @@ struct VM{
pop();
}
void defineMethod(Obj.String* name){
Value method = peek(0);
Obj.Class* cls = peek(1).asObj.asClass;
cls.methods[name] = method.asObj.asClosure;
pop();
}
bool bindMethod(Obj.Class* cls, Obj.String* name){
if(Obj.Closure** method = cls.methods[name]){
Obj.BoundMethod* bound = Obj.BoundMethod.create(peek(0), *method);
pop();
push(Value.from(bound));
return true;
}
runtimeError("Undefined property '%s'.", name.chars.ptr);
return false;
}
bool invoke(Obj.String* name, int argCount){
Value receiver = peek(argCount);
if(!receiver.isType(Obj.Type.Instance)){
runtimeError("Only instances have methods.");
return false;
}
Obj.Instance* instance = receiver.asObj.asInstance;
if(Value* value = instance.fields[name]){
vm.stackTop[-argCount - 1] = *value;
return callValue(*value, argCount);
}
return invokeFromClass(instance.cls, name, argCount);
}
bool invokeFromClass(Obj.Class* cls, Obj.String* name, int argCount){
if(Obj.Closure** method = cls.methods[name])
return call(*method, argCount);
runtimeError("Undefined property '%s'.", name.chars.ptr);
return false;
}
}

View file

@ -7,7 +7,9 @@ import std.conv;
import std.string, std.format;
import std.algorithm, std.range;
void main(){
int failures;
int main(){
"./test/func.lox".match("-1\n6\n1\n");
"./test/bigsum.lox".match("125250\n");
@ -26,7 +28,8 @@ void main(){
"./test/fib_recursive.lox".match(fib(34));
"./test/fib_closure.lox".match(fib(34));
"./test/perverseclosure.lox".match("return from outer create inner closure value ".replace(" ", "\n"));
/* "./test/class.lox".match("The German chocolate cake is delicious!\n"); */
"./test/class.lox".match("The German chocolate cake is delicious!\n");
"./test/methods.lox".match("Enjoy your cup of coffee and chicory\nnot a method\n");
/* "./test/super.lox".match("Fry until golden brown.\nPipe full of custard and coat with chocolate.\nA method\n"); */
"./test/err/invalid_syntax.lox".shouldFail(RetVal.other);
@ -36,6 +39,8 @@ void main(){
"./test/err/global_scope_return.lox".shouldFail(RetVal.other, "Can't return from top-level code");
/* "./test/err/super_outside_class.lox".shouldFail(RetVal.other, "Can't use 'super' outside of a class"); */
/* "./test/err/super_without_superclass.lox".shouldFail(RetVal.other, "Can't use 'super' in a class with no superclass"); */
return failures;
}
enum RetVal{
@ -56,12 +61,27 @@ string fib(uint n){
auto run(string file, Config.Flags f = Config.Flags.stderrPassThrough) => [ "./lox", file ].execute(null, Config(f));
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));
if(res != correct){
stderr.writeln("Match %s failed\n-- Got --\n%s\n-- Expected --\n%s".format(file, res, correct));
failures++;
}
}
void shouldFail(string file, int code = 1, string msg = null){
auto c = file.run(Config.Flags.none);
assert(c.status == code, "Expected %s to fail with code %d but got %d".format(file, code, c.status));
assert(!msg || c.output.toLower.indexOf(msg.toLower) >= 0, "ShouldFail %s failed\n-- Got --\n%s\n-- Expected --\n%s".format(file, c.output, msg));
assert(c.output.indexOf("_Dmain") == -1, "ShouldFail %s got D exception\n%s".format(file, c.output));
if(!(c.status == code)){
stderr.writeln("Expected %s to fail with code %d but got %d".format(file, code, c.status));
failures++;
return;
}
if(!(!msg || c.output.toLower.indexOf(msg.toLower) >= 0)){
stderr.writeln("ShouldFail %s failed\n-- Got --\n%s\n-- Expected --\n%s".format(file, c.output, msg));
failures++;
return;
}
if(!(c.output.indexOf("_Dmain") == -1)){
stderr.writeln("ShouldFail %s got D exception\n%s".format(file, c.output));
failures++;
return;
}
}

29
test/methods.lox Normal file
View file

@ -0,0 +1,29 @@
class CoffeeMaker{
init(coffee){
this.coffee = coffee;
}
brew(){
print "Enjoy your cup of " + this.coffee;
// No reusing the grounds!
this.coffee = nil;
}
}
var maker = CoffeeMaker("coffee and chicory");
maker.brew();
class Oops{
init(){
fun f(){
print "not a method";
}
this.field = f;
}
}
var oops = Oops();
oops.field();