From 28b0c71be12f74b76ed3c2779e985db141bfc0c0 Mon Sep 17 00:00:00 2001 From: nazrin Date: Sat, 14 Jun 2025 14:50:40 +0000 Subject: [PATCH] Methods and Initializers 28 --- .gitignore | 1 + src/clox/chunk.d | 3 ++ src/clox/compiler.d | 19 +++++++++++-- src/clox/dbg.d | 15 ++++++++++ src/clox/gc.d | 9 +++++- src/clox/memory.d | 5 ++++ src/clox/obj.d | 34 ++++++++++++++++------ src/clox/parser.d | 36 +++++++++++++++++++---- src/clox/parserules.d | 12 +++++++- src/clox/vm.d | 66 +++++++++++++++++++++++++++++++++++++++++-- test/all.d | 32 +++++++++++++++++---- test/methods.lox | 29 +++++++++++++++++++ 12 files changed, 233 insertions(+), 28 deletions(-) create mode 100644 test/methods.lox diff --git a/.gitignore b/.gitignore index 04c7c2e..829345e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ lox-test-* .msc/ test/test.lox .ccls-cache/ +.ldc2_cache/ diff --git a/src/clox/chunk.d b/src/clox/chunk.d index 9e3ce62..b03c5e3 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -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{ diff --git a/src/clox/compiler.d b/src/clox/compiler.d index b367ed3..4c56209 100644 --- a/src/clox/compiler.d +++ b/src/clox/compiler.d @@ -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){ diff --git a/src/clox/dbg.d b/src/clox/dbg.d index f8ec38d..fdb1ae2 100644 --- a/src/clox/dbg.d +++ b/src/clox/dbg.d @@ -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); } diff --git a/src/clox/gc.d b/src/clox/gc.d index 3eee098..af301fa 100644 --- a/src/clox/gc.d +++ b/src/clox/gc.d @@ -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); } } diff --git a/src/clox/memory.d b/src/clox/memory.d index 3ae610f..0befdff 100644 --- a/src/clox/memory.d +++ b/src/clox/memory.d @@ -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); } } diff --git a/src/clox/obj.d b/src/clox/obj.d index 4025e46..fdb4fb0 100644 --- a/src/clox/obj.d +++ b/src/clox/obj.d @@ -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("", asClass.name.chars.ptr); case Type.Instance: return printf("", 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); } } diff --git a/src/clox/parser.d b/src/clox/parser.d index b2ddcdc..7c81e87 100644 --- a/src/clox/parser.d +++ b/src/clox/parser.d @@ -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; diff --git a/src/clox/parserules.d b/src/clox/parserules.d index c3a0103..63ca680 100644 --- a/src/clox/parserules.d +++ b/src/clox/parserules.d @@ -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), diff --git a/src/clox/vm.d b/src/clox/vm.d index cd5fb03..b00bb19 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -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; + } } diff --git a/test/all.d b/test/all.d index 0449809..b9b740c 100755 --- a/test/all.d +++ b/test/all.d @@ -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; + } } diff --git a/test/methods.lox b/test/methods.lox new file mode 100644 index 0000000..c109921 --- /dev/null +++ b/test/methods.lox @@ -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(); +