implementing garbage collection (ch26)

This commit is contained in:
Patrick MARIE 2024-08-30 09:53:32 +02:00
parent 96e8b1742c
commit 2e6f977b34
13 changed files with 404 additions and 40 deletions

View File

@ -16,7 +16,7 @@ While reading [Crafting Interpreters](https://craftinginterpreters.com/), after
- [x] 23 - Jumping Back and Forth
- [x] 24 - Calls and Functions
- [x] 25 - Closures
- [ ] 26 - Garbage Collection
- [x] 26 - Garbage Collection
- [ ] 27 - Classes and Instances
- [ ] 28 - Method and Initializers
- [ ] 29 - Superclasses

21
samples/ch26_gc.lox Normal file
View File

@ -0,0 +1,21 @@
fun do_stuff(v) {
var s = v + v;
print s;
}
fun blah() {
var c = "ha" + "ha";
for(var i = 0; i < 100; i = i + 1) {
var d = "ha" + num2str(i);
do_stuff(d);
}
return "hiii";
}
var a = blah();
a = "updated";
// GC here.
print a;

View File

@ -5,12 +5,15 @@ const Allocator = std.mem.Allocator;
const Value = @import("./values.zig").Value;
const ValueArray = @import("./values.zig").ValueArray;
const OpCode = @import("./opcode.zig").OpCode;
const VM = @import("./vm.zig").VM;
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
const grow_capacity = @import("./utils.zig").grow_capacity;
const constants = @import("./constant.zig");
const utils = @import("./utils.zig");
pub const Chunk = struct {
allocator: Allocator,
vm: *VM,
count: usize,
capacity: usize,
@ -18,10 +21,11 @@ pub const Chunk = struct {
lines: []usize,
constants: ValueArray,
pub fn new(allocator: Allocator) *Chunk {
pub fn new(allocator: Allocator, vm: *VM) *Chunk {
var chunk: *Chunk = allocator.create(Chunk) catch unreachable;
chunk.allocator = allocator;
chunk.vm = vm;
chunk.count = 0;
chunk.capacity = 0;
chunk.code = &.{};
@ -45,7 +49,8 @@ pub const Chunk = struct {
pub fn write(self: *Chunk, byte: u8, line: usize) !void {
if (self.capacity < self.count + 1) {
const old_capacity = self.capacity;
self.capacity = grow_capacity(old_capacity);
self.capacity = ZloxAllocator.grow_capacity(old_capacity);
self.code = try self.allocator.realloc(self.code, self.capacity);
self.lines = try self.allocator.realloc(self.lines, self.capacity);
}
@ -151,7 +156,10 @@ pub const Chunk = struct {
}
pub fn add_constant(self: *Chunk, value: Value) !usize {
_ = try self.vm.push(value);
try self.constants.write(value);
_ = self.vm.pop();
return self.constants.count - 1;
}
};

View File

@ -41,7 +41,7 @@ const ParserRule = struct {
precedence: Precedence,
};
const Parser = struct {
pub const Parser = struct {
compiler: *Compiler,
current: ?Token,
previous: ?Token,
@ -865,7 +865,7 @@ const FunctionType = enum {
Script,
};
const Compiler = struct {
pub const Compiler = struct {
enclosing: ?*Compiler,
function: *Obj.Function,
@ -925,6 +925,8 @@ pub fn compile(vm: *VM, contents: []const u8) !?*Obj.Function {
var scanner = Scanner.init(contents);
var parser = Parser.new(vm, &compiler, &scanner);
vm.parser = &parser;
parser.advance();
while (!parser.match(TokenType.EOF)) {
@ -933,6 +935,7 @@ pub fn compile(vm: *VM, contents: []const u8) !?*Obj.Function {
const function = try parser.end_parser();
vm.parser = null;
if (!parser.had_error) {
return function;
} else {

View File

@ -10,7 +10,14 @@ pub const UINT8_COUNT = UINT8_MAX + 1;
pub const FRAMES_MAX = 64;
pub const STACK_MAX = (FRAMES_MAX * UINT8_MAX);
pub const DEBUG_PRINT_CODE = true;
pub const DEBUG_TRACE_EXECUTION = true;
pub const DEBUG_PRINT_CODE = false;
pub const DEBUG_TRACE_EXECUTION = false;
pub const DEBUG_PRINT_INTERNAL_STRINGS = false;
pub const DEBUG_PRINT_GLOBALS = false;
pub const DEBUG_STRESS_GC = true;
pub const DEBUG_LOG_GC = false;
pub const USE_CUSTON_ALLOCATOR = true;
pub const GC_HEAP_GROW_FACTOR = 2;

View File

@ -9,6 +9,8 @@ const OpCode = @import("./opcode.zig").OpCode;
const VM = @import("./vm.zig").VM;
const InterpretResult = @import("./vm.zig").InterpretResult;
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
// XXX imported to run tests.
const Table = @import("./table.zig");
@ -62,8 +64,15 @@ pub fn main() !void {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
var vm = VM.new(allocator);
vm.init_vm();
var vm = VM.new();
if (constants.USE_CUSTON_ALLOCATOR) {
var zlox_allocator_generator = ZloxAllocator.init(allocator, &vm);
const z_allocator = zlox_allocator_generator.allocator();
vm.init_vm(z_allocator);
} else {
vm.init_vm(allocator);
}
defer vm.destroy();
if (args.len == 1) {

286
src/memory.zig Normal file
View File

@ -0,0 +1,286 @@
const std = @import("std");
const VM = @import("./vm.zig").VM;
const Obj = @import("./object.zig").Obj;
const ObjType = @import("./object.zig").ObjType;
const Table = @import("./table.zig").Table;
const Entry = @import("./table.zig").Entry;
const Value = @import("./values.zig").Value;
const Compiler = @import("./compile.zig").Compiler;
const ValueArray = @import("./values.zig").ValueArray;
const constants = @import("./constant.zig");
pub const ZloxAllocator = struct {
parent_allocator: std.mem.Allocator,
vm: *VM,
bytes_allocated: usize,
next_gc: usize,
current_gc: bool,
const Self = @This();
pub fn init(parent_allocator: std.mem.Allocator, vm: *VM) Self {
return .{
.parent_allocator = parent_allocator,
.vm = vm,
.bytes_allocated = 0,
.next_gc = 1024,
.current_gc = false,
};
}
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn allocator(self: *Self) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
const self: *Self = @ptrCast(@alignCast(ctx));
const res = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr);
if (self.bytes_allocated > self.next_gc) {
self.collect_garbage();
}
self.bytes_allocated += len;
if (constants.DEBUG_LOG_GC) {
if (res == null) {
std.debug.print("GC: failed allocing buffer of size {d}\n", .{len});
} else {
std.debug.print("GC: allocing buffer {*} of size {d}\n", .{ res.?, len });
}
}
return res;
}
fn resize(ctx: *anyopaque, buf: []u8, log2_buf_align: u8, new_len: usize, ret_addr: usize) bool {
const self: *Self = @ptrCast(@alignCast(ctx));
if (constants.DEBUG_LOG_GC) {
std.debug.print("GC: resizing buffer {*} from size {d} to size {d}\n", .{ buf, buf.len, new_len });
}
self.bytes_allocated += new_len - buf.len;
if (self.bytes_allocated > self.next_gc or constants.DEBUG_STRESS_GC) {
self.collect_garbage();
}
return self.parent_allocator.rawResize(buf, log2_buf_align, new_len, ret_addr);
}
fn free(ctx: *anyopaque, buf: []u8, log2_buf_align: u8, ret_addr: usize) void {
const self: *Self = @ptrCast(@alignCast(ctx));
if (constants.DEBUG_LOG_GC) {
std.debug.print("GC: freeing buffer {*} of size {d} ({d}/{d})\n", .{ &buf, buf.len, self.bytes_allocated, self.next_gc });
}
return self.parent_allocator.rawFree(buf, log2_buf_align, ret_addr);
}
pub fn set_vm(self: *Self, vm: *VM) void {
self.vm = vm;
}
pub fn grow_capacity(capacity: usize) usize {
if (capacity < 8) {
return 8;
}
return capacity * 2;
}
pub fn collect_garbage(self: *Self) void {
if (self.current_gc) {
return;
}
if (comptime constants.DEBUG_LOG_GC == true) {
std.debug.print("\nGC: collect_garbage(): begin\n", .{});
}
self.current_gc = true;
self.mark_roots();
self.trace_references();
self.table_remove_white(&self.vm.strings);
self.sweep();
self.next_gc = self.bytes_allocated * constants.GC_HEAP_GROW_FACTOR;
if (comptime constants.DEBUG_LOG_GC == true) {
std.debug.print("GC: collect_garbage(): end\n\n", .{});
}
self.current_gc = false;
}
pub fn mark_roots(self: *Self) void {
for (0..self.vm.stack_top) |stack_idx| {
self.mark_value(&self.vm.stack[stack_idx]);
}
for (0..self.vm.frame_count) |frame_idx| {
self.mark_object(&self.vm.frames[frame_idx].closure.obj);
}
var upvalue = self.vm.open_upvalues;
while (upvalue != null) {
self.mark_object(&upvalue.?.obj);
upvalue = upvalue.?.next;
}
self.mark_table(&self.vm.globals);
self.mark_compiler_roots();
}
pub fn mark_value(self: *Self, value: *Value) void {
if (value.is_obj()) {
self.mark_object(value.as_obj());
}
}
pub fn mark_object(self: *Self, obj: *Obj) void {
if (obj.is_marked) {
return;
}
if (constants.DEBUG_LOG_GC) {
std.debug.print("GC: mark {*} ", .{obj});
obj.print();
std.debug.print("\n", .{});
}
obj.is_marked = true;
if (self.vm.gray_capacity < self.vm.gray_count + 1) {
self.vm.gray_capacity = grow_capacity(self.vm.gray_capacity);
self.vm.gray_stack = self.allocator().realloc(self.vm.gray_stack.?, self.vm.gray_capacity) catch {
@panic("failed to realloc gray stack");
};
}
// doing a realloc here will likely recall mark_roots and so on.
self.vm.gray_stack.?[self.vm.gray_count] = obj;
self.vm.gray_count += 1;
}
pub fn mark_table(self: *Self, table: *Table) void {
for (0..table.capacity) |idx| {
const entry = &table.entries[idx];
if (entry.key != null) {
self.mark_object(&entry.key.?.obj);
}
self.mark_value(&entry.value);
}
}
pub fn mark_compiler_roots(self: *Self) void {
if (self.vm.parser == null) {
return;
}
var compiler: ?*Compiler = self.vm.parser.?.compiler;
while (compiler != null) {
self.mark_object(&compiler.?.function.obj);
compiler = compiler.?.enclosing;
}
}
pub fn trace_references(self: *Self) void {
while (self.vm.gray_count > 0) {
self.vm.gray_count -= 1;
const obj: *Obj = self.vm.gray_stack.?[self.vm.gray_count];
self.blacken_object(obj);
}
}
pub fn blacken_object(self: *Self, obj: *Obj) void {
if (constants.DEBUG_LOG_GC) {
std.debug.print("GC: {*} blacken ", .{obj});
obj.print();
std.debug.print("\n", .{});
}
switch (obj.kind) {
ObjType.Native, ObjType.String => {},
ObjType.Upvalue => self.mark_value(&obj.as_upvalue().closed),
ObjType.Function => {
const function: *Obj.Function = obj.as_function();
if (function.name != null) {
self.mark_object(&function.name.?.obj);
}
self.mark_array(&function.chunk.constants);
},
ObjType.Closure => {
const closure: *Obj.Closure = obj.as_closure();
self.mark_object(&closure.function.obj);
for (0..closure.upvalue_count) |i| {
if (closure.upvalues[i] != null) {
self.mark_object(&closure.upvalues[i].?.obj);
}
}
},
}
}
pub fn mark_array(self: *Self, value_array: *ValueArray) void {
for (0..value_array.count) |i| {
self.mark_value(&value_array.values[i]);
}
}
pub fn table_remove_white(self: *Self, table: *Table) void {
_ = self;
for (0..table.capacity) |idx| {
const entry: *Entry = &table.entries[idx];
if (entry.key != null and !entry.key.?.obj.is_marked) {
_ = table.del(entry.key.?);
}
}
}
pub fn sweep(self: *Self) void {
var previous: ?*Obj = null;
var object: ?*Obj = self.vm.objects;
while (object != null) {
if (object.?.is_marked) {
object.?.is_marked = false;
previous = object;
object = object.?.next;
} else {
const unreached: *Obj = object.?;
object = object.?.next;
if (previous != null) {
previous.?.next = object;
} else {
self.vm.objects = object;
}
if (comptime constants.DEBUG_LOG_GC == true) {
std.debug.print("GC: sweeping {*}: ", .{unreached});
unreached.print();
std.debug.print("\n", .{});
}
unreached.destroy();
}
}
}
};

View File

@ -58,7 +58,7 @@ pub fn num2str(vm: *VM, arg_count: usize, args: []Value) Value {
return Value.nil_val();
};
const result = Obj.String.new(vm.allocator, str);
const result = Obj.String.new(vm, str);
return Value.obj_val(result);
return Value.obj_val(&result.obj);
}

View File

@ -22,6 +22,7 @@ pub const Obj = struct {
kind: ObjType,
allocator: Allocator,
next: ?*Obj,
is_marked: bool,
fn new(comptime T: type, vm: *VM, kind: ObjType) *T {
const created_obj = vm.allocator.create(T) catch unreachable;
@ -30,6 +31,7 @@ pub const Obj = struct {
.kind = kind,
.allocator = vm.allocator,
.next = vm.objects,
.is_marked = false,
};
vm.objects = &created_obj.obj;
@ -79,7 +81,7 @@ pub const Obj = struct {
function_obj.arity = 0;
function_obj.upvalue_count = 0;
function_obj.chunk = Chunk.new(vm.allocator);
function_obj.chunk = Chunk.new(vm.allocator, vm);
function_obj.name = null;
return function_obj;

View File

@ -6,13 +6,13 @@ const constants = @import("./constant.zig");
const Obj = @import("./object.zig").Obj;
const Value = @import("./values.zig").Value;
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
const grow_capacity = @import("./utils.zig").grow_capacity;
const compute_hash = @import("./utils.zig").compute_hash;
const Entry = struct {
key: ?*Obj.String,
value: Value,
pub const Entry = struct {
key: ?*Obj.String = null,
value: Value = Value.nil_val(),
};
pub const Table = struct {
@ -43,7 +43,7 @@ pub const Table = struct {
const current_capacity: f32 = @floatFromInt(self.capacity);
if (current_count > current_capacity * constants.TABLE_MAX_LOAD) {
const capacity = grow_capacity(self.capacity);
const capacity = ZloxAllocator.grow_capacity(self.capacity);
self.adjust_capacity(capacity);
}
@ -90,11 +90,10 @@ pub const Table = struct {
}
pub fn adjust_capacity(self: *Table, capacity: usize) void {
var entries = self.allocator.alloc(Entry, capacity) catch unreachable;
const entries = self.allocator.alloc(Entry, capacity) catch unreachable;
for (0..entries.len) |idx| {
entries[idx].key = null;
entries[idx].value = Value.nil_val();
for (entries) |*e| {
e.* = Entry{};
}
self.count = 0;

View File

@ -6,13 +6,6 @@ const Token = @import("./scanner.zig").Token;
const print_value = @import("./values.zig").print_value;
pub fn grow_capacity(capacity: usize) usize {
if (capacity < 8) {
return 8;
}
return capacity * 2;
}
pub fn simple_instruction(opcode_name: []const u8, offset: usize) usize {
debug.print("{s:<16}\n", .{opcode_name});

View File

@ -4,7 +4,7 @@ const debug = std.debug;
const Allocator = std.mem.Allocator;
const Obj = @import("./object.zig").Obj;
const utils = @import("./utils.zig");
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
pub const ValueType = enum {
Bool,
@ -158,7 +158,7 @@ pub const ValueArray = struct {
pub fn write(self: *ValueArray, value: Value) !void {
if (self.capacity < self.count + 1) {
const old_capacity = self.capacity;
self.capacity = utils.grow_capacity(old_capacity);
self.capacity = ZloxAllocator.grow_capacity(old_capacity);
self.values = try self.allocator.realloc(self.values, self.capacity);
}

View File

@ -4,11 +4,14 @@ const Allocator = std.mem.Allocator;
const constants = @import("./constant.zig");
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
const Chunk = @import("./chunk.zig").Chunk;
const OpCode = @import("./opcode.zig").OpCode;
const Value = @import("./values.zig").Value;
const Obj = @import("./object.zig").Obj;
const ObjType = @import("./object.zig").ObjType;
const Parser = @import("./compile.zig").Parser;
const NativeFn = @import("./object.zig").NativeFn;
const Table = @import("./table.zig").Table;
@ -42,25 +45,40 @@ pub const VM = struct {
frame_count: usize,
open_upvalues: ?*Obj.Upvalue,
objects: ?*Obj,
parser: ?*Parser,
gray_count: usize,
gray_capacity: usize,
gray_stack: ?[]*Obj,
pub fn new(allocator: Allocator) VM {
return VM{
.allocator = allocator,
pub fn new() VM {
const vm = VM{
.allocator = undefined,
.stack = undefined,
.stack_top = 0,
.strings = Table.new(allocator),
.globals = Table.new(allocator),
.strings = undefined,
.globals = undefined,
.frames = undefined,
.frame_count = 0,
.open_upvalues = null,
.objects = null,
.parser = null,
.gray_capacity = 0,
.gray_count = 0,
.gray_stack = &.{},
};
return vm;
}
pub fn init_vm(self: *VM) void {
pub fn init_vm(self: *VM, allocator: Allocator) void {
self.allocator = allocator;
self.globals = Table.new(self.allocator);
self.strings = Table.new(self.allocator);
self.define_native("clock", natives.clock);
self.define_native("power", natives.power);
self.define_native("str2num", natives.str2num);
self.define_native("num2str", natives.num2str);
}
pub fn destroy(self: *VM) void {
@ -71,6 +89,10 @@ pub const VM = struct {
self.strings.destroy();
self.globals.destroy();
self.destroy_objects();
if (self.gray_stack != null) {
self.allocator.free(self.gray_stack.?);
}
}
pub fn destroy_objects(self: *VM) void {
@ -82,6 +104,15 @@ pub const VM = struct {
}
}
pub fn dump_objects(self: *VM) void {
var obj = self.objects;
while (obj != null) {
const obj_next = obj.?.next;
std.debug.print("OBJ: {*}\n", .{obj.?});
obj = obj_next;
}
}
inline fn current_chunk(self: *VM) *Chunk {
return self.frames[self.frame_count - 1].closure.function.chunk;
}
@ -329,13 +360,15 @@ pub const VM = struct {
}
pub fn concatenate(self: *VM) !void {
const b = self.pop().as_cstring();
const a = self.pop().as_cstring();
const b = self.peek(0).as_cstring();
const a = self.peek(1).as_cstring();
const concat_str = try std.mem.concat(self.current_chunk().allocator, u8, &.{ a, b });
const string_obj = self.take_string(concat_str);
_ = self.pop();
_ = self.pop();
try self.push(Value.obj_val(&string_obj.obj));
}
@ -395,7 +428,10 @@ pub const VM = struct {
pub fn allocate_string(self: *VM, source: []const u8) *Obj.String {
const obj_string = Obj.String.new(self, source);
_ = try self.push(Value.obj_val(&obj_string.obj));
_ = self.strings.set(obj_string, Value.nil_val());
_ = self.pop();
return obj_string;
}