From d9dc02b92f4bc4d7650042e63f34cba9771bbf65 Mon Sep 17 00:00:00 2001 From: nazrin Date: Fri, 13 Jun 2025 02:29:46 +0000 Subject: [PATCH] Classes and Instances 27 --- src/clox/chunk.d | 4 +++ src/clox/conf.d | 11 ++++++ src/clox/container/dynarray.d | 5 --- src/clox/container/table.d | 68 +++++++++++++++++++++-------------- src/clox/gc.d | 61 +++++++++++++++++++++---------- src/clox/memory.d | 9 +++++ src/clox/memorydbg.d | 7 +++- src/clox/obj.d | 47 +++++++++++++++++++++--- src/clox/parser.d | 15 +++++++- src/clox/parserules.d | 15 ++++++-- src/clox/vm.d | 43 ++++++++++++++++++++-- test/all.d | 1 + test/fields.lox | 48 +++++++++++++++++++++++++ 13 files changed, 271 insertions(+), 63 deletions(-) create mode 100644 src/clox/conf.d create mode 100644 test/fields.lox diff --git a/src/clox/chunk.d b/src/clox/chunk.d index 95eb218..9e3ce62 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -36,6 +36,9 @@ enum OpCode : ubyte{ @OpJump @(OpColour("000", "200", "000")) JumpIfFalse, @OpJump @(OpColour("010", "255", "000")) Loop, + @OpConst @(OpColour("060", "010", "150")) GetProp, + @OpConst @(OpColour("060", "000", "150")) SetProp, + @(OpColour("255", "100", "100")) Equal, @(OpColour("255", "100", "100")) Greater, @(OpColour("255", "100", "100")) Less, @@ -56,6 +59,7 @@ enum OpCode : ubyte{ @(OpColour("250", "200", "250")) Closure, @(OpColour("250", "200", "050")) CloseUpvalue, @(OpColour("250", "190", "200")) Return, + @OpConst @(OpColour("050", "190", "200")) Class, } struct Chunk{ diff --git a/src/clox/conf.d b/src/clox/conf.d new file mode 100644 index 0000000..9159eac --- /dev/null +++ b/src/clox/conf.d @@ -0,0 +1,11 @@ + +module clox.conf; + +size_t tablecheckSizeFunc(size_t alive, size_t pl) => alive+1 < pl/2; +size_t tableGrowthFunc(size_t size) => size < 8 ? 8 : (cast(size_t)(cast(double)size * 1.5)); +size_t nextGCGrowthFunc(size_t alloced) => alloced * 2; + +enum vmDefaultNextGC = 1024 * 4; +enum vmStackSize = 1024; +enum vmNCallFrames = 256; + diff --git a/src/clox/container/dynarray.d b/src/clox/container/dynarray.d index a562ec8..12c6d17 100644 --- a/src/clox/container/dynarray.d +++ b/src/clox/container/dynarray.d @@ -46,11 +46,6 @@ struct DynArray(T){ assert(count > 0); return ptr[--_count]; } - void reset(){ - _count = 0; - debug foreach(ref item; this[]) - item = T.init; - } void free(){ if(ptr) FREE_ARRAY!T(ptr, capacity); diff --git a/src/clox/container/table.d b/src/clox/container/table.d index 2958488..6cca1b0 100644 --- a/src/clox/container/table.d +++ b/src/clox/container/table.d @@ -1,15 +1,16 @@ module clox.container.table; import core.stdc.stdlib : calloc, cfree = free; -import core.stdc.string; import std.algorithm : min, max; import std.traits : isArray; +debug import std.stdio : writeln; import clox.memory; import clox.memorydbg; import clox.obj; import clox.value; import clox.container.dynarray; +import conf = clox.conf; struct Table(K, V){ alias Hash = uint; @@ -21,6 +22,7 @@ struct Table(K, V){ bool isAlive() const => state == State.Alive; bool isFree() const => state != State.Alive; void remove(){ + this = Entry.init; state = State.Dead; } } @@ -44,9 +46,11 @@ struct Table(K, V){ cfree(cast(void*)p.ptr); } private void checkResize(){ - if(alive+1 < pool.length/2) + if(conf.tablecheckSizeFunc(alive, pool.length)) return; - size_t newSize = pool.length * 2; + rehash(conf.tableGrowthFunc(pool.length)); + } + private void rehash(in size_t newSize){ auto newTable = Table!(K, V)(newSize); foreach(Entry entry; pool){ if(entry.isAlive) @@ -81,14 +85,15 @@ struct Table(K, V){ Hash hash = getHash(key); size_t index = hash % pool.length; while(true){ - const entry = pool[index]; + const Entry* entry = &pool[index]; if(entry.isAlive && entry.key == key) break; - if(entry.isFree) + if(entry.isFree){ + alive++; break; + } index = (index+1) % pool.length; } - alive++; pool[index] = Entry(cast(K)key, val); } V* opIndex(in K key){ @@ -97,21 +102,26 @@ struct Table(K, V){ return &entry.val; return null; } - void remove(in K key){ + bool remove(in K key){ Entry* entry = find(key); if(entry){ - entry.remove(); - alive--; + remove(entry); + return true; } + return false; + } + void remove(Entry* entry){ + assert(entry); + entry.remove(); + alive--; } } -void removeWhite(Table!(char[], Obj.String*) table){ +void removeWhite(ref Table!(char[], Obj.String*) table){ + import std.stdio, std.algorithm; foreach(ref entry; table.pool){ - if(entry.state == table.State.Alive && !entry.val.obj.isMarked){ - entry.remove(); - table.alive--; - } + if(entry.state == table.State.Alive && !entry.val.obj.isMarked) + table.remove(&entry); } } @@ -147,20 +157,24 @@ unittest{ assert(tbl["hello"] == null); } - tbl.remove("l"); - tbl["l"] = 3; - tbl["m"] = 2; - tbl["s"] = 1; - assert(*tbl["l"] == 3); - assert(*tbl["m"] == 2); - assert(*tbl["s"] == 1); + foreach(i; 0 .. 3){ + tbl.remove("l"); + tbl["l"] = 3; + tbl["m"] = 2; + tbl["s"] = 1; + assert(*tbl["l"] == 3); + assert(*tbl["m"] == 2); + assert(*tbl["s"] == 1); - tbl["m"] = 20; - tbl["l"] = 30; - tbl["s"] = 10; - assert(*tbl["m"] == 20); - assert(*tbl["l"] == 30); - assert(*tbl["s"] == 10); + tbl["m"] = 20; + tbl["l"] = 30; + tbl["s"] = 10; + assert(*tbl["m"] == 20); + assert(*tbl["l"] == 30); + assert(*tbl["s"] == 10); + if(i & 1) + tbl.remove("m"); + } import std.file, std.algorithm, std.array, std.range; auto words = readText("/usr/share/dict/words").splitter("\n").take(1000); diff --git a/src/clox/gc.d b/src/clox/gc.d index c6b7914..3eee098 100644 --- a/src/clox/gc.d +++ b/src/clox/gc.d @@ -12,6 +12,7 @@ import clox.value; import clox.vm; import clox.container.dynarray; import clox.container.table; +import conf = clox.conf; void collectGarbage(){ debug(disableGC) @@ -34,14 +35,23 @@ void collectGarbage(){ markRoots(); traceReferences(); vm.strings.removeWhite(); - auto sweepStats = sweep(); - debug(logGC) - printf(colour!(" -- GC end %zu/%zu\n", Colour.Yellow).ptr, sweepStats.collected, sweepStats.total); + debug(memTrace){ + import clox.memorydbg : totalGCScanned, totalGCollected, totalGCollections; + auto sweepStats = sweep(); + totalGCollections++; + totalGCollected += sweepStats.collected; + totalGCScanned += sweepStats.total; + debug(logGC) + printf(colour!(" -- GC end %zu/%zu\n", Colour.Yellow).ptr, sweepStats.collected, sweepStats.total); + } else { + sweep(); + } debug(stressGC) {} else { - vm.nextGC = cast(size_t)(vm.bytesAllocated * 2); + vm.nextGC = conf.nextGCGrowthFunc(vm.bytesAllocated); } } -void mark(Obj* object){ +void mark(O)(O* o) if(is(O == Obj) || __traits(hasMember, O, "obj")){ + Obj* object = cast(Obj*)o; if(object is null || object.isMarked) return; debug(logGC) @@ -53,14 +63,14 @@ void mark(Value value){ if(value.isType(Value.Type.Obj)) mark(value.asObj); } -void mark(Table!(Obj.String*, Value) table){ - foreach(entry; table.pool){ - mark(cast(Obj*)entry.key); +void mark(K, V)(ref Table!(K, V) table){ + foreach(ref entry; table.pool){ + mark(entry.key); mark(entry.val); } } -void mark(T)(DynArray!T darray){ - foreach(item; darray[]){ +void mark(T)(ref DynArray!T darray){ + foreach(ref item; darray[]){ mark(item); } } @@ -69,10 +79,10 @@ private void markRoots(){ mark(*slot); } foreach(frame; vm.frames[0 .. vm.frameCount]){ - mark(cast(Obj*)frame.closure); + mark(frame.closure); } for(Obj.Upvalue* upvalue = vm.openUpvalues; upvalue !is null; upvalue = upvalue.next){ - mark(cast(Obj*)upvalue); + mark(upvalue); } mark(vm.globals); markCompilerRoots(); @@ -89,14 +99,23 @@ private void blacken(Obj* object){ break; case Obj.Type.Function: Obj.Function* func = object.asFunc; - mark(cast(Obj*)func.name); + mark(func.name); mark(func.chunk.constants); break; case Obj.Type.Closure: Obj.Closure* closure = object.asClosure; - mark(cast(Obj*)closure.func); + mark(closure.func); foreach(upvalue; closure.upvalues) - mark(cast(Obj*)upvalue); + mark(upvalue); + break; + case Obj.Type.Class: + Obj.Class* cls = object.asClass; + mark(cls.name); + break; + case Obj.Type.Instance: + Obj.Instance* ins = object.asInstance; + mark(ins.cls); + mark(ins.fields); break; case Obj.Type.None: assert(0); } @@ -108,11 +127,13 @@ private void traceReferences(){ } } private auto sweep(){ - size_t nCollected, total; + debug(memTrace) + size_t nCollected, total; Obj* previous = null; Obj* object = vm.objects; while(object !is null){ - total++; + debug(memTrace) + total++; if(object.isMarked){ object.isMarked = false; previous = object; @@ -126,9 +147,11 @@ private auto sweep(){ vm.objects = object; } freeObject(unreached); - nCollected++; + debug(memTrace) + nCollected++; } } - return tuple!("collected", "total")(nCollected, total); + debug(memTrace) + return tuple!("collected", "total")(nCollected, total); } diff --git a/src/clox/memory.d b/src/clox/memory.d index dcec163..3ae610f 100644 --- a/src/clox/memory.d +++ b/src/clox/memory.d @@ -99,6 +99,15 @@ void freeObject(Obj* object){ Obj.NativeFunction* func = cast(Obj.NativeFunction*)object; FREE(func); break; + case Obj.Type.Class: + Obj.Class* cls = cast(Obj.Class*)object; + FREE(cls); + break; + case Obj.Type.Instance: + Obj.Instance* ins = cast(Obj.Instance*)object; + ins.fields.free(); + FREE(ins); + break; case Obj.Type.None: assert(0); } } diff --git a/src/clox/memorydbg.d b/src/clox/memorydbg.d index 8ab7813..65786ea 100644 --- a/src/clox/memorydbg.d +++ b/src/clox/memorydbg.d @@ -71,8 +71,13 @@ debug(memTrace): version(D_BetterC) {} else { size_t totalAllocs, totalReallocs, totalFrees; size_t totalAllocBytes, totalFreedBytes; + size_t totalGCollections, totalGCScanned, totalGCollected; static ~this(){ - stderr.writefln(colour!("Allocs: %d (%,d bytes), Reallocs: %d, Frees: %d (%,d bytes), Peak: %,d bytes", Colour.Black), totalAllocs, totalAllocBytes, totalReallocs, totalFrees, totalFreedBytes, peakMemoryUsage); + stderr.writefln( + colour!("Allocs: %d (%,d bytes), Reallocs: %d, Frees: %d (%,d bytes), Peak: %,d bytes\nTotal GCs: %,d, Collected: %,d, Scanned: %,d", Colour.Black), + totalAllocs, totalAllocBytes, totalReallocs, totalFrees, totalFreedBytes, peakMemoryUsage, + totalGCollections, totalGCollected, totalGCScanned, + ); if(totalAllocBytes > totalFreedBytes){ stderr.writefln("Leaked %d bytes!".lightRed.to!string, totalAllocBytes - totalFreedBytes); diff --git a/src/clox/obj.d b/src/clox/obj.d index 3e2049a..4025e46 100644 --- a/src/clox/obj.d +++ b/src/clox/obj.d @@ -2,6 +2,7 @@ module clox.obj; import core.stdc.stdio : printf; import core.stdc.string : memcpy; +debug import std.stdio : writeln; import clox.chunk; import clox.container.table; @@ -12,7 +13,7 @@ import clox.gc : collectGarbage; struct Obj{ enum Type{ - None, String, Function, Closure, Upvalue, NativeFunction + None, String, Function, Closure, Upvalue, NativeFunction, Class, Instance } Type type; bool isMarked; @@ -27,6 +28,8 @@ struct Obj{ 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; } @@ -42,6 +45,8 @@ struct Obj{ case Type.Closure: Closure* closure = asClosure; 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.Upvalue: case Type.None: assert(0); } @@ -52,6 +57,8 @@ struct Obj{ Closure* asClosure() const pure => as!(Type.Closure); NativeFunction* asNativeFunc() const pure => as!(Type.NativeFunction); Upvalue* asUpvalue() const pure => as!(Type.Upvalue); + Class* asClass() const pure => as!(Type.Class); + Instance* asInstance() const pure => as!(Type.Instance); private static T* allocateObject(T)(){ collectGarbage(); @@ -163,20 +170,46 @@ struct Obj{ Obj obj; Sig func; static NativeFunction* create(Sig func){ - Obj.NativeFunction* native = allocateObject!(typeof(this))(); + NativeFunction* native = allocateObject!(typeof(this))(); native.func = func; return native; } } + struct Class{ + static enum myType = Type.Class; + Obj obj; + String* name; + static Class* create(String* name){ + Class* cls = allocateObject!(typeof(this))(); + cls.name = name; + return cls; + } + } + + struct Instance{ + static enum myType = Type.Instance; + Obj obj; + Class* cls; + Table!(String*, Value) fields; + static Instance* create(Class* cls){ + Instance* ins = allocateObject!(typeof(this))(); + ins.cls = cls; + ins.fields = typeof(ins.fields)(2); + return ins; + } + } + bool opEquals(in ref Obj rhs) const pure{ if(rhs.type != type) return false; final switch(type){ case Type.String: return asString.chars.ptr is rhs.asString.chars.ptr; - case Type.Function: return &this == &rhs; - case Type.NativeFunction: return &this == &rhs; - case Type.Closure: return &this == &rhs; + case Type.Function: return &this is &rhs; + case Type.NativeFunction: return &this is &rhs; + case Type.Closure: return &this is &rhs; + case Type.Class: return &this is &rhs; + case Type.Instance: return &this is &rhs; case Type.Upvalue: case Type.None: assert(0); } @@ -187,6 +220,8 @@ struct Obj{ case Type.Function: return false; case Type.NativeFunction: return false; case Type.Closure: return false; + case Type.Class: return false; + case Type.Instance: return false; case Type.Upvalue: case Type.None: assert(0); } @@ -201,6 +236,8 @@ struct Obj{ case Type.NativeFunction: return "NativeFunction"; case Type.Closure: return "Closure"; case Type.Upvalue: return "Upvalue"; + case Type.Class: return "Class"; + case Type.Instance: return "Instance"; case Type.None: assert(0); } } diff --git a/src/clox/parser.d b/src/clox/parser.d index 7f6ddf6..b2ddcdc 100644 --- a/src/clox/parser.d +++ b/src/clox/parser.d @@ -69,11 +69,24 @@ struct Parser{ func(); defineVariable(global); } + void classDeclaration(){ + consume(Token.Type.Identifier, "Expect class name."); + int nameConstant = identifierConstant(parser.previous); + declareVariable(); + + currentCompiler.emit(OpCode.Class, VarUint(nameConstant).bytes); + defineVariable(nameConstant); + + consume(Token.Type.LeftBrace, "Expect '{' before class body."); + consume(Token.Type.RightBrace, "Expect '}' after class body."); + } void declaration(){ if(match(Token.Type.Var)) varDeclaration(); else if(match(Token.Type.Fun)) funDeclaration(); + else if(match(Token.Type.Class)) + classDeclaration(); else statement(); if(parser.panicMode) @@ -369,7 +382,7 @@ struct Parser{ while(precedence <= ParseRule.get(current.type).precedence){ advance(); ParseFn infixRule = ParseRule.get(previous.type).infix; - infixRule(currentCompiler); + infixRule(currentCompiler, canAssign); } if(canAssign && match(Token.Type.Equal)) error("Invalid assignment target."); diff --git a/src/clox/parserules.d b/src/clox/parserules.d index 486b045..c3a0103 100644 --- a/src/clox/parserules.d +++ b/src/clox/parserules.d @@ -8,7 +8,7 @@ import clox.value; import clox.obj; import clox.container.varint; -alias ParseFn = void function(Compiler* compiler, bool canAssign = false); +alias ParseFn = void function(Compiler* compiler, bool canAssign); private void grouping(Compiler* compiler, bool _){ parser.expression(); @@ -92,6 +92,17 @@ private void call(Compiler* compiler, bool _){ VarUint argCount = VarUint(parser.argumentList()); compiler.emit(OpCode.Call, argCount.bytes); } +private void dot(Compiler* compiler, bool canAssign){ + parser.consume(Token.Type.Identifier, "Expect property name after '.'."); + auto name = VarUint(parser.identifierConstant(parser.previous)); + + if(canAssign && parser.match(Token.Type.Equal)){ + parser.expression(); + compiler.emit(OpCode.SetProp, name.bytes); + } else { + compiler.emit(OpCode.GetProp, name.bytes); + } +} struct ParseRule{ @@ -123,7 +134,7 @@ immutable ParseRule[Token.Type.max+1] rules = [ Token.Type.LeftBrace : ParseRule(null, null, Precedence.None), Token.Type.RightBrace : ParseRule(null, null, Precedence.None), Token.Type.Comma : ParseRule(null, null, Precedence.None), - Token.Type.Dot : ParseRule(null, null, Precedence.None), + Token.Type.Dot : ParseRule(null, &dot, Precedence.Call), Token.Type.Minus : ParseRule(&unary, &binary, Precedence.Term), Token.Type.Plus : ParseRule(null, &binary, Precedence.Term), Token.Type.Semicolon : ParseRule(null, null, Precedence.None), diff --git a/src/clox/vm.d b/src/clox/vm.d index 8aca37f..cd5fb03 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -1,6 +1,7 @@ module clox.vm; import core.stdc.stdio; +debug import std.stdio : writeln; import clox.chunk; import clox.compiler; @@ -13,20 +14,21 @@ import clox.value; import clox.container.dynarray; import clox.container.table; import clox.container.varint; +import conf = clox.conf; VM vm; struct VM{ - Value[1024] stack; + Value[conf.vmStackSize] stack; Value* stackTop; - CallFrame[256] frames; + CallFrame[conf.vmNCallFrames] frames; uint frameCount; Table!(Obj.String*, Value) globals; Table!(char[], Obj.String*) strings; Obj.Upvalue* openUpvalues; Obj* objects; DynArray!(Obj*) greyStack; - size_t bytesAllocated, nextGC = 1024 * 4; + size_t bytesAllocated, nextGC = conf.vmDefaultNextGC; debug(printCode) bool printCode; debug(traceExec) bool traceExec; bool isREPL, dontRun; @@ -194,6 +196,33 @@ struct VM{ globals[name] = peek(0); break; + case OpCode.GetProp: + if(!peek(0).isType(Obj.Type.Instance)){ + runtimeError("Only instances have properties."); + return InterpretResult.RuntimeError; + } + Obj.Instance* ins = peek(0).asObj.asInstance; + Obj.String* name = readString(); + Value* value = ins.fields[name]; + if(value !is null){ + pop(); // Instance + push(*value); + break; + } + runtimeError("Undefined property '%s'.", name.chars.ptr); + return InterpretResult.RuntimeError; + case OpCode.SetProp: + if(!peek(1).isType(Obj.Type.Instance)){ + runtimeError("Only instances have fields."); + return InterpretResult.RuntimeError; + } + Obj.Instance* ins = peek(1).asObj.asInstance; + ins.fields[readString()] = peek(0); + Value value = pop(); + pop(); + push(value); + break; + static foreach(k, op; [ OpCode.Equal: "==", OpCode.NotEqual: "!=" ]){ case k: binaryOp!(op, true, null, Value.Type.None); @@ -275,6 +304,9 @@ struct VM{ push(result); frame = &vm.frames[vm.frameCount - 1]; break; + case OpCode.Class: + push(Value.from(Obj.Class.create(readString()))); + break; } } assert(0); @@ -334,6 +366,11 @@ struct VM{ vm.stackTop -= argCount + 1; push(result); return true; + case Obj.Type.Class: + Obj.Class* cls = obj.asClass; + vm.stackTop[-argCount - 1] = Value.from(Obj.Instance.create(cls)); + return true; + case Obj.Type.Instance: break; case Obj.Type.String: break; case Obj.Type.Function: case Obj.Type.Upvalue: diff --git a/test/all.d b/test/all.d index 0ad3039..0449809 100755 --- a/test/all.d +++ b/test/all.d @@ -16,6 +16,7 @@ void main(){ "./test/for.lox".match("1 2 1 2 3 ".replace(' ', '\n')); "./test/ifelse.lox".match("a1 b2 c3 ".replace(' ', '\n')); "./test/while.lox".match("1 2 3 2 1 0 1 1 2 3 2 1 0 1 ".replace(' ', '\n')); + "./test/fields.lox".match("0 10 ".replace(' ', '\n')); "./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"); diff --git a/test/fields.lox b/test/fields.lox new file mode 100644 index 0000000..24cd593 --- /dev/null +++ b/test/fields.lox @@ -0,0 +1,48 @@ + +class Obj{} + +var p = Obj(); +p.length = 0; +p.next = nil; + +fun append(p, item){ + p.length = p.length + 1; + var n = Obj(); + n.next = p.next; + p.next = n; +} +fun walk(p){ + class Res{} + var n = p; + var d = 0; + while(n.next){ + n = n.next; + d = d + 1; + } + var r = Res(); + r.item = n; + r.depth = d; + return r; +} +fun max(a, b){ + if(a > b) + return a; + return b; +} + +var sum = 0; +var maxLength = 0; + +for(var i = 0; i < 10; i = i + 1){ + append(p, Obj()); + sum = sum + p.length; + var res = walk(p); + sum = sum - res.depth; + maxLength = max(maxLength, p.length); +} + + +print sum; +print maxLength; + +