○
Planned This feature is planned but not yet implemented.
OWED: a tor that re-issues on one arm and converts on another stays a disposal candidate.
Failure Output
error[KORU030]: Resource 'obj' obligation <built!> was not discharged. Call: settle
--> tests/regression/300_ADVANCED_FEATURES/330_PHANTOM_TYPES/330_133_partial_reissue_keeps_converting_arm/input.kz:67:0
❌ Compiler coordination error: Auto-discharge failed (multiple disposal options or no disposal event)
(set KORU_BACKEND_TRACE=1 for the backend return trace) Code
// A tor that re-issues its input state on ONE arm and genuinely converts it
// on ANOTHER stays a disposal candidate for that state.
//
// 330_118 states the rule this pins the other half of: "A CONVERTING tor is
// a different thing and stays a legitimate candidate — only same-obligation
// conservation is disqualifying." 330_118's `step` conserves on EVERY arm,
// so excluding it is right. `settle` below conserves on one arm and converts
// on the other, and the converting arm is called explicitly here.
//
// RED. `eventReIssuesObligation` walks every branch and returns "excluded"
// on the FIRST that re-issues, with no check for whether another arm
// converts — so a partially-conserving tor is dropped from the candidate
// list entirely and the caller is told `No tor accepts <!built>` while
// calling the arm that accepts it. The predicate exists twice, independently
// (phantom_semantic_checker.zig:296, auto_discharge_inserter.zig:3057), and
// both copies carry the same any-arm test, so both `koruc` and
// `--auto-discharge=disable` fail with differently-worded versions of it.
//
// Invisible to `--check`: shape-checking does not walk the obligation flow.
// Needs no cross-module import and no FFI beyond an allocation — the
// one-tor, two-arm shape is the whole trigger.
//
// Found 2026-08-07 by the ukstore Unikraft lift, whose `add` tor had exactly
// this shape (a "nothing happened, retry" refusal re-minting the input).
//
// RE-SPELLED 2026-08-25: the original flow hung named arms off mk's bare
// return (`| ready obj |> ...`), which KORU021 (2026-08-18) refuses — the
// pin died on its own spelling and could not reach the predicate. Now
// mk's result is bound (`~mk(): o |> settle(obj: o)`) with bare-return
// bindings grounded on 330_027/330_123; settle's two arms keep their
// tagged event spelling (two arms, so tags are legal).
//
// PREDICATE FIXED same day (phantom_semantic_checker.zig +
// auto_discharge_inserter.zig): both copies now require conservation on
// EVERY exit before excluding. Measured: settle is a candidate again —
// the refusal reads `... was not discharged. Call: settle` — but the
// program still refuses, because candidacy is not INSERTABILITY: the
// auto-insert path takes only single-void disposers, and inserting
// settle would mint <held!>, needing a second synthesized call. So this
// pin stays RED, now one step further: the remaining gap is chained
// discharge synthesis (insert settle -> then finish), or an authoring
// rule that names where the author must write the chain themselves.
const std = @import("std");
const Obj = struct { v: i32 };
~pub tor mk { } -> *Obj<built!>
~proc mk|zig {
const o = std.heap.page_allocator.create(Obj) catch unreachable;
o.* = Obj{ .v = 1 };
return o;
}
~pub tor settle { obj: *Obj<!built> }
| ok *Obj<held!>
| refused { obj: *Obj<built!>, reason: string }
~proc settle|zig {
return .{ .ok = obj };
}
~pub tor finish { obj: *Obj<!held> }
~proc finish|zig {
_ = obj;
std.debug.print("settled\n", .{});
}
~mk(): o |> settle(obj: o)
| ok held |> finish(obj: held)
| refused _ |> _
Expected output
settled
Flows
flow ~mk click a branch to expand · @labels scroll to their anchor
mk
Test Configuration
MUST_RUN