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 }