arena.zig (5908B)
1 const std = @import("std"); 2 const tree = @import("tree.zig"); 3 4 pub const ReductionFrame = struct { 5 kind: enum { function, discriminator, argument, update }, 6 redex: u32, 7 }; 8 9 pub const Arena = struct { 10 allocator: std.mem.Allocator, 11 nodes: std.ArrayList(tree.Node), 12 // Opt-in collection: every externally retained ID must be in roots. 13 gc_enabled: bool = false, 14 roots: std.ArrayList(u32) = .empty, 15 free: std.ArrayList(u32) = .empty, 16 allocations: usize = 0, 17 // Allocation headroom, not a memory limit. Tests can select a small policy 18 // to force collections at suspension/allocation boundaries. 19 gc_min_interval: usize = 1 << 20, 20 gc_live_multiplier: usize = 1, 21 gc_interval: usize = 1 << 20, 22 collections: usize = 0, 23 last_live: usize = 0, 24 marks: std.ArrayList(bool) = .empty, 25 grey: std.ArrayList(u32) = .empty, 26 reduction_frames: std.ArrayList(ReductionFrame) = .empty, 27 reduction_roots: std.ArrayList(u32) = .empty, 28 29 pub fn init(allocator: std.mem.Allocator) Arena { 30 return .{ 31 .allocator = allocator, 32 .nodes = .empty, 33 }; 34 } 35 36 pub fn deinit(self: *Arena) void { 37 self.nodes.deinit(self.allocator); 38 self.roots.deinit(self.allocator); 39 self.free.deinit(self.allocator); 40 self.marks.deinit(self.allocator); 41 self.grey.deinit(self.allocator); 42 self.reduction_frames.deinit(self.allocator); 43 self.reduction_roots.deinit(self.allocator); 44 } 45 46 inline fn allocSlot(self: *Arena) !u32 { 47 if (self.free.items.len != 0) { 48 const idx = self.free.items[self.free.items.len - 1]; 49 self.free.items.len -= 1; 50 self.allocations += 1; 51 return idx; 52 } 53 if (self.nodes.items.len >= std.math.maxInt(u32)) return error.OutOfMemory; 54 const idx: u32 = @intCast(self.nodes.items.len); 55 _ = try self.nodes.addOne(self.allocator); 56 self.allocations += 1; 57 return idx; 58 } 59 60 pub inline fn alloc(self: *Arena, node: tree.Node) !u32 { 61 const idx = try self.allocSlot(); 62 self.nodes.items[idx] = node; 63 return idx; 64 } 65 66 // Reduction allocates applications in its hottest path. Scalar operands 67 // avoid constructing and then copying a temporary padded tagged union. 68 pub inline fn allocApp(self: *Arena, func: u32, arg: u32) !u32 { 69 const idx = try self.allocSlot(); 70 self.nodes.items[idx] = .{ .app = .{ .func = func, .arg = arg } }; 71 return idx; 72 } 73 74 pub fn get(self: *Arena, idx: u32) *tree.Node { 75 return &self.nodes.items[idx]; 76 } 77 78 pub fn len(self: *const Arena) u32 { 79 return @intCast(self.nodes.items.len); 80 } 81 82 pub fn collectionDue(self: *const Arena) bool { 83 return self.gc_enabled and self.allocations >= self.gc_interval; 84 } 85 86 pub fn setGcPolicy(self: *Arena, minimum: usize, multiplier: usize) error{InvalidData}!void { 87 if (minimum == 0 or multiplier == 0) return error.InvalidData; 88 self.gc_min_interval = minimum; 89 self.gc_live_multiplier = multiplier; 90 self.gc_interval = @max(minimum, self.last_live *| multiplier); 91 } 92 93 // Inline the per-edge duplicate check; shared edges normally need no enqueue. 94 inline fn mark(self: *Arena, idx: u32) !void { 95 if (self.marks.items[idx]) return; 96 // Mark on enqueue so shared children occupy the work stack only once. 97 try self.grey.append(self.allocator, idx); 98 self.marks.items[idx] = true; 99 } 100 101 /// Non-moving mark/sweep. Only reduction safepoints collect automatically; 102 /// construction never collects, so newly allocated children can be linked 103 /// without temporary root registrations. Failure before sweep changes no IDs. 104 pub fn collect(self: *Arena, temporary_roots: []const u32) !void { 105 try self.marks.resize(self.allocator, self.nodes.items.len); 106 @memset(self.marks.items, false); 107 self.grey.clearRetainingCapacity(); 108 for (self.roots.items) |idx| try self.mark(idx); 109 for (temporary_roots) |idx| try self.mark(idx); 110 var live: usize = 0; 111 var end: usize = 0; 112 while (self.grey.pop()) |idx| { 113 live += 1; 114 end = @max(end, @as(usize, idx) + 1); 115 switch (self.nodes.items[idx]) { 116 .leaf => {}, 117 .stem => |s| try self.mark(s.child), 118 .fork => |f| { 119 try self.mark(f.left); 120 try self.mark(f.right); 121 }, 122 .app => |a| { 123 try self.mark(a.func); 124 try self.mark(a.arg); 125 }, 126 } 127 } 128 try self.free.ensureTotalCapacity(self.allocator, end - live); 129 // Free-list entries outside its logical length are never read. Rebuild 130 // without poisoning the previous list only to overwrite it immediately. 131 self.free.items.len = 0; 132 // Dead trailing slots need neither sweeping nor free-list entries. 133 // IDs of live nodes never change; backing capacity remains available. 134 // Slice truncation keeps bounds checks without touching dead backing 135 // storage. alloc/allocApp initialize a recycled slot before publishing it. 136 self.nodes.items = self.nodes.items[0..end]; 137 var i = end; 138 while (i > 0) { 139 i -= 1; 140 if (!self.marks.items[i]) { 141 self.free.appendAssumeCapacity(@intCast(i)); 142 } 143 } 144 self.last_live = live; 145 self.allocations = 0; 146 self.collections += 1; 147 // Amortize scanning a large live graph; this is not a hard RSS bound. 148 // Recompute rather than retaining a transient live-heap peak forever. 149 self.gc_interval = @max(self.gc_min_interval, live *| self.gc_live_multiplier); 150 } 151 };