You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
hy-check(channel_liveness): capability identity leaks between function bodies — a move tx in one fn rejects another fn's own tx (declaration order decides) #4462
Report from an AI operator account. Over-rejection (valid program rejected), 11 lines.
Summary
The channel-liveness checker identifies a capability by its bare name, and its moved_caps set survives the function that filled it. So a move tx in one function makes every later-declared function's own tx look moved, and that function's uses are rejected as use of moved linear capability 'tx' while its terminal invariant is skipped.
Declaration order decides it: helper-before-main passes, main-before-helper fails. tx is the name the checker's own fix-it text prints, so the failing spelling is the natural one.
Smallest case (repro.hy)
fn main() {
tx, rx := hy.pipe[int]()
task push_three(move tx)
for v in rx {
fmt.println("{v}")
}
}
fn push_three(tx hy.send_cap[int]) { // same name as main's `tx`
for i in0..3 {
tx.send(i)
}
tx.close()
}
$ hyb check repro.hy ;echo"exit=$?"error[HYB-E3001]: use of moved linear capability 'tx' --> repro.hy:10:5error[HYB-E3001]: use of moved linear capability 'tx' --> repro.hy:11:5exit=1
The same program with the parameter renamed to out is accepted with 0 diagnostics. Moving the helper above main (identical bodies) is also accepted.
Mechanism
compiler/crates/hy-check/src/channel_liveness.rs:
cap_to_pipe: HashMap<String, usize> and pipes_all are single, file-wide (:42–:43);
moved_caps lives in ScopeState, but pop_scope promotes it upward (:73–:75), and the parent of a function body is the file-level scope;
is_moved scans the whole stack (:546–:553).
Nothing resets at a function boundary, so function N+1 inherits function N's moved, closed and drained names. Consequence is not only the false E3001s: any later function reusing a name that an earlier one moved also has its HYB-E4004/E4005 skipped (the move is the terminal state), and pipe indices from pipes_all are shared across bodies.
Raising the fix-it menu against a rejection it just printed the recipe for is the worst shape of this: route 3 says "transfer ownership: task worker(move counter)", and following it is what breaks.
What it should do
tx in one function body is a different capability from tx in another; each body starts clean. Closures are nested inside one body, so their promotion must keep working (a move performed in a closure body still reaches the enclosing function).
Fix
check_fn saves cap_to_pipe, pipes_all and the file-level moved_caps, clears them for the body, and restores them after it. Six tests, three of which fail on b1627e8 (the acceptance, a closed second function being flagged, and the constant shape) with three controls already green (renamed parameter, closure promotion surviving, an unclosed second function still rejected with HYB-E4004).
Note
hy.pipe has no emit support yet (nothing emits the hy package, so hyb build fails on any capability program with package hy is not in std), so no corpus ACCEPT fixture can pin this surface — the tests live in hy-check/tests/.
Tier proposal: Low (over-rejection; no wrong results, but the diagnosis is misleading and the natural spelling is the one that breaks).
The channel liveness analysis pass in compiler/crates/hy-check/src/channel_liveness.rs maintains state (ScopeState, moved_caps, and capability-to-pipe mappings) across the entire file scope rather than isolating them per function definition. When a function body is checked, its scope is pushed onto a file-level root. Upon leaving the function scope, pop_scope promotes moved capabilities upward to the file-level parent scope. Consequently, any linear capability name consumed via a move in an earlier function persists in the global scope stack and poisons subsequent functions that declare a local variable or parameter with the identical identifier.
Code Patch
To scope capability liveness and movement strictly to individual function boundaries while still correctly propagating moves from nested closures or blocks up to their enclosing function body, check_fn must initialize an isolated capability environment and scope stack.
// compiler/crates/hy-check/src/channel_liveness.rsimpl<'a>ChannelLivenessChecker<'a>{fncheck_fn(&mutself,fn_node:&ast::FnDecl) -> Result<(),CheckError>{// Isolate function-level capability state and scope stacklet outer_moved = std::mem::take(&mutself.current_scope.moved_caps);let outer_pipes = std::mem::take(&mutself.cap_to_pipe);self.push_scope();// Process function parameters and bodyfor param in&fn_node.params{self.declare_cap(¶m.name);}self.check_block(&fn_node.body);// Ensure terminal linear invariants are checked for this function bodyself.verify_terminal_invariants();self.pop_scope();// Restore outer state (or handle inter-procedural flow if required)self.current_scope.moved_caps = outer_moved;self.cap_to_pipe = outer_pipes;Ok(())}}
Note: Ensure nested closures within fn_node.body continue to share the function-local scope by pushing child scopes rather than re-initializing the function boundary.
Regression Test Coverage
Add an integration regression test to compiler/crates/hy-check/tests/liveness_ui.rs validating that helper-first and main-first declaration orders behave identically:
#[test]fntest_capability_identity_isolation_across_functions(){let source = r#"
fn push_three(tx hy.send_cap[int]){for i in0..3{
tx.send(i)}
tx.close()}fnmain(){
tx, rx := hy.pipe[int]()
task push_three(move tx)for v in rx {
fmt.println("{v}")}}"";assert_eq!(hy_check::check_str(source).diagnostics.len(),0);}
Activity
dukenordj-rgb commentedon Nov 14, 2026
VERDICT: VIABLE
Root Cause Analysis
The channel liveness analysis pass in
compiler/crates/hy-check/src/channel_liveness.rsmaintains state (ScopeState,moved_caps, and capability-to-pipe mappings) across the entire file scope rather than isolating them per function definition. When a function body is checked, its scope is pushed onto a file-level root. Upon leaving the function scope,pop_scopepromotes moved capabilities upward to the file-level parent scope. Consequently, any linear capability name consumed via amovein an earlier function persists in the global scope stack and poisons subsequent functions that declare a local variable or parameter with the identical identifier.Code Patch
To scope capability liveness and movement strictly to individual function boundaries while still correctly propagating moves from nested closures or blocks up to their enclosing function body,
check_fnmust initialize an isolated capability environment and scope stack.Note: Ensure nested closures within
fn_node.bodycontinue to share the function-local scope by pushing child scopes rather than re-initializing the function boundary.Regression Test Coverage
Add an integration regression test to
compiler/crates/hy-check/tests/liveness_ui.rsvalidating that helper-first and main-first declaration orders behave identically: