diff --git a/examples/ch25-counter-closure.lox b/examples/ch25-counter-closure.lox new file mode 100644 index 0000000..f97c990 --- /dev/null +++ b/examples/ch25-counter-closure.lox @@ -0,0 +1,13 @@ +fun initCounter() { + var privateCounter = 0; + fun incr() { + privateCounter = privateCounter + 1; + return privateCounter; + } + return incr; +} + +var count = initCounter(); +print(count()); +print(count()); +print(count()); diff --git a/src/compiler.zig b/src/compiler.zig index f2ee56e..a6877fb 100644 --- a/src/compiler.zig +++ b/src/compiler.zig @@ -131,7 +131,7 @@ const FunctionType = enum { Script, }; -const Compiler = struct { +pub const Compiler = struct { function: *ObjFunction, type_: FunctionType, @@ -142,7 +142,7 @@ const Compiler = struct { upvalues: [U8_COUNT]Upvalue, scopeDepth: i16, - pub fn init(type_: FunctionType, om: *ObjManager, enclosing: *Compiler) !Compiler { + pub fn init(type_: FunctionType, om: *ObjManager, enclosing: ?*Compiler) !Compiler { var c = Compiler{ .function = undefined, // see 24.2.1 .. revisit in light of garbage collector .type_ = type_, @@ -171,10 +171,13 @@ pub fn compile(source: []u8, om: *ObjManager) !*ObjFunction { // startup -- could be comptime TODO initRules(); - // setup the compiler + // save reference to object manager objManager = om; - var compiler = try Compiler.init(FunctionType.Script, objManager, current); + + // setup the compiler + var compiler = try Compiler.init(FunctionType.Script, objManager, null); current = &compiler; + objManager.isCompilerInitialized = true; scanner = Scanner.init(source); parser = Parser{ @@ -900,6 +903,7 @@ fn endCompiler() *ObjFunction { } else { // we're at the top of the stack, so we're done // unsafe warning warning + objManager.isCompilerInitialized = false; current = undefined; } @@ -989,3 +993,16 @@ fn emitByte(byte: u8) void { fn tokenString(token: Token) []const u8 { return scanner.source[token.start .. token.start + token.length]; } + +// markCompilerRoots is for garbage collection +pub fn markCompilerRoots() void { + if (debug.LOG_GC) print("-- gc: mark compiler roots\n", .{}); + + var compiler: *Compiler = current; + compiler.function.markObject(objManager); + + while (compiler.enclosing != null) { + compiler = compiler.enclosing.?; + compiler.function.markObject(objManager); + } +} diff --git a/src/debug.zig b/src/debug.zig index 1ff968a..741fe50 100644 --- a/src/debug.zig +++ b/src/debug.zig @@ -1,13 +1,17 @@ // Debug Flags // global flag to enable/disable all -const enable_debugging = false; +const enable_debugging = true; // VM (Runtime) -pub const TRACE_EXECUTION_INCLUDE_INSTRUCTIONS = enable_debugging and true; +pub const TRACE_EXECUTION_INCLUDE_INSTRUCTIONS = enable_debugging and false; pub const TRACE_EXECUTION_SHOW_GET_SET_VARS = enable_debugging and false; -pub const TRACE_EXECUTION_PRINT_STACK = enable_debugging and true; +pub const TRACE_EXECUTION_PRINT_STACK = enable_debugging and false; // Compiler pub const PRINT_CODE_AFTER_END_COMPILER = enable_debugging and false; // TODO: try out comptime pub const TRACE_PARSER = enable_debugging and false; + +// Object Manager (esp Garbage Collection) +pub const STRESS_GC = enable_debugging and true; +pub const LOG_GC = enable_debugging and true; diff --git a/src/object.zig b/src/object.zig index 8a93a5b..b80bea4 100644 --- a/src/object.zig +++ b/src/object.zig @@ -8,9 +8,95 @@ const allocate = @import("./memory.zig").allocate; const Value = @import("./value.zig").Value; const Chunk = @import("./chunk.zig").Chunk; +const VM = @import("./vm.zig").VM; +const compiler = @import("./compiler.zig"); +const debug = @import("./debug.zig"); +const printValue = @import("./value.zig").printValue; + +pub const ObjType = enum { + objString, + objFunction, + objNative, + objClosure, + objUpvalue, +}; + +// Obj abstracts over pointers to various object types +pub const Obj = union(ObjType) { + objString: *ObjString, + objFunction: *ObjFunction, + objNative: *ObjNative, + objClosure: *ObjClosure, + objUpvalue: *ObjUpvalue, + + fn free(self: Obj, om: *ObjManager) void { + switch (self) { + Obj.objString => |o| { + om.allocator.free(o.chars); + om.allocator.destroy(o); + }, + Obj.objFunction => |o| { + // o.chunk.free(); // TODO: This is the segfault + om.allocator.destroy(o.chunk); + om.allocator.destroy(o); + }, + Obj.objNative => |o| om.allocator.destroy(o), + Obj.objClosure => |o| { + om.allocator.free(o.upvalues); + om.allocator.destroy(o); + }, + Obj.objUpvalue => |o| { + om.allocator.destroy(o); + }, + } + } + + fn checkIsMarkedAndUnset(self: Obj) bool { + switch (self) { + Obj.objString => |o| { + const val = o.isMarked; + o.isMarked = false; + return val; + }, + Obj.objFunction => |o| { + const val = o.isMarked; + o.isMarked = false; + return val; + + }, + Obj.objNative => |o| { + const val = o.isMarked; + o.isMarked = false; + return val; + }, + Obj.objClosure => |o| { + const val = o.isMarked; + o.isMarked = false; + return val; + }, + Obj.objUpvalue => |o| { + const val = o.isMarked; + o.isMarked = false; + return val; + }, + } + } +}; + pub const ObjString = struct { length: usize, chars: []const u8, + isMarked: bool = false, + + fn markObject(self: *ObjString, om: *ObjManager) void { + if (self.isMarked) return; + self.isMarked = true; + om.grayStack.append(Obj{ .objString = self }) catch return; + } + + fn blackenObject(_: *ObjString, _: *ObjManager) void { + // no-op + } }; pub const ObjFunction = struct { @@ -18,10 +104,40 @@ pub const ObjFunction = struct { upvalueCount: u8, chunk: *Chunk, name: ?*ObjString = null, + isMarked: bool = false, + + pub fn markObject(self: *ObjFunction, om: *ObjManager) void { + if (self.isMarked) return; + self.isMarked = true; + om.grayStack.append(Obj{ .objFunction = self }) catch return; + } + + fn blackenObject(self: *ObjFunction, om: *ObjManager) void { + if (self.name) |name| { + name.markObject(om); + } + // markArray + var i: u8 = 0; + while (i < self.chunk.values.items.len) { + markValue(self.chunk.values.items[i], om); + i += 1; + } + } }; pub const ObjNative = struct { function: NativeFunction, + isMarked: bool = false, + + fn markObject(self: *ObjNative, om: *ObjManager) void { + if (self.isMarked) return; + self.isMarked = true; + om.grayStack.append(Obj{ .objNative = self }) catch return; + } + + fn blackenObject(_: *ObjNative, _: *ObjManager) void { + // no-op + } }; pub const NativeFunction = fn (argCount: u8) Value; @@ -30,106 +146,106 @@ pub const ObjClosure = struct { function: *ObjFunction, upvalueCount: u8, upvalues: []*ObjUpvalue, + isMarked: bool = false, + + fn markObject(self: *ObjClosure, om: *ObjManager) void { + if (self.isMarked) return; + self.isMarked = true; + om.grayStack.append(Obj{ .objClosure = self }) catch return; + } + + fn blackenObject(self: *ObjClosure, om: *ObjManager) void { + self.function.blackenObject(om); + var i: u8 = 0; + while (i < self.upvalueCount) { + self.upvalues[i].blackenObject(om); + i += 1; + } + } }; pub const ObjUpvalue = struct { location: *Value, next: ?*ObjUpvalue, closed: Value, + isMarked: bool = false, + + fn markObject(self: *ObjUpvalue, om: *ObjManager) void { + if (self.isMarked) return; + self.isMarked = true; + om.grayStack.append(Obj{ .objUpvalue = self }) catch return; + } + + fn blackenObject(self: *ObjUpvalue, om: *ObjManager) void { + markValue(self.closed, om); + } }; +const L = std.SinglyLinkedList(Obj); + pub const ObjManager = struct { allocator: Allocator, - objects: std.ArrayList(*ObjString), - objectFns: std.ArrayList(*ObjFunction), - objectNatives: std.ArrayList(*ObjNative), - objectClosures: std.ArrayList(*ObjClosure), - objectUpvalues: std.ArrayList(*ObjUpvalue), + + objects: L, + strings: std.StringHashMap(*ObjString), globals: std.StringHashMap(Value), + // GC + grayStack: std.ArrayList(Obj), + + // references set up by callers, to support GC + _vm: ?*VM, + isCompilerInitialized: bool, + pub fn init(allocator: Allocator) !*ObjManager { const om: *ObjManager = try allocator.create(ObjManager); om.allocator = allocator; - om.objects = std.ArrayList(*ObjString).init(allocator); - om.objectFns = std.ArrayList(*ObjFunction).init(allocator); - om.objectNatives = std.ArrayList(*ObjNative).init(allocator); - om.objectClosures = std.ArrayList(*ObjClosure).init(allocator); - om.objectUpvalues = std.ArrayList(*ObjUpvalue).init(allocator); + + om.objects = L{}; + om.strings = std.StringHashMap(*ObjString).init(allocator); om.globals = std.StringHashMap(Value).init(allocator); + + // Garbage collection + om.grayStack = std.ArrayList(Obj).init(allocator); + om._vm = null; // set by vm.interpret() + om.isCompilerInitialized = false; // set by compiler.compile() return om; } pub fn free(self: *ObjManager) void { - // free the ObjString's - const ownedSlice = self.objects.toOwnedSlice(); - for (ownedSlice) |o| { - self.allocator.free(o.chars); - self.allocator.destroy(o); - } - self.allocator.free(ownedSlice); - - // free the ObjFunction's - const ownedSlice2 = self.objectFns.toOwnedSlice(); - for (ownedSlice2) |o| { - // o.chunk.free(); // TODO: This is the segfault - self.allocator.destroy(o.chunk); - self.allocator.destroy(o); - } - self.allocator.free(ownedSlice2); + self.strings.deinit(); // interned strings hash table + // self.globals.deinit(); // globals table // TODO - // free the ObjNative's - const ownedSlice3 = self.objectNatives.toOwnedSlice(); - for (ownedSlice3) |o| { - self.allocator.destroy(o); - } - self.allocator.free(ownedSlice3); + // free the nodes in the object linked list + var it: ?*L.Node = self.objects.first; + while (it != null) { + const node = it.?; + it = node.next; - // free the ObjClosures - const ownedSlice4 = self.objectClosures.toOwnedSlice(); - for (ownedSlice4) |o| { - self.allocator.free(o.upvalues); - self.allocator.destroy(o); - } - self.allocator.free(ownedSlice4); + // free the data within + node.data.free(self); - // free the ObjUpvalues - const ownedSlice5 = self.objectUpvalues.toOwnedSlice(); - for (ownedSlice5) |o| { - self.allocator.destroy(o); + // free the linked-list node + self.objects.remove(node); // optional + self.allocator.destroy(node); } - self.allocator.free(ownedSlice5); - - // free the objects arraylist - self.objects.deinit(); - - // free the objectFns arraylist - self.objectFns.deinit(); - - // free the objectFns arraylist - self.objectNatives.deinit(); - // free the objectFns arraylist - self.objectClosures.deinit(); - - // free the objectFns arraylist - self.objectUpvalues.deinit(); - - // free the strings hash table - self.strings.deinit(); + // cleanup the grayStack (GC) + self.grayStack.deinit(); // free the ObjManager itself self.allocator.destroy(self); } fn allocateString(self: *ObjManager, chars: []const u8, length: usize) !*ObjString { + if (debug.STRESS_GC) self.collectGarbage(); var string: *ObjString = try self.allocator.create(ObjString); string.length = length; string.chars = chars; - // capture this object, so we can free later - try self.objects.append(string); + try self.trackObject(Obj{ .objString = string }); // intern the string on creation try self.strings.put(chars, string); @@ -161,59 +277,179 @@ pub const ObjManager = struct { } pub fn newFunction(self: *ObjManager) !*ObjFunction { + if (debug.STRESS_GC) self.collectGarbage(); var function: *ObjFunction = try self.allocator.create(ObjFunction); function.arity = 0; function.upvalueCount = 0; function.name = null; function.chunk = try Chunk.init(self.allocator); - // capture this object, so we can free later - try self.objectFns.append(function); + try self.trackObject(Obj{ .objFunction = function }); return function; } pub fn newNative(self: *ObjManager, function: NativeFunction) !*ObjNative { + if (debug.STRESS_GC) self.collectGarbage(); var native: *ObjNative = try self.allocator.create(ObjNative); native.function = function; - // capture this object, so we can free later - try self.objectNatives.append(native); + try self.trackObject(Obj{ .objNative = native }); return native; } pub fn newClosure(self: *ObjManager, function: *ObjFunction) !*ObjClosure { + if (debug.STRESS_GC) self.collectGarbage(); const upvalues: []*ObjUpvalue = try self.allocator.alloc(*ObjUpvalue, function.upvalueCount); - //// skipping nulling them out ala C-programming - // for (int i = 0; i < function->upvalueCount; i++) { - // upvalues[i] = NULL; - // } + + var i: usize = 0; + while (i < function.upvalueCount) { + try self.trackObject(Obj{ .objUpvalue = upvalues[i] }); + i += 1; + } var closure: *ObjClosure = try self.allocator.create(ObjClosure); closure.function = function; closure.upvalues = upvalues; closure.upvalueCount = function.upvalueCount; - // capture this object, so we can free later - try self.objectClosures.append(closure); + try self.trackObject(Obj{ .objClosure = closure }); return closure; } pub fn newUpvalue(self: *ObjManager, slot: *Value) !*ObjUpvalue { + if (debug.STRESS_GC) self.collectGarbage(); var upvalue: *ObjUpvalue = try self.allocator.create(ObjUpvalue); upvalue.location = slot; upvalue.next = null; upvalue.closed = Value{ .nil = undefined }; - // capture this object, so we can free later - try self.objectUpvalues.append(upvalue); + try self.trackObject(Obj{ .objUpvalue = upvalue }); return upvalue; } + + fn trackObject(self: *ObjManager, obj: Obj) !void { + var node: *L.Node = try self.allocator.create(L.Node); + node.data = obj; + self.objects.prepend(node); + } + + fn collectGarbage(self: *ObjManager) void { + if (debug.LOG_GC) print("-- gc begin\n", .{}); + // when running some unit tests, we don't have a VM or compiler + if (self._vm != null) { + self.markRoots(); + self.traceReferences(); + self.sweep(); + } + // TODO: This is failing in unit tests + if (self.isCompilerInitialized) compiler.markCompilerRoots(); + if (debug.LOG_GC) print("-- gc end\n", .{}); + } + + fn markRoots(self: *ObjManager) void { + const vm = self._vm.?; + // values in Stack + if (debug.LOG_GC) print("-- gc: mark values in Stack\n", .{}); + var i: u8 = 0; + while (i < vm.stack.items.len) { + markValue(vm.stack.items[i], self); + i += 1; + } + + // closures in the CallFrames + if (debug.LOG_GC) print("-- gc: mark closures in the CallFrames\n", .{}); + var j: u8 = 0; + while (j < vm.frameCount) { + vm.frames[j].closure.markObject(self); + j += 1; + } + + // upvalues + if (debug.LOG_GC) print("-- gc: mark upvalues\n", .{}); + var upvalue = vm.openUpvalues; + while (upvalue) |u| { + u.markObject(self); + upvalue = u.next; + } + + // global variables + if (debug.LOG_GC) print("-- gc: mark global variables\n", .{}); + markValuesTable(self.globals, self); + } + + fn traceReferences(self: *ObjManager) void { + if (debug.LOG_GC) print("-- gc: trace references\n", .{}); + while (self.grayStack.popOrNull()) |obj| { + switch (obj) { + Obj.objString => |v| v.blackenObject(self), + Obj.objFunction => |v| v.blackenObject(self), + Obj.objNative => |v| v.blackenObject(self), + Obj.objClosure => |v| v.blackenObject(self), + Obj.objUpvalue => |v| v.blackenObject(self), + } + } + } + + fn sweep(self: *ObjManager) void { + if (debug.LOG_GC) print("-- gc: sweep (start) \n", .{}); + // free the nodes in the object linked list + var it: ?*L.Node = self.objects.first; + while (it != null) { + const node = it.?; + it = node.next; + + const marked = node.data.checkIsMarkedAndUnset(); + if (!marked) { + if (debug.LOG_GC) print("-- gc: sweep: free node {any}\n", .{node.data}); + node.data.free(self); + self.objects.remove(node); + self.allocator.destroy(node); + } + } + + if (debug.LOG_GC) print("-- gc: sweep (end) \n", .{}); + } }; +fn markValue(value: Value, om: *ObjManager) void { + if (debug.LOG_GC) print("{*} mark ", .{&value}); + if (debug.LOG_GC) printValue(value); + if (debug.LOG_GC) print("\n", .{}); + + switch (value) { + Value.number => return, + Value.boolean => return, + Value.nil => return, + Value.objString => |v| v.markObject(om), + Value.objFunction => |v| v.markObject(om), + Value.objNative => |v| v.markObject(om), + Value.objClosure => |v| v.markObject(om), + Value.objUpvalue => |v| v.markObject(om), + } +} + +fn markValuesTable(a: std.StringHashMap(Value), om: *ObjManager) void { + var iterator = a.iterator(); + + while (iterator.next()) |entry| { + // skipping marking keys + markValue(entry.value_ptr.*, om); + } +} + +fn markStringsTable(a: std.StringHashMap(*ObjString), om: *ObjManager) void { + var iterator = a.iterator(); + + while (iterator.next()) |entry| { + // skipping marking keys + entry.value_ptr.*.markObject(om); + } +} + test "Copy a string" { const allocator = std.testing.allocator; diff --git a/src/vm.zig b/src/vm.zig index 5e56b55..778457f 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -12,7 +12,6 @@ const valuesEqual = @import("./value.zig").valuesEqual; const printValue = @import("./value.zig").printValue; const ObjManager = @import("./object.zig").ObjManager; -const ObjString = @import("./object.zig").ObjString; const ObjClosure = @import("./object.zig").ObjClosure; const ObjUpvalue = @import("./object.zig").ObjUpvalue; const NativeFunction = @import("./object.zig").NativeFunction; @@ -65,6 +64,7 @@ pub const VM = struct { ip: usize, // instruction pointer openUpvalues: ?*ObjUpvalue, // linked list of open upvalues + // TODO: return a pointer to a VM? set objManager -> VM ref? pub fn init(a: Allocator) !VM { var vm = VM{ .objManager = try ObjManager.init(a), @@ -91,6 +91,8 @@ pub const VM = struct { pub fn interpret(self: *VM, source: []u8) !InterpretResult { const function = compiler.compile(source, self.objManager) catch return InterpretResult.CompileError; + // register VM with object manager, so it can collect garbage based on VM's stack + self.objManager._vm = self; const fnVal = Value{ .objFunction = function }; try self.push(fnVal); // garbage collection dance