Classes and Instances 27

This commit is contained in:
nazrin 2025-06-13 02:29:46 +00:00
parent 6d5dff6e3d
commit d9dc02b92f
13 changed files with 271 additions and 63 deletions

View file

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

11
src/clox/conf.d Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("<class %s>", asClass.name.chars.ptr);
case Type.Instance: return printf("<instance %s>", 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);
}
}

View file

@ -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.");

View file

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

View file

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

View file

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

48
test/fields.lox Normal file
View file

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