✗
Failing This test is currently failing.
Failed: backend-exec
Failure Output
error[KORU030]: Resource 'return of abandon(...)' obligation <owned!> was not discharged. Call: bin.close
--> tests/regression/300_ADVANCED_FEATURES/330_PHANTOM_TYPES/330_120_unbound_bare_return_obligation_auto_discharges/input.k:35: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
// PINS: a bare return carrying an obligation auto-discharges even when nobody
// NAMED it.
//
// `abandon(j)` hands back a *Bin<owned!>. `bin.close` is its one void terminal,
// so there is nothing to decide — and the compiler proves it knows, because the
// refusal names the call it declines to make:
//
// error[KORU030]: Resource 'return of abandon(...)' obligation <owned!> was
// not discharged. Call: bin.close
//
// The control is one character. Write `: b` and the identical program compiles,
// auto-discharge inserting `bin.close(b)`. A NAME is the only difference, and a
// name is not information the compiler was missing.
//
// Auto-discharge exists to settle obligations the author did not write down —
// `get(url) | ok r |> print(...)` gets its `close(r)` unasked. A mid-chain
// return is the same situation. The asymmetry is an accident of the mechanism:
// auto_discharge_inserter.zig records an unnamed return as
// `__unbound_return.<event>` and force-marks it not_auto_dischargeable, because
// an unnamed value has no referenceable expression to pass. The machinery to
// mint one already lives next door — cloneContinuationWithReturnBinding
// synthesizes `_auto_N` for `: _` discards.
//
// The `Bin` is an implementation detail of abandoning a `Job`; requiring `: b`
// purely so the next line can close it is ceremony over a value the author
// never asked for.
//
// Found writing koru-examples/downloads, where the natural spelling of a store
// drain is `! discharge item |> cancel(item.p)` and curl's `cancel` hands back
// the batch. The store and the effect arm are both BYSTANDERS: this reproduces
// at top level with neither, which is why the pin lives here and not in 690.
import app/lib/job
app/lib/job:start(): j |> app/lib/job:abandon(j)
Expected output
abandoned 7
bin closed
Flows
flow ~start click a branch to expand · @labels scroll to their anchor
start
Imported Files
// A resource whose only exit RETURNS something — the shape a promise has.
//
// `abandon` consumes <!live> and hands back a *Bin<owned!>, so it cannot be
// called unattended at a store teardown (nobody is there to take the Bin). That
// makes a *Job<live!> column drain-required: legal to store, but the author has
// to say how. Modelled on curl's `*Pending<open!>`, whose exits are `drain`,
// `await` and `cancel` — branches, branches, and a return.
const std = @import("std");
const Job = struct { id: i32 };
const Bin = struct { id: i32 };
~pub tor start { } -> *Job<live!>
~proc start|zig {
const j = std.heap.page_allocator.create(Job) catch unreachable;
j.* = .{ .id = 7 };
return j;
}
// The ONLY exit from <live!>, and it returns. Not an unattended discharger.
~pub tor abandon { j: *Job<!live> } -> *Bin<owned!>
~proc abandon|zig {
std.debug.print("abandoned {d}\n", .{j.id});
const b = std.heap.page_allocator.create(Bin) catch unreachable;
b.* = .{ .id = j.id };
std.heap.page_allocator.destroy(j);
return b;
}
// Void terminal for what `abandon` hands back — exactly one, so a Bin settles
// by ordinary auto-discharge wherever it appears.
~pub tor bin.close { b: *Bin<!owned> }
~proc bin.close|zig {
std.debug.print("bin closed\n", .{});
std.heap.page_allocator.destroy(b);
}
Test Configuration
MUST_RUN