tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 };