tricu

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

reduce_test.zig (28013B)


      1 const std = @import("std");
      2 const testing = std.testing;
      3 const Arena = @import("arena.zig").Arena;
      4 const tree = @import("tree.zig");
      5 const machine = @import("reduce.zig");
      6 const codecs = @import("codecs.zig");
      7 
      8 fn stem(a: *Arena, x: u32) !u32 {
      9     return a.alloc(.{ .stem = .{ .child = x } });
     10 }
     11 fn fork(a: *Arena, x: u32, y: u32) !u32 {
     12     return a.alloc(.{ .fork = .{ .left = x, .right = y } });
     13 }
     14 fn app(a: *Arena, x: u32, y: u32) !u32 {
     15     return a.alloc(.{ .app = .{ .func = x, .arg = y } });
     16 }
     17 fn force(a: *Arena, x: u32) !u32 {
     18     return machine.reduce(x, a, 10_000_000);
     19 }
     20 fn equal(a: *Arena, x: u32, y: u32) !void {
     21     const nx = a.get(try force(a, x)).*;
     22     const ny = a.get(try force(a, y)).*;
     23     try testing.expectEqual(std.meta.activeTag(nx), std.meta.activeTag(ny));
     24     switch (nx) {
     25         .leaf => {},
     26         .stem => |s| try equal(a, s.child, ny.stem.child),
     27         .fork => |f| {
     28             try equal(a, f.left, ny.fork.left);
     29             try equal(a, f.right, ny.fork.right);
     30         },
     31         .app => unreachable,
     32     }
     33 }
     34 
     35 test "kernel rules, including lazy distribution and all triage arms" {
     36     var a = Arena.init(testing.allocator);
     37     defer a.deinit();
     38     const t = try a.alloc(.leaf);
     39     const u = try stem(&a, t);
     40     const v = try fork(&a, u, t);
     41     try equal(&a, try app(&a, t, v), try stem(&a, v));
     42     try equal(&a, try app(&a, u, v), try fork(&a, t, v));
     43     try equal(&a, try app(&a, try fork(&a, t, v), u), v);
     44     const s = try fork(&a, try stem(&a, u), t);
     45     try equal(&a, try app(&a, s, v), try app(&a, try app(&a, u, v), try app(&a, t, v)));
     46     const f = try fork(&a, try fork(&a, v, u), t);
     47     try equal(&a, try app(&a, f, t), v);
     48     try equal(&a, try app(&a, f, u), try app(&a, u, t));
     49     try equal(&a, try app(&a, f, v), try app(&a, try app(&a, t, u), t));
     50 }
     51 
     52 test "discarded work and constructor children are never demanded" {
     53     var a = Arena.init(testing.allocator);
     54     defer a.deinit();
     55     const t = try a.alloc(.leaf);
     56     const poison = try app(&a, t, t);
     57     a.get(poison).* = .{ .app = .{ .func = poison, .arg = t } };
     58     const k = try fork(&a, t, t);
     59     const result = try app(&a, k, poison);
     60     try testing.expectEqual(tree.NodeTag.leaf, std.meta.activeTag(a.get(try machine.reduce(result, &a, 1)).*));
     61     const constructor = try app(&a, t, poison);
     62     const r = try machine.reduce(constructor, &a, 1);
     63     try testing.expectEqual(poison, a.get(r).stem.child);
     64     try testing.expectError(error.FuelExhausted, machine.reduce(poison, &a, 100));
     65 }
     66 
     67 test "shared redex updated once and fuel failure can be resumed" {
     68     var a = Arena.init(testing.allocator);
     69     defer a.deinit();
     70     const t = try a.alloc(.leaf);
     71     const shared = try app(&a, t, t);
     72     try testing.expectError(error.FuelExhausted, machine.reduce(shared, &a, 0));
     73     _ = try machine.reduce(shared, &a, 1);
     74     _ = try machine.reduce(shared, &a, 0);
     75     try testing.expectEqual(t, a.get(shared).stem.child);
     76 }
     77 
     78 test "100000 nested function demands and selected-result updates use heap frames" {
     79     var a = Arena.init(testing.allocator);
     80     defer a.deinit();
     81     const t = try a.alloc(.leaf);
     82     const identity = try fork(&a, t, t);
     83     var x = t;
     84     for (0..100000) |_| x = try app(&a, try fork(&a, t, x), t);
     85     try testing.expect(a.get(try force(&a, x)).* == .leaf);
     86     // Left-associated applications of a constant returning itself would be
     87     // cyclic; use alternating constructor applications and constant selection.
     88     x = identity;
     89     for (0..100000) |_| x = try app(&a, x, identity);
     90     _ = try force(&a, x);
     91 }
     92 
     93 test "non-moving collection preserves explicit roots and reuses dead slots" {
     94     var a = Arena.init(testing.allocator);
     95     defer a.deinit();
     96     const t = try a.alloc(.leaf);
     97     const kept = try stem(&a, t);
     98     try a.roots.append(a.allocator, kept);
     99     for (0..1000) |_| _ = try fork(&a, t, t);
    100     const before = a.len();
    101     try a.collect(&.{});
    102     try testing.expectEqual(@as(usize, 0), a.free.items.len);
    103     for (0..1000) |_| _ = try stem(&a, t);
    104     try testing.expectEqual(before, a.len());
    105     try testing.expectEqual(t, a.get(kept).stem.child);
    106 }
    107 
    108 fn gcWorkload(allocator: std.mem.Allocator) !void {
    109     var a = Arena.init(allocator);
    110     defer a.deinit();
    111     const t = try a.alloc(.leaf);
    112     const k = try stem(&a, t);
    113     const direct_id = try fork(&a, try stem(&a, k), k);
    114     // S (K K) I is extensionally K, but not a constructor shortcut. Keep
    115     // real distribution allocations here so GC/failure stress cannot disappear
    116     // when the direct S K b identity is fused by the evaluator.
    117     const extended_k = try fork(&a, try stem(&a, try fork(&a, t, k)), direct_id);
    118     const id = try fork(&a, try stem(&a, extended_k), k);
    119     var x = t;
    120     for (0..100) |_| x = try app(&a, id, x);
    121     a.gc_enabled = true;
    122     try a.setGcPolicy(8, 1);
    123     try testing.expect(a.get(try force(&a, x)).* == .leaf);
    124     try testing.expect(a.collections > 1);
    125     try a.collect(&.{x});
    126     try testing.expectEqual(@as(usize, a.len() - 1), a.free.items.len);
    127 }
    128 
    129 test "GC during distribution preserves pending demands" {
    130     try gcWorkload(testing.allocator);
    131 }
    132 
    133 test "allocation failures unwind machine and collector without leaking" {
    134     try testing.checkAllAllocationFailures(testing.allocator, gcWorkload, .{});
    135 }
    136 
    137 test "deep shared DAG copying uses a growable work stack" {
    138     var a = Arena.init(testing.allocator);
    139     defer a.deinit();
    140     var b = Arena.init(testing.allocator);
    141     defer b.deinit();
    142     var x = try a.alloc(.leaf);
    143     for (0..10000) |_| x = try fork(&a, x, x);
    144     const copied = try tree.copyTree(a.nodes.items, &b, x);
    145     try testing.expectEqual(a.len(), b.len());
    146     try testing.expectEqual(b.get(copied).fork.left, b.get(copied).fork.right);
    147 }
    148 
    149 test "boolean observation forces its child and rejects malformed booleans" {
    150     var a = Arena.init(testing.allocator);
    151     defer a.deinit();
    152     const t = try a.alloc(.leaf);
    153     const delayed = try app(&a, try fork(&a, t, t), t);
    154     try testing.expectEqual(true, (try codecs.toBool(&a, try stem(&a, delayed))).?);
    155     try testing.expectEqual(@as(?bool, null), try codecs.toBool(&a, try stem(&a, try stem(&a, t))));
    156 }
    157 
    158 const handles = @import("handles.zig");
    159 fn handleWorkload(allocator: std.mem.Allocator) !void {
    160     var a = Arena.init(allocator);
    161     defer a.deinit();
    162     var table: handles.Table = .{};
    163     defer table.deinit(allocator);
    164     const t = try a.alloc(.leaf);
    165     const handle = try table.create(allocator, t);
    166     try table.release(handle);
    167     const replacement = try table.create(allocator, t);
    168     try testing.expectError(error.InvalidHandle, table.get(handle));
    169     try testing.expectEqual(t, try table.get(replacement));
    170     for (0..1000) |_| {
    171         const node = try stem(&a, t);
    172         const h = try table.create(allocator, node);
    173         try table.release(h);
    174     }
    175     a.roots.clearRetainingCapacity();
    176     try table.appendRoots(&a);
    177     try a.collect(&.{});
    178     try testing.expectEqual(@as(usize, 0), a.free.items.len);
    179     try testing.expectEqual(@as(usize, 2), table.slots.items.len);
    180 }
    181 
    182 test "handle table failure cleanup and reclamation" {
    183     try testing.checkAllAllocationFailures(testing.allocator, handleWorkload, .{});
    184 }
    185 
    186 // Independent eager reference on finite, already-normal inputs. No application
    187 // nodes, mutation or continuation/update machinery are shared with the machine.
    188 fn reference(a: *Arena, f: u32, x: u32, fuel: *usize) error{ OutOfMemory, FuelExhausted }!u32 {
    189     if (fuel.* == 0) return error.FuelExhausted;
    190     fuel.* -= 1;
    191     return switch (a.get(f).*) {
    192         .leaf => stem(a, x),
    193         .stem => |s| fork(a, s.child, x),
    194         .fork => |b| switch (a.get(b.left).*) {
    195             .leaf => b.right,
    196             .stem => |s| reference(a, try reference(a, s.child, x, fuel), try reference(a, b.right, x, fuel), fuel),
    197             .fork => |branches| switch (a.get(x).*) {
    198                 .leaf => branches.left,
    199                 .stem => |s| reference(a, branches.right, s.child, fuel),
    200                 .fork => |arg| reference(a, try reference(a, b.right, arg.left, fuel), arg.right, fuel),
    201                 .app => unreachable,
    202             },
    203             .app => unreachable,
    204         },
    205         .app => unreachable,
    206     };
    207 }
    208 
    209 test "all pairs of depth-two values agree with an independent reference" {
    210     var a = Arena.init(testing.allocator);
    211     defer a.deinit();
    212     const t = try a.alloc(.leaf);
    213     const small = [_]u32{ t, try stem(&a, t), try fork(&a, t, t) };
    214     var values: std.ArrayList(u32) = .empty;
    215     defer values.deinit(a.allocator);
    216     try values.append(a.allocator, t);
    217     for (small) |x| {
    218         try values.append(a.allocator, try stem(&a, x));
    219         for (small) |y| try values.append(a.allocator, try fork(&a, x, y));
    220     }
    221     for (values.items) |f| for (values.items) |x| {
    222         var fuel: usize = 10000;
    223         const expected = try reference(&a, f, x, &fuel);
    224         try equal(&a, try app(&a, f, x), expected);
    225         try equal(&a, try machine.reduceUnbounded(try app(&a, f, x), &a), expected);
    226     };
    227 }
    228 
    229 test "result tags are demanded booleans, not arbitrary non-true error tags" {
    230     var a = Arena.init(testing.allocator);
    231     defer a.deinit();
    232     const t = try a.alloc(.leaf);
    233     const delayed = try app(&a, try fork(&a, t, t), t);
    234     const payload = try fork(&a, t, t);
    235     const good = try fork(&a, try stem(&a, delayed), payload);
    236     try testing.expect((try codecs.unwrapResult(&a, good)).?.ok);
    237     const malformed = try fork(&a, try fork(&a, t, t), payload);
    238     try testing.expectEqual(@as(?codecs.UnwrapResult, null), try codecs.unwrapResult(&a, malformed));
    239 }
    240 
    241 test "oversized host numbers return a decode failure rather than trapping" {
    242     var a = Arena.init(testing.allocator);
    243     defer a.deinit();
    244     const max = try codecs.ofNumber(&a, std.math.maxInt(u64));
    245     try testing.expectEqual(std.math.maxInt(u64), (try codecs.toNumber(&a, max)).?);
    246     const overflow = try fork(&a, try a.alloc(.leaf), max);
    247     try testing.expectEqual(@as(?u64, null), try codecs.toNumber(&a, overflow));
    248 }
    249 
    250 test "headroom policy can shrink after a live peak and rejects zero atomically" {
    251     var a = Arena.init(testing.allocator);
    252     defer a.deinit();
    253     try a.setGcPolicy(8, 4);
    254     var root = try a.alloc(.leaf);
    255     for (0..100) |_| root = try stem(&a, root);
    256     try a.collect(&.{root});
    257     try testing.expectEqual(@as(usize, 404), a.gc_interval);
    258     try testing.expectError(error.InvalidData, a.setGcPolicy(0, 2));
    259     try testing.expectError(error.InvalidData, a.setGcPolicy(2, 0));
    260     try testing.expectEqual(@as(usize, 8), a.gc_min_interval);
    261     try testing.expectEqual(@as(usize, 4), a.gc_live_multiplier);
    262     try testing.expectEqual(@as(usize, 404), a.gc_interval);
    263     a.get(root).* = .leaf;
    264     try a.collect(&.{root});
    265     try testing.expectEqual(@as(usize, 8), a.gc_interval);
    266     try a.setGcPolicy(1, std.math.maxInt(usize));
    267     try testing.expectEqual(std.math.maxInt(usize), a.gc_interval);
    268     // Two live nodes exercise saturating multiplication, including on 32-bit.
    269     const next = try stem(&a, root);
    270     try a.collect(&.{next});
    271     try testing.expectEqual(std.math.maxInt(usize), a.gc_interval);
    272 }
    273 
    274 test "collection trims only dead tails and reuses interior holes without moving roots" {
    275     var a = Arena.init(testing.allocator);
    276     defer a.deinit();
    277     const t = try a.alloc(.leaf);
    278     const hole = try stem(&a, t);
    279     const root = try fork(&a, t, t);
    280     for (0..1000) |_| _ = try a.alloc(.leaf);
    281     const capacity = a.nodes.capacity;
    282     try a.collect(&.{root});
    283     try testing.expectEqual(root + 1, a.len());
    284     try testing.expectEqual(capacity, a.nodes.capacity);
    285     try testing.expectEqualSlices(u32, &.{hole}, a.free.items);
    286     try testing.expectEqual(hole, try a.alloc(.leaf));
    287     try testing.expectEqual(root + 1, try a.alloc(.leaf));
    288     try testing.expectEqual(t, a.get(root).fork.left);
    289     try a.collect(&.{});
    290     try testing.expectEqual(@as(u32, 0), a.len());
    291     try testing.expectEqual(@as(usize, 0), a.free.items.len);
    292     try testing.expectEqual(@as(u32, 0), try a.alloc(.leaf));
    293 }
    294 
    295 test "constructor fast return still honors collection with zero fuel" {
    296     var a = Arena.init(testing.allocator);
    297     defer a.deinit();
    298     const t = try a.alloc(.leaf);
    299     const root = try stem(&a, t);
    300     for (0..1000) |_| _ = try a.alloc(.leaf);
    301     try a.setGcPolicy(1, 1);
    302     a.gc_enabled = true;
    303     try testing.expectEqual(root, try machine.reduce(root, &a, 0));
    304     try testing.expectEqual(@as(usize, 1), a.collections);
    305     try testing.expectEqual(t, a.get(root).stem.child);
    306     try testing.expectEqual(@as(u32, 2), a.len());
    307 }
    308 
    309 test "suspended demands preserve fuel boundaries and clear retained scratch on failure" {
    310     var a = Arena.init(testing.allocator);
    311     defer a.deinit();
    312     const t = try a.alloc(.leaf);
    313     const delayed_function = try app(&a, t, t);
    314     const root = try app(&a, delayed_function, t);
    315     try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 1));
    316     try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
    317     try testing.expectEqual(@as(usize, 0), a.reduction_roots.items.len);
    318     try testing.expect(a.reduction_frames.capacity > 0);
    319     _ = try machine.reduce(root, &a, 2);
    320     try testing.expectEqual(t, a.get(root).fork.left);
    321     // A delayed discriminator and a divergent, discarded argument.
    322     const delayed_leaf = try app(&a, try fork(&a, t, t), t);
    323     const poison = try app(&a, t, t);
    324     a.get(poison).* = .{ .app = .{ .func = poison, .arg = t } };
    325     const selected = try app(&a, try fork(&a, delayed_leaf, t), poison);
    326     _ = try machine.reduce(selected, &a, 2);
    327     try testing.expect(a.get(selected).* == .leaf);
    328     // A delayed selected result takes a second application entry.
    329     const delayed_result = try app(&a, t, t);
    330     const select = try app(&a, try fork(&a, t, delayed_result), poison);
    331     try testing.expectError(error.FuelExhausted, machine.reduce(select, &a, 1));
    332     try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
    333     _ = try machine.reduce(select, &a, 2);
    334     try testing.expectEqual(t, a.get(select).stem.child);
    335     // Delayed triage argument, then the stem arm creates another application.
    336     const triage = try fork(&a, try fork(&a, t, t), t);
    337     const argument = try app(&a, t, t);
    338     const inspect = try app(&a, triage, argument);
    339     try testing.expectError(error.FuelExhausted, machine.reduce(inspect, &a, 2));
    340     _ = try machine.reduce(inspect, &a, 1);
    341     try testing.expectEqual(t, a.get(inspect).stem.child);
    342 }
    343 
    344 test "collector allocation failure is retryable without losing live nodes or free slots" {
    345     // Fail each allocation/growth in the first collection, then retry in the
    346     // same arena. This also exercises dirty marks/grey left by failed marking.
    347     for (0..12) |offset| {
    348         var failing = testing.FailingAllocator.init(testing.allocator, .{});
    349         var a = Arena.init(failing.allocator());
    350         defer a.deinit();
    351         const t = try a.alloc(.leaf);
    352         _ = try a.alloc(.leaf); // interior hole
    353         var root = t;
    354         // Traversing right leaves many left demands on the grey stack;
    355         // each stem also shares t, exercising mark-on-enqueue deduplication.
    356         for (0..100) |_| root = try fork(&a, try stem(&a, t), root);
    357         for (0..100) |_| _ = try a.alloc(.leaf);
    358         const before = a.len();
    359         failing.fail_index = failing.alloc_index + offset;
    360         failing.resize_fail_index = failing.resize_index;
    361         a.collect(&.{root}) catch |err| {
    362             try testing.expectEqual(error.OutOfMemory, err);
    363             try testing.expectEqual(before, a.len());
    364             try testing.expectEqual(@as(usize, 0), a.free.items.len);
    365             try testing.expectEqual(@as(usize, 0), a.collections);
    366         };
    367         failing.fail_index = std.math.maxInt(usize);
    368         failing.resize_fail_index = std.math.maxInt(usize);
    369         try a.collect(&.{root});
    370         try testing.expectEqual(@as(usize, 201), a.last_live);
    371         try testing.expectEqualSlices(u32, &.{1}, a.free.items);
    372         var cursor = root;
    373         for (0..100) |_| {
    374             try testing.expectEqual(t, a.get(a.get(cursor).fork.left).stem.child);
    375             cursor = a.get(cursor).fork.right;
    376         }
    377         try testing.expectEqual(t, cursor);
    378     }
    379 }
    380 
    381 test "continuation allocation failure returns reusable scratch and evaluation can resume" {
    382     var failing = testing.FailingAllocator.init(testing.allocator, .{});
    383     var a = Arena.init(failing.allocator());
    384     defer a.deinit();
    385     const t = try a.alloc(.leaf);
    386     var root = t;
    387     for (0..1000) |_| root = try app(&a, try fork(&a, t, root), t);
    388     failing.fail_index = failing.alloc_index + 1;
    389     failing.resize_fail_index = failing.resize_index;
    390     try testing.expectError(error.OutOfMemory, force(&a, root));
    391     try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
    392     try testing.expectEqual(@as(usize, 0), a.reduction_roots.items.len);
    393     failing.fail_index = std.math.maxInt(usize);
    394     failing.resize_fail_index = std.math.maxInt(usize);
    395     try testing.expect(a.get(try force(&a, root)).* == .leaf);
    396     try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
    397 }
    398 
    399 // Small recursive oracle: the literal unfused rules, with one fuel charge for
    400 // each application entry. Test inputs are shallow; this is not a runtime path.
    401 fn unfused(root: u32, a: *Arena, fuel: *u64) machine.ReduceError!u32 {
    402     const node = a.get(root).*;
    403     if (node != .app) return root;
    404     if (fuel.* == 0) return error.FuelExhausted;
    405     fuel.* -= 1;
    406     const function = a.get(try unfused(node.app.func, a, fuel)).*;
    407     switch (function) {
    408         .leaf => a.get(root).* = .{ .stem = .{ .child = node.app.arg } },
    409         .stem => |s| a.get(root).* = .{ .fork = .{ .left = s.child, .right = node.app.arg } },
    410         .fork => |f| {
    411             const discriminator = a.get(try unfused(f.left, a, fuel)).*;
    412             switch (discriminator) {
    413                 .leaf => {
    414                     const value = try unfused(f.right, a, fuel);
    415                     a.get(root).* = a.get(value).*;
    416                 },
    417                 .stem => |s| {
    418                     const left = try app(a, s.child, node.app.arg);
    419                     const right = try app(a, f.right, node.app.arg);
    420                     a.get(root).* = .{ .app = .{ .func = left, .arg = right } };
    421                     return unfused(root, a, fuel);
    422                 },
    423                 .fork => |branches| {
    424                     const argument = a.get(try unfused(node.app.arg, a, fuel)).*;
    425                     switch (argument) {
    426                         .leaf => {
    427                             const value = try unfused(branches.left, a, fuel);
    428                             a.get(root).* = a.get(value).*;
    429                         },
    430                         .stem => |s| {
    431                             a.get(root).* = .{ .app = .{ .func = branches.right, .arg = s.child } };
    432                             return unfused(root, a, fuel);
    433                         },
    434                         .fork => |v| {
    435                             const inner = try app(a, f.right, v.left);
    436                             a.get(root).* = .{ .app = .{ .func = inner, .arg = v.right } };
    437                             return unfused(root, a, fuel);
    438                         },
    439                         .app => unreachable,
    440                     }
    441                 },
    442                 .app => unreachable,
    443             }
    444         },
    445         .app => unreachable,
    446     }
    447     return root;
    448 }
    449 
    450 fn equalUnfused(a: *Arena, x: u32, b: *Arena, y: u32) !void {
    451     var fuel: u64 = 10000;
    452     const expected = a.get(try unfused(x, a, &fuel)).*;
    453     const actual = b.get(try machine.reduce(y, b, 10000)).*;
    454     try testing.expectEqual(std.meta.activeTag(expected), std.meta.activeTag(actual));
    455     switch (expected) {
    456         .leaf => {},
    457         .stem => |s| try equalUnfused(a, s.child, b, actual.stem.child),
    458         .fork => |f| {
    459             try equalUnfused(a, f.left, b, actual.fork.left);
    460             try equalUnfused(a, f.right, b, actual.fork.right);
    461         },
    462         .app => unreachable,
    463     }
    464 }
    465 
    466 fn fusionFixture(a: *Arena, variant: usize, argument: usize, triage: bool) !u32 {
    467     const t = try a.alloc(.leaf);
    468     const k = try stem(a, t);
    469     const payload = try fork(a, k, t);
    470     const value = switch (variant) {
    471         0 => t,
    472         1 => k,
    473         2 => try fork(a, t, t),
    474         3 => try fork(a, t, k),
    475         4 => try fork(a, t, try fork(a, t, payload)),
    476         5 => try fork(a, t, try app(a, try fork(a, t, k), t)),
    477         // Negative controls: delayed discriminator, non-K stem, non-K fork.
    478         6 => try fork(a, try app(a, try fork(a, t, t), t), k),
    479         7 => try stem(a, k),
    480         8 => try fork(a, t, try fork(a, k, t)),
    481         else => unreachable,
    482     };
    483     const x = switch (argument) {
    484         0 => t,
    485         1 => k,
    486         2 => payload,
    487         3 => try app(a, try fork(a, t, payload), t),
    488         else => unreachable,
    489     };
    490     if (triage) {
    491         const pair = try fork(a, x, k);
    492         const input = if (argument == 3) try app(a, try fork(a, t, pair), t) else pair;
    493         return app(a, try fork(a, try fork(a, k, t), value), input);
    494     }
    495     return app(a, try fork(a, try stem(a, value), t), x);
    496 }
    497 
    498 test "fused distribution and triage agree with literal rules at every small fuel boundary" {
    499     for ([_]bool{ false, true }) |triage| for (0..9) |variant| for (0..4) |argument| for (0..20) |budget| {
    500         var expected = Arena.init(testing.allocator);
    501         defer expected.deinit();
    502         var actual = Arena.init(testing.allocator);
    503         defer actual.deinit();
    504         const x = try fusionFixture(&expected, variant, argument, triage);
    505         const y = try fusionFixture(&actual, variant, argument, triage);
    506         var fuel: u64 = budget;
    507         const reference_result = unfused(x, &expected, &fuel);
    508         if (reference_result) |_| {
    509             _ = try machine.reduce(y, &actual, budget);
    510             try equalUnfused(&expected, x, &actual, y);
    511         } else |err| {
    512             try testing.expectEqual(error.FuelExhausted, err);
    513             try testing.expectError(error.FuelExhausted, machine.reduce(y, &actual, budget));
    514             // Partial graph shapes can differ; both must remain resumable.
    515             try equalUnfused(&expected, x, &actual, y);
    516         }
    517     };
    518 }
    519 
    520 test "fused identity and constant selection discard divergent branches" {
    521     var a = Arena.init(testing.allocator);
    522     defer a.deinit();
    523     const t = try a.alloc(.leaf);
    524     const poison = try app(&a, t, t);
    525     a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
    526     const k = try stem(&a, t);
    527     const identity = try fork(&a, try stem(&a, k), poison);
    528     const result = try app(&a, identity, t);
    529     const before = a.allocations;
    530     _ = try machine.reduce(result, &a, 3);
    531     try testing.expect(a.get(result).* == .leaf);
    532     try testing.expectEqual(before, a.allocations);
    533     const constant = try fork(&a, try stem(&a, try fork(&a, t, try fork(&a, t, k))), poison);
    534     const discard = try app(&a, constant, poison);
    535     const allocated = a.allocations;
    536     _ = try machine.reduce(discard, &a, 3);
    537     try testing.expectEqual(t, a.get(discard).stem.child);
    538     try testing.expectEqual(allocated, a.allocations);
    539     // Selecting divergence must still demand it, rather than return a thunk.
    540     const demand = try app(&a, identity, poison);
    541     try testing.expectError(error.FuelExhausted, machine.reduce(demand, &a, 100));
    542 }
    543 
    544 test "fused selection updates shared demanded payloads and survives collection" {
    545     var a = Arena.init(testing.allocator);
    546     defer a.deinit();
    547     const t = try a.alloc(.leaf);
    548     const payload = try app(&a, try fork(&a, t, t), t);
    549     const poison = try app(&a, t, t);
    550     a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
    551     const constant = try fork(&a, t, try fork(&a, t, payload));
    552     const root = try app(&a, try fork(&a, try stem(&a, constant), poison), poison);
    553     try a.roots.append(a.allocator, payload); // independently retained alias
    554     a.gc_enabled = true;
    555     try a.setGcPolicy(1, 1);
    556     _ = try machine.reduce(root, &a, 4);
    557     try testing.expect(a.get(root).* == .leaf);
    558     try testing.expect(a.get(payload).* == .leaf);
    559     try a.collect(&.{root});
    560     try testing.expect(a.get(root).* == .leaf);
    561     try testing.expect(a.get(payload).* == .leaf);
    562 }
    563 
    564 test "scalar application allocation initializes dirty recycled slots completely" {
    565     var a = Arena.init(testing.allocator);
    566     defer a.deinit();
    567     const t = try a.alloc(.leaf);
    568     const hole = try app(&a, t, t);
    569     a.get(hole).* = .{ .app = .{ .func = hole, .arg = hole } };
    570     const kept = try stem(&a, t);
    571     try a.collect(&.{kept});
    572     try testing.expectEqual(hole, try a.allocApp(kept, t));
    573     try testing.expectEqual(kept, a.get(hole).app.func);
    574     try testing.expectEqual(t, a.get(hole).app.arg);
    575     _ = try machine.reduce(hole, &a, 1);
    576     try testing.expectEqual(t, a.get(hole).fork.left);
    577     try testing.expectEqual(t, a.get(hole).fork.right);
    578     try a.collect(&.{kept});
    579     try testing.expectEqual(hole, try a.alloc(.leaf));
    580     try testing.expect(a.get(hole).* == .leaf);
    581 }
    582 
    583 test "fused triage preserves lazy children and shared updates with forced GC" {
    584     for ([_]bool{ false, true }) |gc| for (0..5) |variant| {
    585         var a = Arena.init(testing.allocator);
    586         defer a.deinit();
    587         const t = try a.alloc(.leaf);
    588         const k = try stem(&a, t);
    589         const poison = try app(&a, t, t);
    590         a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
    591         const payload = try app(&a, try fork(&a, t, t), t);
    592         const handler = switch (variant) {
    593             0 => t,
    594             1 => k,
    595             2 => try fork(&a, t, t),
    596             3 => try fork(&a, t, k),
    597             4 => try fork(&a, t, try fork(&a, t, payload)),
    598             else => unreachable,
    599         };
    600         const left = if (variant == 1) payload else poison;
    601         const pair = try fork(&a, left, poison);
    602         const root = try app(&a, try fork(&a, try fork(&a, poison, poison), handler), pair);
    603         try a.roots.append(a.allocator, payload);
    604         a.gc_enabled = gc;
    605         try a.setGcPolicy(1, 1);
    606         const before = a.allocations;
    607         _ = try machine.reduce(root, &a, if (variant == 1 or variant == 4) 4 else 3);
    608         if (gc) {
    609             try testing.expect(a.collections > 0);
    610             try a.collect(&.{root});
    611         } else try testing.expectEqual(before, a.allocations);
    612         switch (variant) {
    613             0 => {
    614                 try testing.expectEqual(poison, a.get(root).fork.left);
    615                 try testing.expectEqual(poison, a.get(root).fork.right);
    616             },
    617             1, 4 => {
    618                 try testing.expect(a.get(root).* == .leaf);
    619                 try testing.expect(a.get(payload).* == .leaf);
    620             },
    621             2 => try testing.expectEqual(poison, a.get(root).stem.child),
    622             3 => {
    623                 try testing.expect(a.get(a.get(root).fork.left).* == .leaf);
    624                 try testing.expectEqual(poison, a.get(root).fork.right);
    625             },
    626             else => unreachable,
    627         }
    628         if (variant != 1 and variant != 4) try testing.expect(a.get(payload).* == .app);
    629     };
    630 }
    631 
    632 test "fused triage still demands a selected divergent child" {
    633     var a = Arena.init(testing.allocator);
    634     defer a.deinit();
    635     const t = try a.alloc(.leaf);
    636     const poison = try app(&a, t, t);
    637     a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
    638     const handler = try stem(&a, t);
    639     const root = try app(&a, try fork(&a, try fork(&a, t, t), handler), try fork(&a, poison, t));
    640     a.gc_enabled = true;
    641     try a.setGcPolicy(1, 1);
    642     try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 100));
    643     try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 100));
    644 }