From a7b7348f6188dc4f40606322f321b9a7305fd4e7 Mon Sep 17 00:00:00 2001 From: nazrin Date: Sat, 7 Jun 2025 04:09:48 +0000 Subject: [PATCH] Hash Tables 20 --- src/clox/chunk.d | 123 +++++++++++------------- src/clox/compiler.d | 30 +++--- src/clox/container/int24.d | 26 ----- src/clox/container/rle.d | 62 ------------ src/clox/container/stack.d | 4 +- src/clox/container/table.d | 176 ++++++++++++++++++++++++++++++++++ src/clox/container/varint.d | 73 -------------- src/clox/container/vartype.d | 73 -------------- src/clox/dbg.d | 74 ++++++--------- src/clox/emitter.d | 50 +++------- src/clox/main.d | 90 +++++++++++------- src/clox/{mem.d => memory.d} | 150 +++++++++++++---------------- src/clox/obj.d | 99 +++++++++++++++++++ src/clox/object.d | 79 ---------------- src/clox/parser.d | 73 ++++++++------ src/clox/parserules.d | 32 ++++--- src/clox/scanner.d | 124 ++++++++++++------------ src/clox/util.d | 61 +++++++++--- src/clox/value.d | 128 +++++++++++++++++-------- src/clox/vm.d | 178 ++++++++++++++++++----------------- 20 files changed, 868 insertions(+), 837 deletions(-) delete mode 100644 src/clox/container/int24.d delete mode 100644 src/clox/container/rle.d create mode 100644 src/clox/container/table.d delete mode 100644 src/clox/container/varint.d delete mode 100644 src/clox/container/vartype.d rename src/clox/{mem.d => memory.d} (52%) create mode 100644 src/clox/obj.d delete mode 100644 src/clox/object.d diff --git a/src/clox/chunk.d b/src/clox/chunk.d index cbcde8b..d473e93 100644 --- a/src/clox/chunk.d +++ b/src/clox/chunk.d @@ -1,83 +1,68 @@ module clox.chunk; -import std.container.array; -import std.stdio; -import std.algorithm; - +import clox.memory; import clox.value; -import clox.object; -import clox.container.rle; -import clox.container.int24; -enum SimpleOp; -enum LogicOp; -enum ValueOp; -enum ArithOp; -enum CompOp; -enum OpCode : ubyte{ - Constant, - @ValueOp Nil, - @ValueOp True, - @ValueOp False, - - @(SimpleOp, CompOp) Equal, - @(SimpleOp, CompOp) Greater, - @(SimpleOp, CompOp) Less, - @(SimpleOp, CompOp) NotEqual, - @(SimpleOp, CompOp) GreaterEqual, - @(SimpleOp, CompOp) LessEqual, - - @(SimpleOp, ArithOp) Add, - @(SimpleOp, ArithOp) Subtract, - @(SimpleOp, ArithOp) Multiply, - @(SimpleOp, ArithOp) Divide, - - @(SimpleOp, LogicOp) Not, - @(SimpleOp, LogicOp) Negate, - @(SimpleOp) Return, +struct OpColour{ + string r, g, b; +} + +enum OpCode : ubyte{ + @(OpColour("200", "200", "100")) Constant, + @(OpColour("255", "200", "100")) Nil, + @(OpColour("255", "200", "100")) True, + @(OpColour("255", "200", "100")) False, + + @(OpColour("255", "100", "100")) Equal, + @(OpColour("255", "100", "100")) Greater, + @(OpColour("255", "100", "100")) Less, + @(OpColour("255", "100", "100")) NotEqual, + @(OpColour("255", "100", "100")) GreaterEqual, + @(OpColour("255", "100", "100")) LessEqual, + + @(OpColour("100", "100", "255")) Add, + @(OpColour("100", "100", "255")) Subtract, + @(OpColour("100", "100", "255")) Multiply, + @(OpColour("100", "100", "255")) Divide, + + @(OpColour("200", "100", "100")) Not, + @(OpColour("100", "100", "200")) Negate, + @(OpColour("000", "200", "100")) Return, } -import std.traits: hasUDA; -bool isSimpleOp(alias op)() => hasUDA!(op, SimpleOp); -bool isValueOp(alias op)() => hasUDA!(op, ValueOp); -bool isLogicOp(alias op)() => hasUDA!(op, LogicOp); -bool isCompOp(alias op)() => hasUDA!(op, CompOp); -bool isArithOp(alias op)() => hasUDA!(op, ArithOp); -bool isSize1Op(alias op)() => isSimpleOp!op || isValueOp!op; -static assert( isSimpleOp!(OpCode.Equal) ); -static assert( !isSimpleOp!(OpCode.Constant) ); struct Chunk{ - Array!ubyte code; - Rle!(Uint24, ubyte) lines; - Array!Value constants; - string name; - ~this(){ - stderr.writeln("Deallocing chunk ", name); - foreach(value; constants[].filter!isObj) - value.getObj.freeObject(); + uint count; + uint capacity; + ubyte* code; + uint* lines; + ValueArray constants; + void initialise(){ + count = 0; + capacity = 0; + code = null; + lines = null; + constants.initialise(); } - uint addConstant(in Value value) @nogc nothrow { - long index; - switch(value.type){ - case Value.Type.Str: - index = constants[].countUntil!(v => v.isStr && v.getStr.data == value.getStr.data); - break; - default: - index = constants[].countUntil(value); + void write(ubyte b, uint line = 0){ + if(capacity < count + 1){ + uint oldCapacity = capacity; + capacity = GROW_CAPACITY(oldCapacity); + code = GROW_ARRAY!ubyte(code, oldCapacity, capacity); + lines = GROW_ARRAY!uint(lines, oldCapacity, capacity); } - if(index >= 0) - return cast(uint)index; - constants ~= value; - return cast(uint)((constants.length) - 1); + code[count] = b; + lines[count] = line; + count++; } - void write(ubyte b, uint line = 0) @nogc nothrow { - ubyte[1] data = [ b ]; - this.write(data, line); + int addConstant(Value value){ + constants.write(value); + return constants.count - 1; } - void write(ubyte[] b, uint line = 0) @nogc nothrow { - code ~= b; - foreach(i; 0 .. b.length) - lines ~= Uint24(line); // TODO could be done without a loop + void free(){ + FREE_ARRAY!ubyte(code, capacity); + FREE_ARRAY!uint(lines, capacity); + constants.free(); + initialise(); } } diff --git a/src/clox/compiler.d b/src/clox/compiler.d index a19942d..8edc60b 100644 --- a/src/clox/compiler.d +++ b/src/clox/compiler.d @@ -1,30 +1,38 @@ module clox.compiler; -import std.stdio; +import core.stdc.stdio; -import clox.scanner, clox.parser, clox.emitter; +import clox.scanner; +import clox.parser; import clox.chunk; -import clox.value; -import clox.util; -import clox.parserules; +import clox.emitter; import clox.dbg; struct Compiler{ Scanner scanner; Parser parser; Emitter emitter; - bool compile(string source, Chunk* chunk){ - scanner = Scanner(source); - parser = Parser(&this); - emitter = Emitter(&this, chunk); + private Chunk* compilingChunk; + Chunk* currentChunk() => compilingChunk; + bool compile(const(char)* source, Chunk* chunk){ + scanner.initialise(source); + parser.initialise(&this); + emitter.initialise(&this); + compilingChunk = chunk; parser.advance(); parser.expression(); parser.consume(Token.Type.EOF, "Expect end of expression."); - emitter.endCompiler(); - + end(); return !parser.hadError; } + void end(){ + emitter.emitReturn(); + debug(printCode){ + if(!parser.hadError) + currentChunk.disassembleChunk(); + } + } } diff --git a/src/clox/container/int24.d b/src/clox/container/int24.d deleted file mode 100644 index 406a933..0000000 --- a/src/clox/container/int24.d +++ /dev/null @@ -1,26 +0,0 @@ -module clox.container.int24; - -struct Uint24{ - nothrow: @nogc: @safe: - ubyte[3] data; - static Uint24 opCall(uint n){ - import std.bitmanip : nativeToLittleEndian; - Uint24 u3; - assert(n <= 16_777_215); - ubyte[uint.sizeof] d = nativeToLittleEndian!uint(n); - u3.data[0 .. 3] = d[0 .. 3]; - return u3; - } -} -uint toUint(Uint24 u3) @nogc nothrow @safe { - import std.bitmanip : littleEndianToNative; - ubyte[4] temp; - temp[0 .. 3] = u3.data; - return littleEndianToNative!uint(temp); -} -unittest{ - static assert(Uint24.sizeof == 3); - assert(Uint24(5).toUint == 5); - assert(Uint24(16_777_215).toUint == 16_777_215); -} - diff --git a/src/clox/container/rle.d b/src/clox/container/rle.d deleted file mode 100644 index f6e117a..0000000 --- a/src/clox/container/rle.d +++ /dev/null @@ -1,62 +0,0 @@ -module clox.container.rle; - -import std.traits; -import std.container.array; - -struct Rle(T, L = ubyte) if(isUnsigned!L){ - nothrow: @nogc: - align(1) struct Count{ - T item; - L num; - } - size_t total; - Array!Count data; - private void pushNew(T item){ - data ~= Count(item, 0); - } - size_t push(T item){ - if(data.length){ - Count* d = &data[(data.length)-1]; - if(d.item == item && d.num < L.max){ - d.num++; - return total++; - } - } - pushNew(item); - return total++; - } - T opOpAssign(string op: "~")(T rhs){ - push(rhs); - return rhs; - } - T opIndex(size_t n) const @safe{ - assert(n < total); - size_t c; - for(size_t i; i < n;){ - i += data[c].num + 1; - if(i <= n) - c++; - } - return data[c].item; - } -} -unittest{ - import clox.container.int24; - auto rl = Rle!(Uint24, ubyte)(); - static assert(rl.Count.sizeof == 4); - foreach(i; 0..300){ - size_t index = rl.push(Uint24(5)); - assert(rl[index].toUint == 5); - } - assert(rl[299].toUint == 5); - foreach(i; 0..30){ - size_t index = rl.push(Uint24(0)); - assert(rl[index].toUint == 0); - } - assert(rl[0].toUint == 5); - foreach(i; 0..300){ - size_t index = rl.push(Uint24(16_777_215)); - assert(rl[index].toUint == 16_777_215); - } -} - diff --git a/src/clox/container/stack.d b/src/clox/container/stack.d index 9dabed1..a719ad1 100644 --- a/src/clox/container/stack.d +++ b/src/clox/container/stack.d @@ -4,8 +4,8 @@ struct Stack(T, size_t N){ @nogc: nothrow: T* top; T[N] data; - invariant{ assert(top <= data.ptr + N); assert(top >= data.ptr); } - this(int _) @safe{ + /* invariant{ assert(top <= data.ptr + N); assert(top >= data.ptr); } */ + void reset(){ top = data.ptr; } void push(T value){ diff --git a/src/clox/container/table.d b/src/clox/container/table.d new file mode 100644 index 0000000..1c88e21 --- /dev/null +++ b/src/clox/container/table.d @@ -0,0 +1,176 @@ +module clox.container.table; + +import core.stdc.string; +import core.stdc.stdlib; + +import clox.object; +import clox.value; +import clox.memory; + +enum TABLE_MAX_LOAD = 0.75; + +struct Table{ + uint count, capacity; + Entry* entries; + struct Entry{ + Obj.String* key; + Value value; + } + void initialise(){ + count = capacity = 0; + entries = null; + } + void free(){ + if(entries !is null) + FREE_ARRAY!Entry(entries, capacity); + initialise(); + } + private void adjustCapacity(uint cap){ + Entry* ent = ALLOCATE!Entry(cap); + for(int i = 0; i < cap; i++){ + ent[i].key = null; + ent[i].value = Value.init; + } + count = 0; + for(int i = 0; i < capacity; i++){ + Entry* entry = &entries[i]; + if(entry.key is null) + continue; + Entry* dest = findEntry(entries, capacity, entry.key); + dest.key = entry.key; + dest.value = entry.value; + count++; + } + if(entries !is null) + FREE_ARRAY!Entry(entries, capacity); + entries = ent; + capacity = cap; + } + private Entry* findEntry(Entry* entries, int capacity, in Obj.String* key){ + uint index = key.hash % capacity; + Entry* tombstone = null; + while(true){ + Entry* entry = &entries[index]; + if(entry.key is null){ + if(entry.value.isType(Value.Type.None)){ + return tombstone !is null ? tombstone : entry; + } else { + if(tombstone is null) + tombstone = entry; + } + } else if(entry.key is key){ + return entry; + } + index = (index + 1) % capacity; + } + } + bool set(Obj.String* key, Value value){ + if(count + 1 > capacity * TABLE_MAX_LOAD){ + uint cap = GROW_CAPACITY(capacity); + adjustCapacity(cap); + } + Entry* entry = findEntry(entries, capacity, key); + bool isNewKey = entry.key == null; + if(isNewKey && entry.value.isType(Value.Type.None)) + count++; + entry.key = key; + entry.value = value; + return isNewKey; + } + bool get(in Obj.String* key, out Value value){ + if(count == 0) + return false; + Entry* entry = findEntry(entries, capacity, key); + if(entry.key is null) + return false; + value = entry.value; + return true; + } + bool del(in Obj.String* key){ + if(count == 0) + return false; + Entry* entry = findEntry(entries, capacity, key); + if(entry.key is null) + return false; + entry.key = null; + entry.value = Value.from(true); + return true; + } + void addAll(Table* from){ + for(int i = 0; i < from.capacity; i++){ + Entry* entry = &from.entries[i]; + if(entry.key !is null) + set(entry.key, entry.value); + } + } + Obj.String* findString(const char[] chars, uint hash){ + if(count == 0) + return null; + uint index = hash % capacity; + while(true){ + Entry* entry = &entries[index]; + if(entry.key == null){ + if(entry.value.isType(Value.Type.None)) + return null; + } else if(entry.key.hash == hash && entry.key.chars == chars){ + return entry.key; + } + index = (index + 1) % capacity; + } + } +} + +unittest{ + Table tbl; + tbl.initialise(); + scope(exit) + tbl.free(); + scope(exit) + freeObjects(); + + assert(tbl.count == 0); + + Obj.String* str = Obj.String.copy("hello"); + tbl.set(str, Value.from(true)); + assert(tbl.count == 1); + + assert(tbl.findString("hello", str.hash) is str); + + Value val; + assert(tbl.get(str, val)); + assert(val.asBoolean == true); + + assert(tbl.del(str)); + assert(tbl.get(str, val) == false); + assert(val.isType(Value.Type.None)); + assert(tbl.count == 1); + + foreach(i; 0..25){ + Obj.String* str2 = Obj.String.copy("hello"); + tbl.set(str2, Value.from(i)); + assert(tbl.get(str2, val)); + assert(val.asNumber == i); + } + + assert(tbl.get(str, val) == false); + + tbl.set(str, Value.from(true)); + assert(tbl.get(str, val)); + assert(val.asBoolean == true); +} + +uint jenkinsOneAtATimeHash(T)(in T[] key){ + uint hash = 0; + foreach(v; key){ + hash += v; + hash += hash << 10; + hash ^= hash >> 6; + } + hash += hash << 3; + hash ^= hash >> 11; + hash += hash << 15; + return hash; +} + +alias hashString = jenkinsOneAtATimeHash; + diff --git a/src/clox/container/varint.d b/src/clox/container/varint.d deleted file mode 100644 index d1d467b..0000000 --- a/src/clox/container/varint.d +++ /dev/null @@ -1,73 +0,0 @@ -module clox.container.varint; - -struct VarUint{ - import std.bitmanip; - nothrow: @nogc: - uint i; - ubyte len; - ubyte[4] data; - this(long l) @safe { - if(l < 0b1000_0000){ - len = 1; - data[0] = (cast(ubyte)l); - return; - } - if(l < 0b0100_0000__0000_0000){ - len = 2; - data[0 .. 2] = nativeToBigEndian(cast(ushort)l); - data[0] |= 0b1000_0000; - return; - } - if(l < 0b0010_0000__0000_0000__0000_0000__0000_0000){ - len = 4; - data[0 .. 4] = nativeToBigEndian(cast(uint)l); - data[0] |= 0b1100_0000; - return; - } - assert(0); - } - static VarUint read(const(ubyte)[] data) @safe { - VarUint v; - ubyte a = data[0]; - if((data[0] & 0b1000_0000) == 0){ - v.i = a; - v.len = 1; - return v; - } - if((a & 0b0100_0000) == 0){ - ubyte[2] d = data[0 .. 2]; - d[0] &= 0b0111_1111; - v.i = bigEndianToNative!ushort(d); - v.len = 2; - return v; - } - if((a & 0b0010_0000) == 0){ - ubyte[4] d = data[0 .. 4]; - d[0] &= 0b0011_1111; - v.i = bigEndianToNative!uint(d); - v.len = 4; - return v; - } - assert(0); - } - ubyte[] bytes() @nogc nothrow { - return data[0 .. len]; - } -} -unittest{ - import std.range; - assert(VarUint(5).bytes.length == 1); - assert(VarUint(127).bytes.length == 1); - assert(VarUint(128).bytes.length == 2); - assert(VarUint(536_870_911).bytes.length == 4); - foreach(ulong i; [ - 0, 1, 2, 5, - 150, 127, 128, - 536_870_911, - ushort.max * 100 - ]){ - auto vi = VarUint(i); - assert(i == VarUint.read(vi.bytes).i); - } -} - diff --git a/src/clox/container/vartype.d b/src/clox/container/vartype.d deleted file mode 100644 index 1d2a25a..0000000 --- a/src/clox/container/vartype.d +++ /dev/null @@ -1,73 +0,0 @@ -module clox.container.vartype; - -import std.stdio; -import std.algorithm; -import std.array; -import std.uni; -import std.conv; - -struct VarType(S) if(is(S == union)){ - private enum members = __traits(derivedMembers, S); - mixin("enum Type{ None, ", [members].map!asCapitalized.join(", "), "}"); - private S value; - private Type _type; - Type type() const @safe @nogc nothrow => _type; - private void check(Type t) const @safe nothrow @nogc{ - assert(this.type == t, "Tried to get wrong type"); - } - private template funcs(string G, string T){ - mixin("bool is", G, "() const nothrow @nogc @safe => this.type == this.Type.", G, ";"); - mixin("auto get", G, "() const nothrow @nogc { - check(this.Type.", G, "); - return value.", T, "; - }"); - mixin("void set", G, "(typeof(S.", T, ") v = typeof(S.", T, ").init){ - this._type = this.Type.", G, "; - this.value.", T, " = v; - }"); - mixin("static auto ", T, "(typeof(S.", T, ") v){ - typeof(this) vt; vt.set", G, "(v); - return vt; - }"); - mixin("static auto ", T, "(){ - typeof(this) vt; - vt.set", G, "(); - return vt; - }"); - } - static foreach(s; members){ - mixin funcs!(s.asCapitalized.to!string, s); - } - string toString() const{ - final switch(_type){ - static foreach(s; members){ - mixin("case Type.", s.asCapitalized.to!string, ": - return _type.to!string ~ ':' ~ get", s.asCapitalized.to!string, ".to!string ;"); - } - case Type.None: return "None"; - } - } -} - -unittest{ - import std.exception, std.stdio; - union Test{ - uint u; - int i; - double d; - } - auto i = VarType!Test.i(-5); - assert(i.getI == -5); - assert(i.type == i.Type.I); - assert(i.isI); - assert(!i.isD); - i.setD(0.5); - assert(i.getD == 0.5); - assert(i.type == i.Type.D); - assert(i.isD); - assert(!i.isU); - - auto i2 = VarType!Test.i(); - assert(i2.isI); -} - diff --git a/src/clox/dbg.d b/src/clox/dbg.d index 157c7db..19d4d27 100644 --- a/src/clox/dbg.d +++ b/src/clox/dbg.d @@ -1,64 +1,52 @@ module clox.dbg; -import std.stdio; -import std.conv; -import std.uni; -import std.format; -import std.meta : Filter; -import std.traits : EnumMembers; - -import colored; +import core.stdc.stdio; +import std.functional : ctEval; +import std.traits : EnumMembers, hasUDA, getUDAs; import clox.chunk; import clox.value; import clox.util; -import clox.container.varint; -import clox.container.int24; -private ulong simpleInstruction(alias op)(string name, ulong offset){ - static if(isValueOp!op) - stderr.writeln(name.cyan); - else static if(isLogicOp!op) - stderr.writeln(name.lightRed); - else static if(isCompOp!op) - stderr.writeln(name.red); - else static if(isArithOp!op) - stderr.writeln(name.yellow); - else - stderr.writeln(name.lightCyan); +private int simpleInstruction(alias OpCode op)(const char* name, int offset){ + enum c = getUDAs!(op, OpColour)[0]; + printf(colour!("%s\n", c.r, c.g, c.b).ptr, name); return offset + 1; } -private ulong constantInstruction(string name, Chunk* chunk, ulong offset){ - VarUint constant = VarUint.read(chunk.code.data[offset + 1 .. $]); - stderr.write("%-16s".format(name).cyan, " %4d ".format(constant.i).lightMagenta, "'"); - printValue(chunk.constants[constant.i]); - stderr.writeln("'"); - return offset + 1 + constant.len; +private int constantInstruction(alias OpCode op)(const char* name, Chunk* chunk, int offset){ + enum c = getUDAs!(op, OpColour)[0]; + ubyte constant = chunk.code[offset + 1]; + printf(ctEval!(colour!("%-16s", c.r, c.g, c.b) ~ " %4d '").ptr, name, constant); + chunk.constants.values[constant].print(); + printf("'\n"); + return offset + 2; } -void disassembleChunk(Chunk* chunk, string name = "chunk"){ - stderr.writefln("== %s ==", name); - for(ulong offset = 0; offset < chunk.code.length;) +void disassembleChunk(Chunk* chunk, const char* name = "chunk"){ + printf(" == %s ==\n", name); + for(int offset = 0; offset < chunk.count;){ offset = disassembleInstruction(chunk, offset); + } } -ulong disassembleInstruction(Chunk* chunk, const ulong offset){ - stderr.write(" %04d ".format(offset).lightGray); + +int disassembleInstruction(Chunk* chunk, int offset){ + printf("%04d ", offset); if(offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1]){ - stderr.write(" | ".darkGray); + printf(colour!(" | ", Colour.Black).ptr); } else { - stderr.write(" %4d ".format(chunk.lines[offset].toUint).lightGray); + printf(colour!("%4d ", Colour.Black).ptr, chunk.lines[offset]); } ubyte instruction = chunk.code[offset]; - with(OpCode) switch(instruction){ - case Constant: - return constantInstruction("OP_CONSTANT", chunk, offset); - static foreach(k; Filter!(isSize1Op, EnumMembers!OpCode)){ - case k: - static name = "OP_" ~ (k.to!string).toUpper; - return simpleInstruction!k(name, offset); + switch(instruction){ + static foreach(e; EnumMembers!OpCode){ + static if(e == OpCode.Constant){ + case OpCode.Constant: return constantInstruction!(OpCode.Constant)("OP_CONSTANT", chunk, offset); + } else { + case e: + return simpleInstruction!e(e.stringof, offset); + } } - default: - stderr.writefln("Unknown opcode %d", instruction); + default: printf("Unknown opcode %d\n", instruction); return offset + 1; } } diff --git a/src/clox/emitter.d b/src/clox/emitter.d index 34d049c..49b972a 100644 --- a/src/clox/emitter.d +++ b/src/clox/emitter.d @@ -1,51 +1,31 @@ module clox.emitter; -import clox.compiler; -import clox.chunk; -import clox.value; -import clox.util; -import clox.dbg; +import core.stdc.stdio; -import clox.container.varint; +import clox.chunk; +import clox.compiler; +import clox.value; struct Emitter{ Compiler* compiler; - Chunk* chunk; private uint line = 1; - Chunk* currentChunk() @nogc nothrow { - return chunk; + uint setLine(uint line) => this.line = line; + void initialise(Compiler* compiler){ + this.compiler = compiler; } - void emit(Args...)(Args args) @nogc nothrow { - static foreach(v; args){{ - static if(is(typeof(v) == OpCode)){ - auto bytes = v; - } else static if(is(typeof(v) == uint)){ - auto bytes = VarUint(v).bytes; - } else { - static assert(0); - } - currentChunk.write(bytes, line); - }} + void emit(ubyte[] bytes...){ + foreach(b; bytes) + compiler.currentChunk.write(b, compiler.parser.previous.line); } - void emitConstant(Value value) @nogc nothrow { - emit(OpCode.Constant, makeConstant(value)); - } - void emitReturn() @nogc nothrow { + void emitReturn(){ emit(OpCode.Return); } - void endCompiler(){ - emitReturn(); - debug(printCode){ - if(!compiler.parser.hadError) - disassembleChunk(currentChunk()); - } + void emitConstant(Value value){ + emit(OpCode.Constant, cast(ubyte)makeConstant(value)); } - uint makeConstant(Value value) @nogc nothrow { - uint constant = chunk.addConstant(value); + uint makeConstant(Value value){ + uint constant = compiler.currentChunk.addConstant(value); return constant; } - void setLine(uint l) @nogc nothrow { - this.line = l; - } } diff --git a/src/clox/main.d b/src/clox/main.d index 98241b6..35a3e0c 100644 --- a/src/clox/main.d +++ b/src/clox/main.d @@ -1,47 +1,73 @@ module clox.main; -import std.stdio; -import std.file; +import core.sys.posix.unistd : STDIN_FILENO; +import core.stdc.stdio; +import core.stdc.stdlib; +import clox.util; import clox.chunk; import clox.dbg; import clox.vm; -import clox.object; -extern(C) int isatty(int); +int interpretResult(VM.InterpretResult result){ + if(result == VM.InterpretResult.CompileError) + return 65; + else if(result == VM.InterpretResult.RuntimeError) + return 70; + return 0; +} -struct Lox{ - VM vm; - this(int _){ - vm = VM(0); - } - int runFile(string path){ - string source = path.readText(); - VM.InterpretResult result = vm.interpret(source); - final switch(result){ - case VM.InterpretResult.CompileError: return 65; - case VM.InterpretResult.RuntimeError: return 70; - case VM.InterpretResult.Ok: return 0; +int runPrompt(){ + char[1024 * 4] line; + while(true){ + printf(colour!("lox> ", Colour.Green).ptr); + if(!fgets(line.ptr, line.sizeof, stdin)){ + printf("\n"); + return 0; } + vm.interpret(line.ptr); } - int runPrompt(){ - while(true){ - write("lox> "); - string line = stdin.readln(); - if(!line){ - writeln(); - return 0; - } - vm.interpret(line); - } +} +int runStdin(){ + char* source = readStdin(); + scope(exit) + source.free(); + return vm.interpret(source).interpretResult; +} +int runFile(const char* path){ + char* source = path.readFile(); + if(!source) + return 74; + scope(exit) + source.free(); + return vm.interpret(source).interpretResult; +} + +int runMain(ulong argc, const char** argv){ + vm.initialise(); + scope(exit) + vm.free(); + + if(argc == 1 && isatty(STDIN_FILENO)){ + return runPrompt(); + } else if(argc == 1){ + return runStdin(); + } else if(argc == 2){ + return runFile(argv[1]); + } else { + stderr.fprintf("Usage: lox [path]\n"); + return 64; } } -int main(string[] argv){ - Lox lox = Lox(0); - if(isatty(stdin.fileno)) - return lox.runPrompt(); - else - return lox.runFile("/dev/stdin"); +version(D_BetterC){ + extern(C) int main(int argc, const char** argv){ + return runMain(argc, argv); + } +} else { + int main(string[] args){ + import std.string, std.algorithm, std.array; + return runMain(args.length, cast(const char**)args.map!toStringz.array); + } } diff --git a/src/clox/mem.d b/src/clox/memory.d similarity index 52% rename from src/clox/mem.d rename to src/clox/memory.d index a802382..ade0b44 100644 --- a/src/clox/mem.d +++ b/src/clox/memory.d @@ -1,17 +1,17 @@ -module clox.mem; +module clox.memory; -import std.stdio; -import std.traits; -import std.conv; -import core.stdc.stdlib : calloc, realloc, free; -import std.range, std.algorithm; +import core.stdc.stdlib; -import colored; +auto GROW_CAPACITY(uint capacity) => capacity < 8 ? 8 : capacity * 2; +auto GROW_ARRAY(T)(T* ptr, size_t oldCount, size_t newCount) => cast(T*)reallocate(ptr, T.sizeof * oldCount, T.sizeof * newCount); +auto FREE_ARRAY(T)(T* ptr, size_t oldCount) => reallocate!T(ptr, T.sizeof * oldCount, 0); +auto FREE(T)(T* ptr) => reallocate(ptr, T.sizeof, 0); +auto ALLOCATE(T)(size_t count) => cast(T*)reallocate!T(null, 0, T.sizeof * count); -import clox.util; - -debug { - private: +version(D_BetterC) {} else { + debug(memTrace): private: + import std.stdio, std.conv, std.algorithm, std.range; + import colored; alias backtrace_state = void; // "This struct is intentionally not defined in the public interface" alias uintptr_t = void*; extern(C) alias backtrace_error_callback = extern(C) void function(void *data, const char *msg, int errnum) nothrow; @@ -35,8 +35,9 @@ debug { } return 0; } - static this(){ - bts = backtrace_create_state(null, false, &btsErrCB, null).validateAssert(); + shared static this(){ + bts = backtrace_create_state(null, false, &btsErrCB, null); + assert(bts); } struct BackTraceBuilder{ @@ -48,10 +49,10 @@ debug { } BackTraceBuilder createBacktrace(int skip = 1) nothrow{ BackTraceBuilder btb; - backtrace_full(bts, skip, &btsFullCB, &btsErrCB, &btb).validateAssert!"!a"; + assert(backtrace_full(bts, skip, &btsFullCB, &btsErrCB, &btb) == 0); return btb; } - + struct Allocation{ ulong size; BackTraceBuilder backtrace; @@ -71,82 +72,61 @@ debug { } } } + } -T* allocate(T)() nothrow @nogc{ - return allocate!T(1).ptr; -} -T[] allocate(T)(size_t n) nothrow @nogc{ - T* data = cast(T*)calloc(n, T.sizeof); - assert(data); - - debug { - allocatedPointers[data] = Allocation(n * T.sizeof, createBacktrace()); - totalAllocs++; - totalAllocBytes += n * T.sizeof; - } - - return data[0 .. n]; -} -void reallocate(T)(ref T[] arr, size_t newSize) nothrow @nogc{ - debug { - assert(arr.ptr, "Null pointer reallocate"); - assert(arr.ptr in allocatedPointers, "Invalid ptr"); - } - if(arr.length == newSize) - return; - - T* newPtr = cast(T*)arr.ptr.realloc(newSize * T.sizeof); - assert(newPtr); - - debug if(arr.ptr != newPtr){ - totalReallocs++; - totalFreedBytes += allocatedPointers[arr.ptr].size; - totalAllocBytes += newSize; - allocatedPointers[newPtr] = Allocation(newSize); - allocatedPointers[arr.ptr] = Allocation(0); - } - - if(newSize > arr.length){ - foreach(i; arr.length .. newSize) - newPtr[i] = 0; - } - - T[] newArr = newPtr[0 .. newSize]; - arr = newArr; -} -void deallocate(T)(T* ptr) nothrow @nogc { - debug { +T* reallocate(T)(T* ptr, size_t oldSize, size_t newSize){ + if(newSize == 0){ // Free assert(ptr, "Null pointer free"); - assert(ptr in allocatedPointers, "Invalid ptr"); - assert(allocatedPointers[ptr].size, "Double free"); - totalFrees++; - totalFreedBytes += allocatedPointers[ptr].size; - allocatedPointers[ptr] = Allocation(0); + debug(memTrace){ + import std.conv : to; + assert(ptr in allocatedPointers, "Invalid pointer free: " ~ ptr.to!string); + assert(allocatedPointers[ptr].size, "Double free: " ~ ptr.to!string); + totalFrees++; + totalFreedBytes += allocatedPointers[ptr].size; + allocatedPointers[ptr] = Allocation(0); + } + ptr.free(); + return null; } - ptr.free(); -} -void deallocate(T)(ref T[] arr) nothrow @nogc{ - arr.ptr.deallocate(); - arr = []; + T* result = cast(T*)ptr.realloc(newSize); + assert(result); + debug(memTrace){ + if(ptr is null){ // Malloc + totalAllocs++; + allocatedPointers[result] = Allocation(newSize, createBacktrace()); + totalAllocBytes += newSize; + } else if(ptr !is result){ // Realloc + totalReallocs++; + totalFreedBytes += allocatedPointers[ptr].size; + totalAllocBytes += newSize; + allocatedPointers[result] = Allocation(newSize); + allocatedPointers[ptr] = Allocation(0); + } + } + return result; } -unittest{ - int[] i = allocate!int(1); +import clox.object; +import clox.vm; - i[0] = 5; - - i.reallocate(64); - i.reallocate(2); - - assert(i == [ 5, 0 ]); - - i.deallocate(); - - assert(i == []); - - int* ip = allocate!int; - assert(ip); - ip.deallocate(); +void freeObjects(){ + Obj* object = vm.objects; + while(object != null){ + Obj* next = object.next; + object.freeObject(); + object = next; + } + vm.objects = null; +} +void freeObject(Obj* object){ + final switch(object.type){ + case Obj.Type.String: + Obj.String* str = cast(Obj.String*)object; + FREE_ARRAY!char(str.chars.ptr, str.chars.length + 1); + FREE!(Obj.String)(cast(Obj.String*)object); + break; + case Obj.Type.None: assert(0); + } } diff --git a/src/clox/obj.d b/src/clox/obj.d new file mode 100644 index 0000000..40f211a --- /dev/null +++ b/src/clox/obj.d @@ -0,0 +1,99 @@ +module clox.object; + +import core.stdc.stdio; + +import clox.memory; +import clox.value; +import clox.vm; +import clox.container.table; + +struct Obj{ + enum Type{ + None, String, + } + Type type; + Obj* next; + + bool isType(Type type) const pure => this.type == type; + 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.None) return &this; + } + + void print() const{ + final switch(type){ + case Type.String: printf(`"%s"`, asString.chars.ptr); break; + case Type.None: assert(0); + } + } + + String* asString() const pure => as!(Type.String); + + private static T* allocateObject(T)(){ + Obj* object = reallocate!Obj(null, 0, T.sizeof); + object.type = T.myType; + object.next = vm.objects; + vm.objects = object; + return cast(T*)object; + } + + struct String{ + static enum myType = Type.String; + Obj obj; + char[] chars; + uint hash; + + static String* allocateString(char[] chars, uint hash){ + String* str = allocateObject!String(); + str.chars = chars; + str.hash = hash; + vm.strings.set(str, Value.nil); + return str; + } + static String* copy(const char[] chars){ + uint strHash = hashString(chars); + Obj.String* interned = vm.strings.findString(chars, strHash); + if(interned !is null) + return interned; + char* heapChars = ALLOCATE!char(chars.length + 1); + heapChars[0 .. chars.length] = chars; + heapChars[chars.length] = '\0'; + return allocateString(heapChars[0 .. chars.length], strHash); + } + static String* concat(String* a, String* b){ + size_t len = a.chars.length + b.chars.length; + char* c = ALLOCATE!char(len + 1); + c[0 .. a.chars.length] = a.chars; + c[a.chars.length .. len] = b.chars; + c[len] = '\0'; + return take(c[0 .. len]); + } + static Obj.String* take(char[] c) { + uint strHash = hashString(c); + Obj.String* interned = vm.strings.findString(c, strHash); + if(interned !is null){ + FREE_ARRAY!char(c.ptr, c.length + 1); + return interned; + } + return allocateString(c, strHash); + } + } + + 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.None: assert(0); + } + } + bool opCmp(in ref Obj rhs) const pure{ + final switch(type){ + case Type.String: return asString.chars > rhs.asString.chars; + case Type.None: assert(0); + } + } +} + diff --git a/src/clox/object.d b/src/clox/object.d deleted file mode 100644 index 5964cdb..0000000 --- a/src/clox/object.d +++ /dev/null @@ -1,79 +0,0 @@ -module clox.object; - -import std.stdio; -import std.conv; -import core.stdc.string : memcpy, memcmp, strcat; - -import clox.mem; -import clox.vm; -import clox.value; - -struct Obj{ - enum Type{ - String, - } - Type type; - Obj* next; - static T* create(T)(VM* vm = null) @nogc nothrow{ - T* obj = cast(T*)allocate!T(); - obj.obj.type = T.type; - return obj; - } - static struct String{ - static enum type = Type.String; - Obj obj; - char[] data; - static String* create(size_t len, VM* vm = null) @nogc nothrow{ - String* str = Obj.create!String(); - if(vm){ - str.obj.next = vm.objects; - vm.objects = &str.obj; - } - str.data = allocate!char(len + 1); - return str; - } - static String* copy(string s, VM* vm = null) nothrow @nogc{ - String* str = String.create(s.length, vm); - str.data.ptr.memcpy(s.ptr, s.length); - return str; - } - static String* concat(const(String)* a, const(String)* b, VM* vm) nothrow @nogc{ - String* newStr = String.create((a.data.length)-1 + (b.data.length)-1, vm); - newStr.data.ptr.memcpy(a.data.ptr, a.data.length); - newStr.data.ptr.strcat(b.data.ptr); - return newStr; - } - } - string toString() const{ - final switch(type){ - case Type.String: - const(String)* str = cast(String*)&this; - return cast(immutable)str.data; - } - } -} - -bool isObj(Value value){ - switch(value.type){ - case value.Type.Str: return true; - default: return false; - } -} -Obj* getObj(Value value){ - switch(value.type){ - case value.Type.Str: return cast(Obj*)value.getStr; - default: assert(0); - } -} -void freeObject(Obj.String* str) @nogc nothrow{ - str.data.deallocate(); - str.deallocate(); -} -void freeObject(Obj* obj) @nogc nothrow{ - final switch(obj.type){ - case Obj.Type.String: - freeObject(cast(Obj.String*)obj); - break; - } -} - diff --git a/src/clox/parser.d b/src/clox/parser.d index 24bbdbd..cb943c6 100644 --- a/src/clox/parser.d +++ b/src/clox/parser.d @@ -1,50 +1,38 @@ module clox.parser; -import clox.compiler; -import clox.value; +import core.stdc.stdio; +import std.functional : ctEval; + import clox.scanner; +import clox.compiler; +import clox.chunk; import clox.parserules; +import clox.util; struct Parser{ Compiler* compiler; Token current, previous; bool hadError, panicMode; - void errorAtCurrent(string message){ - errorAt(current, message); - } - void error(string message){ - errorAt(previous, message); - } - void errorAt(in ref Token token, string message){ - import core.stdc.stdio; - if(panicMode) - return; - panicMode = true; - fprintf(stderr, "[line %d] Error", token.line); - if(token.type == Token.Type.EOF){ - fprintf(stderr, " at end"); - } else if(token.type != Token.Type.Error){ - fprintf(stderr, " at '%.*s'", cast(int)token.lexeme.length, token.lexeme.ptr); - } - fprintf(stderr, ": %.*s\n", cast(int)message.length, message.ptr); - hadError = true; - } - auto consume(Token.Type type, string msg){ - if(current.type == type){ - advance(); - return; - } - errorAtCurrent(msg); + void initialise(Compiler* compiler){ + this.compiler = compiler; } void advance(){ previous = current; while(true){ - current = compiler.scanner.scan(); + current = compiler.scanner.scanToken(); if(current.type != Token.Type.Error) break; - errorAtCurrent(current.lexeme); + + errorAtCurrent(current.lexeme.ptr); } } + void consume(Token.Type type, in char* message){ + if(current.type == type){ + advance(); + return; + } + errorAtCurrent(message); + } void expression(){ parsePrecedence(Precedence.Assignment); @@ -63,4 +51,29 @@ struct Parser{ infixRule(compiler); } } + + void errorAtCurrent(in char* message){ + errorAt(current, message); + } + void error(in char* message){ + errorAt(previous, message); + } + void errorAt(in ref Token token, in char* message){ + if(panicMode) + return; + panicMode = true; + fprintf(stderr, ctEval!("[line %d] " ~ colour!("Error", Colour.Red)).ptr, token.line); + + if (token.type == Token.Type.EOF) { + fprintf(stderr, " at end"); + } else if (token.type == Token.Type.Error) { + // Nothing. + } else { + fprintf(stderr, " at '%.*s'", cast(int)token.lexeme.length, token.lexeme.ptr); + } + + fprintf(stderr, ": %s\n", message); + hadError = true; + } } + diff --git a/src/clox/parserules.d b/src/clox/parserules.d index 35e6562..9e725c4 100644 --- a/src/clox/parserules.d +++ b/src/clox/parserules.d @@ -1,20 +1,15 @@ module clox.parserules; -import clox.compiler; import clox.chunk; +import clox.compiler; +import clox.parser; import clox.scanner; +import clox.emitter; import clox.value; import clox.object; alias ParseFn = void function(Compiler* compiler); -private void number(Compiler* compiler){ - import core.stdc.stdlib : strtod; - Token token = compiler.parser.previous; - double value = strtod(token.lexeme.ptr, null); - compiler.emitter.setLine(token.line); - compiler.emitter.emitConstant(Value.num(value)); -} private void grouping(Compiler* compiler){ compiler.parser.expression(); compiler.parser.consume(Token.Type.RightParen, "Expect ')' after expression."); @@ -25,7 +20,7 @@ private void unary(Compiler* compiler){ compiler.emitter.setLine(operator.line); switch(operator.type){ case Token.Type.Minus: compiler.emitter.emit(OpCode.Negate); break; - case Token.Type.Bang: compiler.emitter.emit(OpCode.Not); break; + case Token.Type.Bang: compiler.emitter.emit(OpCode.Not); break; default: assert(0); } } @@ -52,6 +47,14 @@ private void binary(Compiler* compiler){ default: assert(0); } } + +private void number(Compiler* compiler){ + import core.stdc.stdlib : strtod; + Token token = compiler.parser.previous; + double value = strtod(token.lexeme.ptr, null); + compiler.emitter.setLine(token.line); + compiler.emitter.emitConstant(Value.from(value)); +} private void literal(Compiler* compiler){ Token token = compiler.parser.previous; compiler.emitter.setLine(token.line); @@ -62,19 +65,20 @@ private void literal(Compiler* compiler){ default: assert(0); } } -private void strlit(Compiler* compiler) @nogc{ +private void strlit(Compiler* compiler){ Token token = compiler.parser.previous; - string str = token.lexeme[1 .. $-1]; + const char[] str = token.lexeme[1 .. $-1]; Obj.String* strObj = Obj.String.copy(str); compiler.emitter.setLine(token.line); - compiler.emitter.emitConstant(Value.str(strObj)); + compiler.emitter.emitConstant(Value.from(strObj)); } + struct ParseRule{ ParseFn prefix; ParseFn infix; Precedence precedence; - static immutable(ParseRule)* get(Token.Type type) @nogc nothrow{ + static immutable(ParseRule)* get(Token.Type type){ return &rules[type]; } } @@ -136,3 +140,5 @@ immutable ParseRule[Token.Type.max+1] rules = [ Token.Type.EOF : ParseRule(null, null, Precedence.None), ]; + + diff --git a/src/clox/scanner.d b/src/clox/scanner.d index 5eac814..a3e93d0 100644 --- a/src/clox/scanner.d +++ b/src/clox/scanner.d @@ -1,12 +1,7 @@ module clox.scanner; -import std.stdio; -import std.ascii; - -import common.util; - -struct Token{ - enum Type : ubyte { +struct Token { + enum Type{ None, Error, EOF, // Special LeftParen, RightParen, // Single-character tokens. LeftBrace, RightBrace, @@ -23,70 +18,49 @@ struct Token{ True, Var, While, } Type type; - int line; string lexeme; - static Token error(string msg) nothrow @nogc => Token(Token.Type.Error, 0, msg); + int line; } +bool isDigit(char c) => c >= '0' && c <= '9'; +bool isAlpha(char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + struct Scanner{ - nothrow: - @nogc: - string start; - string current; - int line = 1; - this(string source) @nogc nothrow{ - start = current = source; + const(char)* start; + const(char)* current; + int line; + void initialise(const char* source){ + start = source; + current = source; + line = 1; } - bool isAtEnd() const => current.length == 0; - private char peek() const => current[0]; - private char peekNext() const => current.length >= 2 ? current[1] : '\0'; - private Token makeToken(Token.Type type) const{ - Token token; - token.type = type; - token.lexeme = start[0 .. current.ptr - start.ptr]; - token.line = line; - return token; + char peek() const => *current; + char peekNext() const => isAtEnd ? '\0' : current[1]; + Token makeToken(Token.Type type) => Token(type, cast(string)start[0 .. current - start], line); + Token errorToken(string msg) => Token(Token.Type.Error, msg); + bool isAtEnd() const => *current == '\0'; + char advance(){ + current++; + return current[-1]; } - private char advance(){ - char c = current[0]; - current = current[1 .. $]; - return c; - } - private bool match(char needle){ - if(isAtEnd || current[0] != needle) + bool match(char expected){ + if(isAtEnd || *current != expected) return false; - current = current[1 .. $]; + current++; return true; } - private void skipWhitespace(){ - while(!isAtEnd){ - char c = peek(); - if(!c) - return; - if(c == '/' && peekNext() == '/'){ - while(!isAtEnd && peek() != '\n') - advance(); - continue; - } - if(!c.isWhite) - return; - if(c == '\n') - line++; - current = current[1 .. $]; - } - } - private Token parseString(){ - while(!isAtEnd && peek() != '"'){ + Token parseString(){ + while(peek() != '"' && !isAtEnd){ if(peek() == '\n') line++; advance(); } if(isAtEnd) - return Token.error("Unterminated string."); - advance(); + return errorToken("Unterminated string."); + advance(); // Closing " return makeToken(Token.Type.String); } - private Token parseNumber(){ + Token parseNumber(){ while(peek().isDigit) advance(); if(peek() == '.' && peekNext().isDigit){ @@ -96,8 +70,8 @@ struct Scanner{ } return makeToken(Token.Type.Number); } - private Token parseIdentifier(){ - while(peek().isAlphaNum_) + Token parseIdentifier(){ + while(peek().isAlpha || peek().isDigit) advance(); Token token = makeToken(Token.Type.Identifier); switch(token.lexeme){ @@ -121,15 +95,40 @@ struct Scanner{ } return token; } - Token scan(){ + void skipWhitespace(){ + while(true){ + char c = peek(); + switch(c){ + case ' ', '\r': + advance(); + break; + case '\n': + line++; + advance(); + break; + case '/': + if(peekNext() == '/'){ + while(peek() != '\n' && !isAtEnd) + advance(); + } else { + return; + } + break; + default: return; + } + } + } + Token scanToken(){ skipWhitespace(); start = current; if(isAtEnd) return Token(Token.Type.EOF); char c = advance(); - if(c.isAlpha_) - return parseIdentifier(); switch(c){ + case 'a': .. case 'z': + case 'A': .. case 'Z': + case '_': return parseIdentifier(); + case '0': .. case '9': return parseNumber(); case '(': return makeToken(Token.Type.LeftParen); case ')': return makeToken(Token.Type.RightParen); case '{': return makeToken(Token.Type.LeftBrace); @@ -148,9 +147,8 @@ struct Scanner{ case '"': return parseString(); default: break; } - if(c.isDigit) - return parseNumber(); - return Token.error("Unexpected character."); + return errorToken("Unexpected character."); } } + diff --git a/src/clox/util.d b/src/clox/util.d index 786c9fc..8970a60 100644 --- a/src/clox/util.d +++ b/src/clox/util.d @@ -1,20 +1,55 @@ module clox.util; -import std.stdio; -import std.traits : isUnsigned; -import std.container.array; -import std.functional : unaryFun; +import core.stdc.stdio; +import core.stdc.stdlib; +import std.functional : ctEval; -T validateAssert(alias pred = "!!a", T)(T v, lazy string msg = null) nothrow { - try{ - string m = msg; - static if(is(typeof(pred) == string)) - m = msg ? msg : pred; - assert(v.unaryFun!pred, m); - return v; - } catch(Exception){ - assert(0); +enum Colour : string{ + Black = "30", Red = "31", Green = "32", Yellow = "33", Blue = "34", Pink = "35", Cyan = "36", +} +string colour(string text, string num)() => ctEval!("\033[1;" ~ num ~ "m" ~ text ~ "\033[0m"); +string colour(string text, string r, string g, string b)() => ctEval!("\033[38;2;" ~ r ~ ";" ~ g ~ ";" ~ b ~ "m" ~ text ~ "\033[0m"); + +extern(C) int isatty(int); + +static char* readFile(const char* path) { + FILE* file = path.fopen("rb"); + if(file == null){ + stderr.fprintf("Could not open file \"%s\".\n", path); + return null; } + scope(exit) + file.fclose(); + + file.fseek(0, SEEK_END); + size_t fileSize = file.ftell(); + file.rewind(); + + char* buffer = cast(char*)malloc(fileSize + 1); + size_t bytesRead = fread(buffer, char.sizeof, fileSize, file); + buffer[bytesRead] = '\0'; + + return buffer; +} + +char* readStdin(){ + size_t bufferSize = 512; + char* buffer = cast(char*)malloc(bufferSize); + if (buffer == null) { + perror("Unable to allocate buffer"); + return null; + } + size_t totalBytesRead = 0; + size_t bytesRead; + while((bytesRead = fread(buffer + totalBytesRead, 1, bufferSize - totalBytesRead, stdin)) > 0){ + totalBytesRead += bytesRead; + if(totalBytesRead == bufferSize){ + bufferSize *= 2; + buffer = cast(char*)realloc(buffer, bufferSize); + } + } + buffer[totalBytesRead] = '\0'; + return buffer; } diff --git a/src/clox/value.d b/src/clox/value.d index 5699662..ff985a5 100644 --- a/src/clox/value.d +++ b/src/clox/value.d @@ -1,51 +1,99 @@ module clox.value; -import std.stdio; -import std.conv; - -import colored; +import core.stdc.stdio; +import clox.memory; import clox.object; -import clox.container.vartype; -private union U{ - bool bln; - bool nil; - double num; - Obj.String* str; -} -alias Value = VarType!U; +struct Value{ + enum Type{ + None, Bool, Nil, Number, Obj + } + Type type; + private union Un{ + bool boolean; + double number; + Obj* obj; + } + private Un un; -void printValue(Value value){ - final switch(value.type){ - case value.Type.Bln: stderr.writef("%s", value.getBln.to!string.yellow); break; - case value.Type.Num: stderr.writef("%g", value.getNum); break; - case value.Type.Nil: stderr.writef("nil"); break; - case value.Type.Str: stderr.writef("%s", value.getStr.data); break; - case value.Type.None: assert(0); + bool isType(Type type) const pure => this.type == type; + bool isType(Obj.Type type) const pure => this.type == Type.Obj && asObj.isType(type); + auto as(Type type)() const pure{ + static if(type != Type.None) + assert(this.type == type); + static if(type == Type.Number) return un.number; + static if(type == Type.Bool) return un.boolean; + static if(type == Type.Obj) return un.obj; + static if(type == Type.None) return this; } -} -bool isTruthy(Value value) nothrow @nogc { - final switch(value.type){ - case value.Type.Bln: return value.getBln; - case value.Type.Num: return true; - case value.Type.Nil: return false; - case value.Type.Str: return true; - case value.Type.None: assert(0); + + bool asBoolean() const pure => as!(Type.Bool); + double asNumber() const pure => as!(Type.Number); + Obj* asObj() const pure => cast(Obj*)as!(Type.Obj); + + static Value from(T: double)(T val) => Value(Type.Number, Un(number: val)); + static Value from(T: bool)(T val) => Value(Type.Bool, Un(boolean: val)); + static Value from(T)(T* val) if(__traits(compiles, val.obj)) => Value(Type.Obj, Un(obj: cast(Obj*)val)); + static Value from(T: Value)(T val) => val; + static Value nil() pure => Value(Type.Nil); + + bool isFalsey() const pure => (type == Type.Bool && asBoolean == false) || type == Type.Nil; + bool isTruthy() const pure => !isFalsey; + bool opEquals(Value rhs) const pure{ + if(rhs.type != type) + return false; + final switch(type){ + case Type.Number: return asNumber == rhs.asNumber; + case Type.Bool: return asBoolean == rhs.asBoolean; + case Type.Nil: return true; + case Type.Obj: return *asObj == *rhs.asObj; + case Type.None: assert(0); + } } -} -bool isFalsey(Value value) nothrow @nogc { - return !isTruthy(value); -} -bool compare(string op)(Value a, Value b){ - if(a.type != b.type) - return false; - final switch(a.type){ - case a.Type.Bln: return mixin("a.getBln", op, "b.getBln"); - case a.Type.Num: return mixin("a.getNum", op, "b.getNum"); - case a.Type.Str: return mixin("a.getStr.data", op, "b.getStr.data"); - case a.Type.Nil: return true; - case a.Type.None: assert(0); + int opCmp(Value rhs) const pure{ + final switch(type){ + case Type.Number: return asNumber > rhs.asNumber; + case Type.Bool: return asBoolean > rhs.asBoolean; + case Type.Nil: return 0; + case Type.Obj: return asObj > rhs.asObj; + case Type.None: assert(0); + } + } + void print() const{ + final switch(type){ + case Type.Number: printf("%g", asNumber); break; + case Type.Bool: printf(asBoolean ? "true" : "false"); break; + case Type.Nil: printf("nil"); break; + case Type.Obj: asObj.print(); break; + case Type.None: assert(0); + } + } +} + + +struct ValueArray{ + uint count; + uint capacity; + Value* values; + void initialise(){ + count = 0; + capacity = 0; + values = null; + } + void write(Value value){ + if(capacity < count + 1){ + int oldCapacity = capacity; + capacity = GROW_CAPACITY(oldCapacity); + values = GROW_ARRAY!Value(values, oldCapacity, capacity); + } + values[count] = value; + count++; + } + void free(){ + if(values) + FREE_ARRAY!Value(values, capacity); + initialise(); } } diff --git a/src/clox/vm.d b/src/clox/vm.d index 8777027..4d4cefa 100644 --- a/src/clox/vm.d +++ b/src/clox/vm.d @@ -1,135 +1,137 @@ module clox.vm; -import std.stdio; +import core.stdc.stdio; import clox.chunk; -import clox.value; import clox.dbg; -import clox.util; -import clox.compiler; import clox.object; +import clox.value; +import clox.object; +import clox.compiler; +import clox.memory; import clox.container.stack; -import clox.container.varint; -import clox.container.int24; +import clox.container.table; -enum stackMax = 256; +VM vm; struct VM{ - const(ubyte)* ip; - Stack!(Value, stackMax) stack; Chunk* chunk; + ubyte* ip; + Stack!(Value, 256) stack; + Table strings; Obj* objects; enum InterpretResult{ Ok, CompileError, RuntimeError } - this(int _) @nogc nothrow { - stack = typeof(stack)(0); + void initialise(){ + stack.reset(); + strings.initialise(); } - ~this(){ + void free(){ freeObjects(); + strings.free(); } - InterpretResult interpret(string source){ - Chunk c = Chunk(); + InterpretResult interpret(const char* source){ + Chunk cnk; + cnk.initialise(); + scope(exit) + cnk.free(); Compiler compiler; - if(!compiler.compile(source, &c)) + if(!compiler.compile(source, &cnk)) return InterpretResult.CompileError; - chunk = &c; - return interpret(chunk); - } - InterpretResult interpret(Chunk* chunk){ - this.chunk = chunk; - ip = &chunk.code[0]; + this.chunk = &cnk; + this.ip = cnk.code; return run(); } - private void runtimeError(Args...)(string format, Args args) nothrow { - size_t instruction = ip - (&chunk.code[0]) - 1; - uint line = chunk.lines[instruction].toUint; - try{ - stderr.writef("[line %d] ", line); - stderr.writefln(format, args); - } catch(Exception){} - /* stack.reset(); */ + void runtimeError(Args...)(const char* format, Args args){ + fprintf(stderr, format, args); + fputs("\n", stderr); + + size_t instruction = vm.ip - vm.chunk.code - 1; + int line = vm.chunk.lines[instruction]; + fprintf(stderr, "[line %d] in script\n", line); + stack.reset(); } - private InterpretResult run() nothrow { - auto readByte() => *ip++; - auto readIns() => cast(OpCode)readByte(); - Value readConstant(){ - VarUint constant = VarUint.read(ip[0 .. 4]); - ip += constant.len; - return chunk.constants[constant.i]; - } - Value peek(int distance = 0){ - return stack.top[-1 - distance]; + InterpretResult run(){ + ubyte readByte() => *vm.ip++; + auto readConstant() => chunk.constants.values[readByte()]; + Value peek(int distance) => stack.top[-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)(){ + if(!check){ + runtimeError(checkMsg.ptr); + return 1; + } + auto b = stack.pop().as!pre; + auto a = stack.pop().as!pre; + stack.push(Value.from(mixin("a", op, "b"))); + return 0; } while(true){ debug(traceExec){ - stderr.writeln(" ", stack.live); - disassembleInstruction(chunk, ip - &chunk.code[0]); + printf(" "); + foreach(slot; stack.live){ + printf("[ "); + slot.print(); + printf(" ]"); + } + printf("\n"); + disassembleInstruction(vm.chunk, cast(int)(vm.ip - vm.chunk.code)); } - OpCode instruction = readIns(); - with(OpCode) opSwitch: final switch(instruction){ - case Constant: + OpCode instruction; + opSwitch: final switch(instruction = cast(OpCode)readByte()){ + case OpCode.Constant: Value constant = readConstant(); stack.push(constant); break; - case True: - stack.push(Value.bln(true)); - break; - case False: - stack.push(Value.bln(false)); - break; - case Nil: - stack.push(Value.nil()); - break; - static foreach(k, op; [ Add: "+", Subtract: "-", Multiply: "*", Divide: "/" ]){ + case OpCode.Nil: stack.push(Value.nil); break; + case OpCode.True: stack.push(Value.from(true)); break; + case OpCode.False: stack.push(Value.from(false)); break; + + static foreach(k, op; [ OpCode.Equal: "==", OpCode.NotEqual: "!=" ]){ case k: - static if(k == Add){ - if(peek(0).isStr && peek(1).isStr){ - const(Obj.String)* b = stack.pop().getStr; - const(Obj.String)* a = stack.pop().getStr; - Obj.String* newStr = Obj.String.concat(a, b, &this); - stack.push(Value.str(newStr)); + binaryOp!(op, true, null, Value.Type.None); + break opSwitch; + } + static foreach(k, op; [ OpCode.Greater: ">", OpCode.Less: "<", OpCode.GreaterEqual: ">=", OpCode.LessEqual: "<=" ]){ + case k: + if(binaryOp!(op, checkSameType, "Operands must be of the same type.", Value.Type.None)) + return InterpretResult.RuntimeError; + break opSwitch; + } + + static foreach(k, op; [ OpCode.Add: "+", OpCode.Subtract: "-", OpCode.Multiply: "*", OpCode.Divide: "-" ]){ + case k: + static if(k == OpCode.Add){ + if(checkBinaryType!(Obj.Type.String)){ + Obj.String* b = stack.pop().asObj.asString; + Obj.String* a = stack.pop().asObj.asString; + Obj.String* result = Obj.String.concat(a, b); + stack.push(Value.from(result)); break opSwitch; } } - if(!peek(0).isNum || !peek(1).isNum){ - runtimeError("Operands must be numbers."); + if(binaryOp!(op, checkBinaryType!(Value.Type.Number), "Operands must be numbers.", Value.Type.Number)()) return InterpretResult.RuntimeError; - } - double b = stack.pop().getNum; - double a = stack.pop().getNum; - stack.push(Value.num(mixin("a", op, "b"))); break opSwitch; } - static foreach(k, op; [ NotEqual: "!=", Equal: "==", Greater: ">", GreaterEqual: ">=", Less: "<", LessEqual: "<=" ]){ - case k: - Value b = stack.pop(); - Value a = stack.pop(); - stack.push(Value.bln(compare!op(a, b))); - break opSwitch; - } - case Not: - stack.push(Value.bln(stack.pop().isFalsey)); + + case OpCode.Not: + stack.push(Value.from(stack.pop().isFalsey)); break; - case Negate: - if(!peek(0).isNum){ + case OpCode.Negate: + if(!peek(0).isType(Value.Type.Number)){ runtimeError("Operand must be a number."); return InterpretResult.RuntimeError; } - stack.push(Value.num(-stack.pop().getNum)); + stack.push(Value.from(-stack.pop().asNumber)); break; - case Return: - debug printValue(stack.pop()); - debug stderr.writeln(); + case OpCode.Return: + stack.pop().print(); + printf("\n"); return InterpretResult.Ok; } } assert(0); } - void freeObjects(){ - for(Obj* obj = objects; obj !is null;){ - Obj* next = obj.next; - obj.freeObject(); - obj = next; - } - } }