✗
Failing This test is currently failing.
Failed: must-fail-passed
Failure Output
🎯 Compiler coordination: Passes: 14 (flow-based: frontend, analysis, emission) Code
// PIN (RED) — `--auto-discharge=disable` fails to enforce an obligation that
// has passed through a bare-borrow.
//
// `open` grants <hashing!>. `update` BORROWS it (bare <hashing>), so `d` is
// still live and still owing after the call. We then FORGET to finalize `d`.
//
// default `koruc build` → REJECTED: KORU030 "Resource 'd' <hashing!> was
// not discharged. Call one of: final.hex, final.bytes"
// --auto-discharge=disable → currently COMPILES CLEAN ← THE BUG
//
// `koruc --help` promises the opposite for disable: "Disable automatic disposal
// insertion — phantom errors WILL fire for unsatisfied obligations." The
// trigger is specifically the bare-borrow: WITHOUT the `update` line, disable
// correctly rejects (via the phantom-semantic-checker). WITH it, the obligation
// is dropped from tracking and the leak slips through. The number of discharge
// events is irrelevant; the borrow is what blinds the check.
//
// Enforcement under disable lives in a different pass (phantom-semantic-checker)
// than under default (auto-discharge-inserter); the two disagree on whether an
// obligation survives a bare-borrow. Control test: the green sibling
// 2104_21_open_tx_forgot_close is the same forgotten-discharge WITHOUT a borrow,
// and disable rejects it correctly.
//
// MUST_FAIL under --auto-discharge=disable (see COMPILER_FLAGS). RED until the
// disable-path obligation check tracks obligations through a bare-borrow.
~import app/digest
~import std/io
~app/digest:open()
| ctx d |> app/digest:update(d, n: 1) |> std/io:print.ln("BUG: forgot to finalize d after a borrow, but disable compiled it clean")
Must contain:
not dischargedError Verification
Actual Compiler Output
🎯 Compiler coordination: Passes: 14 (flow-based: frontend, analysis, emission)Flows
flow ~open click a branch to expand · @labels scroll to their anchor
open
Imported Files
// Mock digest lifecycle — mirrors the real shape of evp's `hashing` obligation
// (koru-libs evp/index.kz) without any external C. `open` grants <hashing!>;
// `update` BORROWS it (bare <hashing>) so the same handle stays live and still
// owing after the call; `final.hex`/`final.bytes` CONSUME it (<!hashing>) and
// discharge the obligation. page_allocator stands in for the EVP context so the
// pin has zero link dependencies.
const std = @import("std");
const Digest = struct { n: i64 };
~pub event open {}
| ctx *Digest<hashing!>
~proc open|zig {
const d = std.heap.page_allocator.create(Digest) catch unreachable;
d.* = Digest{ .n = 0 };
return .{ .ctx = d };
}
// Feed a chunk. BORROWS the handle (bare <hashing>): the obligation stays with
// the caller, so `d` is still live (and still owing <hashing!>) after this call.
// This borrow is the trigger for the bug the sibling input.kz pins.
~pub event update { d: *Digest<hashing>, n: i64 }
~proc update|zig {
d.n += n;
}
// Discharge the obligation. Two options, neither a `[!]` default — same as evp.
~pub event final.hex { d: *Digest<!hashing> }
| digest []const u8
~proc final.hex|zig {
std.heap.page_allocator.destroy(d);
return .{ .digest = "hex" };
}
~pub event final.bytes { d: *Digest<!hashing> }
| digest []const u8
~proc final.bytes|zig {
std.heap.page_allocator.destroy(d);
return .{ .digest = "bytes" };
}
Test Configuration
MUST_FAIL
Compiler Flags:
--auto-discharge=disable