diff options
| -rw-r--r-- | src/zisp/gc.zig | 22 | ||||
| -rw-r--r-- | src/zisp/gc/ListPool.zig | 290 | ||||
| -rw-r--r-- | src/zisp/value.zig | 29 | ||||
| -rw-r--r-- | src/zisp/value/istr.zig | 39 |
4 files changed, 182 insertions, 198 deletions
diff --git a/src/zisp/gc.zig b/src/zisp/gc.zig index b36628f..9cb66d1 100644 --- a/src/zisp/gc.zig +++ b/src/zisp/gc.zig @@ -13,35 +13,37 @@ const HeapType = value.HeapType; var main_alloc: Alloc = undefined; -var list_heap_start: usize = undefined; -var main_heap_start: usize = undefined; -var istr_heap_start: usize = undefined; +const u32max = std.math.maxInt(u32); + +var list_heap: *[u32max]Value = undefined; +var main_heap: *[u32max]u64 = undefined; +var istr_heap: *[u32max]u8 = undefined; var main_list_pool: ListPool = undefined; var main_istr_pool: IstrPool = undefined; pub fn listPtrFromIdx(idx: u32) [*]Value { - return @ptrFromInt(list_heap_start + idx * 8); + return &list_heap[idx]; } pub fn heapPtrFromIdx(comptime typ: HeapType, idx: u32) typ.PtrType() { - return @ptrFromInt(main_heap_start + idx * 8); + return @ptrCast(&main_heap[idx]); } pub fn istrPtrFromIdx(idx: u32) [*]const u8 { - return @ptrFromInt(istr_heap_start + idx); + return &istr_heap[idx]; } pub fn listIdxFromPtr(ptr: [*]Value) u32 { - return @intCast(@intFromPtr(ptr - list_heap_start) / 8); + return @intCast(ptr - list_heap); } pub fn heapIdxFromPtr(ptr: HeapPtr) u32 { - return @intCast(@intFromPtr(ptr - main_heap_start) / 8); + return @intCast(ptr - main_heap); } pub fn istrIdxFromPtr(ptr: [*]const u8) u32 { - return @intCast(@intFromPtr(ptr - istr_heap_start)); + return @intCast(ptr - istr_heap); } pub fn allocHeap(comptime typ: HeapType, len: usize) !typ.PtrType() { @@ -92,5 +94,5 @@ pub fn mainListPool() *ListPool { pub fn mainIstrPool() *IstrPool { init() catch @panic("OOM"); // TODO this is only here for the test suite - return &main_istr_set; + return &main_istr_pool; } diff --git a/src/zisp/gc/ListPool.zig b/src/zisp/gc/ListPool.zig index 42bb50e..d63a6f8 100644 --- a/src/zisp/gc/ListPool.zig +++ b/src/zisp/gc/ListPool.zig @@ -1,99 +1,99 @@ -//! List allocation with optimization for short lists: -//! -//! The point of this is to ensure that whenever code consists of a bunch of -//! short lists, which is almost always the case for Lisp code, their elements -//! are allocated in tight bundles, without any padding, making them share a -//! cache line, or at least a page, whenever possible. -//! -//! To this effect, we allocate memory in blocks, and create the next whenever -//! the current one doesn't have enough space left for a requested allocation. -//! A block should be the size of a page or a multiple thereof. We have three -//! types of block; the first is the FlexiBlock which fulfills our goal of no -//! padding among short lists. -//! -//! Example of how a series of short list allocations (labeled a to e) may have -//! their elements allocated within a FlexiBlock: -//! -//! [ a0 a1 a2 b0 b1 b2 b3 b4 c0 d0 d1 e0 e1 e2 e3 __ __ __ __ ... ] -//! ^ ^ ^ ^ ^ ^ -//! a[3] b[5] c[1] d[2] e[4] FREE -> -//! -//! For list element counts of up to 8, each count is its own size class, and -//! they are allocated serially in a FlexiBlock without padding. Deallocation -//! pushes the freed heap index into a free-list for that exact list size. -//! -//! If the array 'b' were to be freed, its heap index (pointing to b0) would be -//! pushed onto free_lists[4] which holds the list of freed 5-element arrays in -//! already allocated blocks. -//! -//! Over time, each FlexiBlock ends up being "frozen" into a specific run of -//! size classes, since we never try to find adjacent freed arrays, but this -//! shouldn't be a problem since lists of length 1 to 8 are very frequent in -//! source code and should see high reuse. -//! -//! The second type of block is the ChunkBlock which introduces some waste to -//! reduce fragmentation for moderate-length lists. In the following, the -//! abbreviation 'CM' stands for ChunkMax: -//! -//! For element counts 9 to CM, we split blocks into a series of "chunks" where -//! each chunk has space for an array within a certain size class, such as 9 to -//! 12 elements, 13 to 16 elements, and so on. Thus, freeing a list of e.g. 11 -//! elements frees a chunk that can hold any list 9 to 12 elements. This helps -//! against fragmentation, since lists in these size ranges aren't as common as -//! the shorter ones, making free-lists for specific element counts impractical. -//! -//! A request for a list of e.g. 12 elements could still be fulfilled by chunks -//! larger than that, if the size class 9-12 is depleted. This avoids causing -//! too much wasted space if larger lists occur very rarely in source code. -//! -//! To perfectly fill a 4 KiB page, which can hold 512 Value (64-bit) elements, -//! we could combine various chunk sizes and counts (size classes). Currently, -//! the following is in use: -//! -//! 30 * 12 + 8 * 16 + 1 * 24 = 512 -//! -//! We could increase CM from 24 to e.g. 32, or use blocks that are multiple -//! pages in size, in which case the chunk split could take various different -//! forms; we could use empiric testing over large bodies of code to find an -//! optimum, but it's unlikely to make a difference since list sizes < 9 are -//! extremely dominant. -//! -//! Chunk use within the current (last allocated) block is kept track of via a -//! bit-map that indicates the used or not status of each chunk regardless of -//! size class. Finding the first available chunk for a given size class is -//! then a matter of (optionally) applying a bit-mask to mask out the bits for -//! the smaller size classes, followed with a bit-counting operation to locate -//! the index of the first suitable chunk. Since chunks have different sizes, -//! this index is mapped to an actual byte offset via a static lookup table. -//! -//! Upon deallocation, we can use 'MOD block_size' on the heap index of the -//! list, combined with a small series of less-than checks, to figure out the -//! size class. -//! -//! As an example: If the first group of chunks occupy elements [0, 24*12), and -//! we're given heap index J for a list to be deallocated, we can check if it's -//! in that first group by testing: J % 512 < 24*12 -//! -//! Finally, we have the ArrayBlock, which is simply an N-array of M-element -//! chunks. Currently, we use N = 64 with M = 32. This is probably not the -//! most useful optimization, but it's easy to implement anyway. -//! -//! Starting from 33 elements, we stop caring and call the underlying allocator -//! directly, since lists that long in source code are extremely rare. -//! -//! The main remaining issue that all of the above cannot solve is that if we -//! free and allocate new code repeatedly (e.g. of whole modules), each time -//! there will be bits and pieces that end up in a completely different place -//! due to the frozen nature of FlexiBlocks. E.g. freeing a module may have -//! freed 25 arrays of length 6, but the newly loaded code needs 28 arrays of -//! length 6, so three of them land somewhere far away. -//! -//! This shouldn't affect programs that simply load up all their modules, run -//! forever without dynamic modification, and thus never deallocate code. -//! -//! Otherwise, a simple solution is to use separate ListPool instances for -//! modules, or just forget about all of this and rely on a compacting GC. -//! +/// List allocation with optimization for short lists: +/// +/// The point of this is to ensure that whenever code consists of a bunch of +/// short lists, which is almost always the case for Lisp code, their elements +/// are allocated in tight bundles, without any padding, making them share a +/// cache line, or at least a page, whenever possible. +/// +/// To this effect, we allocate memory in blocks, and create the next whenever +/// the current one doesn't have enough space left for a requested allocation. +/// A block should be the size of a page or a multiple thereof. We have three +/// types of block; the first is the FlexiBlock which fulfills our goal of no +/// padding among short lists. +/// +/// Example of how a series of short list allocations (labeled a to e) may have +/// their elements allocated within a FlexiBlock: +/// +/// [ a0 a1 a2 b0 b1 b2 b3 b4 c0 d0 d1 e0 e1 e2 e3 __ __ __ __ ... ] +/// ^ ^ ^ ^ ^ ^ +/// a[3] b[5] c[1] d[2] e[4] FREE -> +/// +/// For list element counts of up to 8, each count is its own size class, and +/// they are allocated serially in a FlexiBlock without padding. Deallocation +/// pushes the freed heap index onto an intrusive free-list for that list size. +/// +/// If the array 'b' were to be freed, its heap index (pointing to b0) would be +/// pushed onto free_lists[4] which points to the last freed 5-element array in +/// an already allocated block. +/// +/// Over time, each FlexiBlock ends up being "frozen" into a specific run of +/// size classes, since we never try to find adjacent freed arrays, but this +/// shouldn't be a problem since lists of length 1 to 8 are very frequent in +/// source code and should see high reuse. +/// +/// The second type of block is the ChunkBlock which introduces some waste to +/// reduce fragmentation for moderate-length lists. In the following, the +/// abbreviation 'CM' stands for ChunkMax: +/// +/// For element counts 9 to CM, we split blocks into a series of "chunks" where +/// each chunk has space for an array within a certain size class, such as 9 to +/// 12 elements, 13 to 16 elements, and so on. Thus, freeing a list of e.g. 11 +/// elements frees a chunk that can hold any list 9 to 12 elements. This helps +/// against fragmentation, since lists in these size ranges aren't as common as +/// the shorter ones, making free-lists for specific element counts impractical. +/// +/// A request for a list of e.g. 12 elements could still be fulfilled by chunks +/// larger than that, if the size class 9-12 is depleted. This avoids causing +/// too much wasted space if larger lists occur very rarely in source code. +/// +/// To perfectly fill a 4 KiB page, which can hold 512 Value (64-bit) elements, +/// we could combine various chunk sizes and counts (size classes). Currently, +/// the following is in use: +/// +/// 30 * 12 + 8 * 16 + 1 * 24 = 512 +/// +/// We could increase CM from 24 to e.g. 32, or use blocks that are multiple +/// pages in size, in which case the chunk split could take various different +/// forms; we could use empiric testing over large bodies of code to find an +/// optimum, but it's unlikely to make a difference since list sizes < 9 are +/// extremely dominant. +/// +/// Chunk use within the current (last allocated) block is kept track of via a +/// bit-map that indicates the used or not status of each chunk regardless of +/// size class. Finding the first available chunk for a given size class is +/// then a matter of (optionally) applying a bit-mask to mask out the bits for +/// the smaller size classes, followed with a bit-counting operation to locate +/// the index of the first suitable chunk. Since chunks have different sizes, +/// this index is mapped to an actual byte offset via a static lookup table. +/// +/// Upon deallocation, we can use 'MOD page_size' on a pointer, combined with a +/// small series of less-than checks, to figure out the size class. +/// +/// As an example: If the first group of chunks occupy elements [0, 30*12), and +/// we're given pointer P for a list to be deallocated, we can check if it's in +/// that first group by testing: P % 4096 < 30 * 12 * 8 (elements are 8 bytes). +/// +/// Finally, we have the ArrayBlock, which is simply an N-array of M-element +/// chunks. Currently, we use N = 64 with M = 32. This is probably not the +/// most useful optimization, but it's easy to implement anyway. +/// +/// Starting from 33 elements, we stop caring and call the underlying allocator +/// directly, since lists that long in source code are extremely rare. +/// +/// The main remaining issue that all of the above cannot solve is that if we +/// free and allocate new code repeatedly (e.g. of whole modules), each time +/// there will be bits and pieces that end up in a completely different place +/// due to the frozen nature of FlexiBlocks. E.g. freeing a module may have +/// freed 25 arrays of length 6, but the newly loaded code needs 28 arrays of +/// length 6, so three of them land somewhere far away. +/// +/// This shouldn't affect programs that simply load up all their modules, run +/// forever without dynamic modification, and thus never deallocate code. +/// +/// Otherwise, a simple solution is to use separate ListPool instances for +/// modules, or just forget about all of this and rely on a compacting GC. +/// +const Self = @This(); const std = @import("std"); @@ -109,8 +109,6 @@ const SegStack = seg_stack.SegStack; const i2p = gc.listPtrFromIdx; const p2i = gc.listIdxFromPtr; -const Self = @This(); - // 4 KiB blocks fit 512 Value elements, which should be good. const block_size = 4096 / @sizeOf(Value); const Block = [block_size]Value; @@ -139,6 +137,12 @@ const chunk_empty_big: u64 = (2 ^ 1 - 1) << 38; const array_block_chunk_size = 32; const array_block_max_index = block_size / array_block_chunk_size; +const FreeListNode = packed union(u64) { + bits: u64, + vals_ptr: [*]Value, + prev_ptr: *FreeListNode, +}; + alloc: Alloc, cur_flexi_block: *Block, @@ -153,9 +157,9 @@ flexi_block_stack: SegStack(*Block, 512), chunk_block_stack: SegStack(*Block, 256), array_block_stack: SegStack(*Block, 64), -flexi_free_lists: [8]SegStack(u32, 256), -chunk_free_lists: [3]SegStack(u32, 256), -array_free_list: SegStack(u32, 64), +flexi_free_lists: [8]FreeListNode, +chunk_free_lists: [3]FreeListNode, +array_free_list: FreeListNode, pub fn init(alloc: Alloc) !Self { return .{ @@ -166,30 +170,13 @@ pub fn init(alloc: Alloc) !Self { .flexi_block_stack = try .init(alloc), .chunk_block_stack = try .init(alloc), .array_block_stack = try .init(alloc), - .flexi_free_lists = .{ - try .init(alloc), - try .init(alloc), - try .init(alloc), - try .init(alloc), - try .init(alloc), - try .init(alloc), - try .init(alloc), - try .init(alloc), - }, - .chunk_free_lists = .{ - try .init(alloc), - try .init(alloc), - try .init(alloc), - }, - .array_free_list = try .init(alloc), + .flexi_free_lists = @splat(.{ .bits = 0 }), + .chunk_free_lists = @splat(.{ .bits = 0 }), + .array_free_list = .{ .bits = 0 }, }; } pub fn deinit(self: *Self) void { - self.array_free_list.deinit(self.alloc); - for (self.chunk_free_lists) |l| l.deinit(self.alloc); - for (self.flexi_free_lists) |l| l.deinit(self.alloc); - while (self.array_block_stack.pop(self.alloc)) |b| self.alloc.destroy(b); while (self.chunk_block_stack.pop(self.alloc)) |b| self.alloc.destroy(b); while (self.flexi_block_stack.pop(self.alloc)) |b| self.alloc.destroy(b); @@ -203,9 +190,21 @@ pub fn deinit(self: *Self) void { self.alloc.destroy(self.cur_flexi_block); } +fn pushFree(list: *FreeListNode, len: u8, ptr: [*]Value) void { + ptr[0].bits = list.bits; + list.vals_ptr = ptr; +} + +fn popFree(list: *FreeListNode, len: u8) ?[*]Value { + if (list.bits == 0) return null; + const ptr = list.vals_ptr; + list.* = list.prev_ptr.*; + return ptr; +} + fn newFlexiBlock(self: *Self, rem_len: u8) !void { if (rem_len != 0) { - self.flexi_free_lists[rem_len - 1].push(self.curFlexiBlockPtr()); + pushFree(&self.flexi_free_lists[rem_len - 1], self.curFlexiBlockPtr()); } try self.flexi_block_stack.push(self.alloc, self.cur_flexi_block); self.cur_flexi_block = try self.alloc.create(Block); @@ -218,16 +217,13 @@ fn newChunkBlock(self: *Self) !void { while (empty != 0) { const idx = @ctz(chunks); if (idx < 30) { - const heap_idx = p2i(curChunkBlockPtr(idx)); - self.chunk_free_lists[0].push(self.alloc, heap_idx); + pushFree(&self.chunk_free_lists[0], curChunkBlockPtr(idx)); empty &= ~@shlExact(@as(u64, 1), @intCast(idx)); } else if (idx < 38) { - const heap_idx = p2i(curChunkBlockPtr(idx)); - self.chunk_free_lists[1].push(self.alloc, heap_idx); + pushFree(&self.chunk_free_lists[1], curChunkBlockPtr(idx)); empty &= ~@shlExact(@as(u64, 1), @intCast(idx)); } else { - const heap_idx = p2i(curChunkBlockPtr(38)); - self.chunk_free_lists[2].push(self.alloc, heap_idx); + pushFree(&self.chunk_free_lists[2], curChunkBlockPtr(38)); break; } } @@ -266,7 +262,7 @@ pub fn allocVals(self: *Self, len: u8) ![*]Value { if (len > 24) { @branchHint(.unlikely); - if (self.array_free_list.pop(self.alloc)) |i| return i2p(i); + if (popFree(&self.array_free_list)) |p| return p; if (self.cur_array_index == array_block_max_index) { try self.newArrayBlock(); } @@ -276,7 +272,7 @@ pub fn allocVals(self: *Self, len: u8) ![*]Value { if (len > 16) { @branchHint(.unlikely); - if (self.chunk_free_lists[2].pop(self.alloc)) |i| return i2p(i); + if (popFree(&self.chunk_free_lists[2])) |p| return p; if ((self.cur_chunk_empty & chunk_empty_big) == 0) { try self.newChunkBlock(); } @@ -286,8 +282,8 @@ pub fn allocVals(self: *Self, len: u8) ![*]Value { if (len > 12) { @branchHint(.unlikely); - if (self.chunk_free_lists[1].pop(self.alloc)) |i| return i2p(i); - if (self.chunk_free_lists[2].pop(self.alloc)) |i| return i2p(i); + if (popFree(&self.chunk_free_lists[1])) |p| return p; + if (popFree(&self.chunk_free_lists[2])) |p| return p; const empty = self.cur_chunk_empty & ~chunk_empty_sml; if (empty != 0) { const idx: u8 = @ctz(empty); @@ -301,9 +297,9 @@ pub fn allocVals(self: *Self, len: u8) ![*]Value { } if (len > 8) { - if (self.chunk_free_lists[0].pop(self.alloc)) |i| return i2p(i); - if (self.chunk_free_lists[1].pop(self.alloc)) |i| return i2p(i); - if (self.chunk_free_lists[2].pop(self.alloc)) |i| return i2p(i); + if (popFree(&self.chunk_free_lists[0])) |p| return p; + if (popFree(&self.chunk_free_lists[1])) |p| return p; + if (popFree(&self.chunk_free_lists[2])) |p| return p; const empty = self.cur_chunk_empty; if (empty != 0) { const idx: u8 = @ctz(empty); @@ -316,7 +312,7 @@ pub fn allocVals(self: *Self, len: u8) ![*]Value { } } - if (self.flexi_free_lists[len - 1].pop(self.alloc)) |i| return i2p(i); + if (popFree(&self.flexi_free_lists[len - 1])) |p| return p; const rem_len = self.cur_flexi_block.len - self.cur_flexi_index; if (len > rem_len) try self.newFlexiBlock(rem_len); @@ -331,23 +327,27 @@ pub fn freeVals(self: *Self, len: u8, ptr: [*]Value) !void { @branchHint(.unlikely); return self.alloc.free(ptr); } - const idx = p2i(ptr); if (len > 24) { @branchHint(.unlikely); - return self.array_free_list.push(self.alloc, idx); + return pushFree(&self.array_free_list, ptr); } if (len > 16) { @branchHint(.unlikely); - return self.chunk_free_lists[2].push(self.alloc, idx); + return pushFree(&self.chunk_free_lists[2], ptr); } - if (len > 12) { - @branchHint(.unlikely); - return self.chunk_free_lists[1].push(self.alloc, idx); + if (len <= 8) { + @branchHint(.likely); + return pushFree(&self.flexi_free_lists[len - 1], ptr); } - if (len > 8) { - return self.chunk_free_lists[0].push(self.alloc, idx); + // Find real size class, since 9-12 and 13-16 can occupy larger chunks. + const pos_in_page = @intFromPtr(ptr) % 4096; + if (pos_in_page < 30 * 12 * 8) { + return pushFree(&self.chunk_free_lists[0], ptr); + } else if (pos_in_page < 38 * 16 * 8) { + return pushFree(&self.chunk_free_lists[1], ptr); + } else { + return pushFree(&self.chunk_free_lists[2], ptr); } - return self.flexi_free_lists[len - 1].push(self.alloc, idx); } pub fn freeList(self: *Self, list: Value) !void { diff --git a/src/zisp/value.zig b/src/zisp/value.zig index 1a63349..f075b35 100644 --- a/src/zisp/value.zig +++ b/src/zisp/value.zig @@ -73,12 +73,13 @@ pub const none = Value{ .misc = .{ .value = .none } }; // zig fmt: on /// A plain (unpacked, untagged, uncompressed) pointer into the main heap. -pub const HeapPtr = *align(8) anyopaque; +pub const HeapPtr = *align(16) anyopaque; /// Values for the 8 type bits on main heap pointers. pub const HeapType = enum(u8) { /// Pair (car, cdr) pair, + /// Array of various types: see `ArrayPtr`. array, @@ -115,13 +116,11 @@ pub const hi16_list = hi16(0b0000); pub const hi16_hptr = hi16(0b0001); pub const hi16_istr = hi16(0b0010); pub const hi16_sstr = hi16(0b0011); -pub const hi16_rpos = hi16(0b0100); -pub const hi16_rneg = hi16(0b0101); -pub const hi16_ndef = hi16(0b0110); +pub const hi16_srat = hi16(0b0100); +pub const hi16_ndf1 = hi16(0b0101); +pub const hi16_ndf2 = hi16(0b0110); pub const hi16_rune = hi16(0b0111); -pub const hi15_srat: u15 = @intCast(hi16(0b0100) >> 1); - pub const hi16_const_list = hi16(0b1000); pub const hi16_const_hptr = hi16(0b1001); pub const hi16_const_istr = hi16(0b1010); @@ -195,15 +194,21 @@ pub const Value = packed union { /// Small rat (rational number) srat: packed struct { - q: u24, - p: i25, - _hi: u15 = hi15_srat, + p: i32, + q: u16, + _hi: u16 = hi16_srat, + }, + + /// Undefined 1 + ndf1: packed struct { + _: u48, + _hi: u16 = hi16_ndf1, }, - /// Undefined, for now - ndef: packed struct { + /// Undefined 2 + ndf2: packed struct { _: u48, - _hi: u16 = hi16_ndef, + _hi: u16 = hi16_ndf2, }, /// Rune (6-byte ASCII string) diff --git a/src/zisp/value/istr.zig b/src/zisp/value/istr.zig index a581289..528280a 100644 --- a/src/zisp/value/istr.zig +++ b/src/zisp/value/istr.zig @@ -1,6 +1,6 @@ //! Intermediate-length String //! -//! Prefixed with a 1-byte length. Typically interned in an IstrSet. +//! Length up to 255, stored in pointer. Typically interned in an IstrPool. const std = @import("std"); @@ -9,36 +9,13 @@ const Alloc = std.mem.Allocator; const gc = @import("../gc.zig"); const value = @import("../value.zig"); -const IstrSet = gc.IstrSet; +const IstrPool = gc.IstrPool; const Value = value.Value; pub const max_len = 255; // Zig API -/// Pointer to an interned string. First byte is length. -pub const IstrPtr = *IstrHead; - -pub const IstrHead = packed struct(u8) { - len: u8, - - fn bufU8(self: *@This()) [*]u8 { - return @ptrCast(self); - } - - pub fn bytes(self: *@This()) []const u8 { - const start = @sizeOf(IstrHead); - const len: usize = self.len; - return self.bufU8()[start .. start + len]; - } - - pub fn putStr(self: *@This(), s: []const u8) void { - const start = @sizeOf(IstrHead); - self.len = @intCast(s.len); - @memcpy(self.bufU8()[start .. start + s.len], s); - } -}; - pub fn check(v: Value) bool { return v.isIstr(); } @@ -70,12 +47,12 @@ pub fn new(alloc: Alloc, s: []const u8) !IstrPtr { } pub fn getOrNew(s: []const u8) !IstrPtr { - return getOrNewInSet(gc.mainIstrSet(), s); + return getOrNewInPool(gc.mainIstrPool(), s); } -pub fn getOrNewInSet(set: *IstrSet, s: []const u8) !IstrPtr { +pub fn getOrNewInPool(pool: *IstrPool, s: []const u8) !IstrPtr { assertValidIstr(s); - return try set.getOrNew(s); + return try pool.getOrNew(s); } pub fn pack(istr: IstrPtr) Value { @@ -104,10 +81,10 @@ pub fn getLen(v: Value) Value { return value.fixnum.pack(istr.len); } -// TODO: Zisp representation of IstrSet & ability to intern in given IstrSet +// TODO: Zisp representation of IstrPool & ability to intern in given IstrPool pub fn intern(v: Value) Value { const istr = assert(v); - const set = gc.mainIstrSet(); - return pack(set.getOrPut(istr)); + const pool = gc.mainIstrPool(); + return pack(pool.getOrPut(istr)); } |
