✓
Passing This code compiles and runs correctly.
Code
// BOUNDARY (negative): the carry across a label-fold back-edge is sound ONLY if
// it is 1:1 conserved — every iteration consumes the carried obligation AND
// re-issues exactly one to carry forward or escape. This test breaks that: the
// `| again` back-edge re-invokes `@loop()` WITHOUT carrying the re-issued handle,
// dropping the cleanup obligation. The checker must reject it.
//
// This is the asymmetric-drop mirror of GREEN 330_074 (where `@loop(h: v)`
// carries the handle). It is the same class of bug as MUST_FAIL 370_020
// (`| failed |> @loop()` drops the connection).
//
// EXPECTED: KORU030 — "Label jump '@loop' drops cleanup obligation for ..."
// (phantom_semantic_checker.zig validateStep / label_jump path).
//
// HISTORY: until 2026-06-19 the phantom checker did NOT catch this. The
// label_jump uncleaned-resource guard was SKIPPED for loop jumps via
// `is_loop_jump` ("jumps to declared labels (#loop) ... obligations survive
// across iterations"). That exception was too broad: it also let a back-edge
// that DROPS the re-issued obligation pass, and the error surfaced only at
// Stage D as a raw Zig compile error (`missing struct field: h`) — an
// internal-looking leak of a real obligation-conservation bug, not a clean
// language rejection. The back-edge conservation check now verifies (not
// assumes) that a carried obligation is routed into the loop head via a jump
// arg, so this is rejected cleanly with KORU030. See
// /tmp/obligation-loop-review.md.
const std = @import("std");
const Handle = struct { n: i32 };
~event make {}
| made *Handle<owned!>
~proc make|zig {
const h = std.heap.page_allocator.create(Handle) catch unreachable;
h.* = .{ .n = 0 };
return .{ .made = h };
}
~event step { h: *Handle<!owned> }
| again *Handle<owned!>
| stop *Handle<owned!>
~proc step|zig {
h.n += 1;
if (h.n < 3) return .{ .again = h };
return .{ .stop = h };
}
~event done { h: *Handle<!owned> }
~proc done|zig {
std.debug.print("n={}\n", .{h.n});
std.heap.page_allocator.destroy(h);
}
~event spin {}
| finished *Handle<owned!>
// BUG: `| again _ |> @loop()` discards the re-issued <owned!> handle from the
// `again` branch and re-enters the fold without carrying it across the back-edge,
// leaking the obligation. (`_` is used rather than a named binding so KORU100
// unused-binding does not fire first; this exercises the phantom-drop path.)
~spin = make()
| made h0 |> #loop step(h: h0)
| again _ |> @loop()
| stop r => finished r
~spin()
| finished hf |> done(h: hf)
Backend must reject with:
CONTAINS error[KORU030]
CONTAINS drops cleanup obligation
CONTAINS @loopError Verification
Actual Compiler Output
error[KORU030]: Label jump '@loop' drops cleanup obligation for '_auto_0' - pass it as an argument or discharge it before jumping
--> phantom_semantic_check:63:0
❌ Compiler coordination error: Phantom semantic validation failed
error: CompilerCoordinationFailed
/Users/larsde/src/koru/tests/regression/300_ADVANCED_FEATURES/330_PHANTOM_TYPES/330_075_back_edge_drops_obligation/backend.zig:94:13: 0x102cdf20b in emit (backend)
return error.CompilerCoordinationFailed;
^
/Users/larsde/src/koru/tests/regression/300_ADVANCED_FEATURES/330_PHANTOM_TYPES/330_075_back_edge_drops_obligation/backend.zig:190:28: 0x102cdfef7 in main (backend)
const generated_code = try RuntimeEmitter.emit(compile_allocator, final_ast);
^Flows
subflow ~spin click a branch to expand · @labels scroll to their anchor
make
flow ~spin click a branch to expand · @labels scroll to their anchor
spin
Test Configuration
MUST_FAIL