✓
Passing Passing: the compiler rejects this program as expected.
Code
// PINS: an effect payload and a capture nested inside its own handler are
// real shadowing, and Koru refuses it.
//
// ! item x |> classify(v: x) <- x bound here…
// | lo x |> sink(v: x) <- …still live when this binds
//
// The two are NOT siblings. `| lo` hangs off `classify`, which is inside the
// `! item` handler body, so the payload's `x` is in scope when the capture
// binds and the inner name hides the outer one. That is nesting, and it is the
// whole difference from `| lo x` / `| hi x`, which share a name legally because
// disjoint arms never see each other.
//
// Ruled by Lars 2026-07-27. This test previously asserted the opposite — that
// the two names denote one value flowing through, that "Koru has no rule
// against it", and that the EMITTER should alpha-rename one of them. That
// reading was wrong: the language forbids shadowing, and the pun machinery
// rests on it ("no shadowing means the in-scope match is always unique",
// ast_transform.zig). Allowing it would have meant a scope stack in the
// desugar, where today a nested duplicate is skipped and a pun inside the inner
// scope silently resolves to the OUTER binding.
//
// What changed is the SENTENCE, not the verdict. This program has always been
// refused; it was refused by Zig — `capture 'x' shadows function parameter
// from outer scope` — a host error standing in for a language one, pointing
// into a file the author never opened. KORU106 names the binding, both sites,
// and what to do instead.
//
// The fix an author writes is to rename one of them. Sibling arms are
// untouched: `| lo x` and `| hi x` still share `x` legally, and this wall does
// not reach them.
const std = @import("std");
~pub tor run { n: u64 }
! item u64
~pub tor classify { v: u64 }
| lo u64
| hi u64
~pub tor sink { v: u64 }
~proc run|zig {
var i: u64 = 0;
while (i < n) : (i += 1) {
item(i);
}
}
~proc classify|zig {
if (v < 10) return .{ .lo = v };
return .{ .hi = v };
}
~proc sink|zig {
std.debug.print("sink {d}\n", .{v});
}
// The collision: effect binding `x` and branch captures `x` share a name.
~run(n: 1)
! item x |> classify(v: x)
| lo x |> sink(v: x)
| hi x |> sink(v: x)
Actual compiler output
error[KORU106]: 'x' is already bound at line 60 — this bind would shadow it, and Koru has no shadowing
--> tests/regression/800_CHALLENGES/800_002_effect_branch_capture_shadow/input.kz:61:4
|
61 | | lo x |> sink(v: x)
| ^
hint: give the second bind a different name, or drop it and keep using the one already in scopeMust fail at frontend compile:
Parsing or type-checking must reject the program.
Error output must contain
KORU106Flows
flow ~run click a branch to expand · @labels scroll to their anchor
run (n: 1)
Test Configuration
MUST_ERROR