[CONCURRENCY-CHANNEL] promises that recv(c) on a Channel<T> returns a T
with its full representation. It does for a direct local, a named-function
parameter, and a concrete named-record field. It does not for a function
return, a function-value/lambda parameter, or a generic/anonymous record field,
and the failure on those routes is silent.
What is already closed
Channel(0) is no longer a poison handle. Every Channel(n) lowers to
channel_create_checked (fiber_runtime.c:429),
which aborts with a diagnostic naming the capacity and the native code instead
of handing a negative word back as a Channel<T>. Because the check is at
runtime it covers literals, parameters, allocation failure and handle
exhaustion alike.
gen_recv also no longer falls back to the raw i64 wire word when the element
type is missing — it is a hard codegen error
(fiber.rs:174). Those two are not
what this issue is about.
The defect
The element owner — the tag that distinguishes a flat list from a nested one,
and a plain pointer from a Result payload — is carried on Value as
fiber_elem_owner / fiber_elem_payload_owner. FiberSig does not carry it:
// crates/osprey-codegen/src/builder.rs:245
pub(crate) struct FiberSig {
pub(crate) elem: LType,
pub(crate) result_inner: Option<LType>,
}
// crates/osprey-codegen/src/builder.rs:270
pub(crate) fn restore(self, mut value: Value) -> Value {
value.fiber_elem = Some(self.elem);
value.fiber_elem_result_inner = self.result_inner;
value // fiber_elem_owner is never restored
}
So a restored channel value passes the gen_recv guard — fiber_elem is
Some(LType::Ptr) — and is then unboxed as an untagged pointer. The nested
descriptor is gone, with no diagnostic and no crash. This is the silent-wrong-
answer class, not the loud one.
Three routes reach it:
- Function return.
expr.rs:1171-1175
sets the owner from cg.fn_ret_owner(name), which calls owner_name on the
Channel type itself and deliberately answers None, then applies
FiberSig::restore, which does not supply one either. recv(makeNestedChannel())
recovers "pointer" and loses the nested-list descriptor.
- Function-value / lambda parameter.
closure.rs:216
binds every parameter with incoming_param(cg, ..., None) — no owner at all,
unlike the named-function path in
monofn.rs:242, which passes
owner.clone().
- Generic and anonymous record fields.
ObjField is
(String, LType, Option<String>) (builder.rs:310) —
there is no FiberSig slot in a layout. The named-record repair
(ctor_field_handle + gen_field_access) recovers the metadata by looking
back at the declared field type of a named constructor. A generic
instantiation or an anonymous object field has no such declaration to look
back at, so the lookup is bypassed.
The common cause is that the metadata is reconstructed from declarations at each
consumer instead of travelling with the value.
Reproduction shape
type Grid = Grid { rows: List<List<int>> }
fn makeNestedChannel() = Channel(4)
fn main() = {
let c = makeNestedChannel()
send(c, [[1, 2, 3], [4, 5]])
let got = recv(c) // recovered as an untagged pointer
print("${listLength(listGet(got, 0))}") // wants 3
}
The same shape through a lambda parameter (|ch| => recv(ch)) and through a
generic record field (type Box<t> = Box { slot: t }) exercises routes 2 and 3.
What "fixed" means
Carry FiberSig plus element owner and payload owner with the value through
every ABI boundary — returns, aliases, captures, function-value parameters, and
every field layout including generic instantiations and anonymous objects.
Reconstructing from a declaration at the consumer is what leaves the holes.
Each route needs its own corpus program under tests/regressions/, and per the
branch review's acceptance standard a "did not crash" or outer-listLength
assertion is not enough. Each one must:
- send at least two structurally distinct nested payloads,
- receive them in FIFO order,
- assert outer and inner lengths plus exact edge values,
- exist in both Default and ML syntax (sharing one golden), and
- return ARC live-object and live-byte counts to baseline after cleanup.
Routes still owed observable assertions: function return, let alias,
receive-before-send, function-value parameter, closure capture, generic record
field, anonymous object field.
Related
tests/core/collections/nested_generic_collections_fibers.test.osp.expectedoutput
carries an explicit, reasoned skip for a Channel<collection> round trip. That
skip is the visible tip of this issue; it can only be deleted once the routes
above are proven, not merely once one of them passes.
[CONCURRENCY-CHANNEL]promises thatrecv(c)on aChannel<T>returns aTwith its full representation. It does for a direct local, a named-function
parameter, and a concrete named-record field. It does not for a function
return, a function-value/lambda parameter, or a generic/anonymous record field,
and the failure on those routes is silent.
What is already closed
Channel(0)is no longer a poison handle. EveryChannel(n)lowers tochannel_create_checked(fiber_runtime.c:429),which aborts with a diagnostic naming the capacity and the native code instead
of handing a negative word back as a
Channel<T>. Because the check is atruntime it covers literals, parameters, allocation failure and handle
exhaustion alike.
gen_recvalso no longer falls back to the rawi64wire word when the elementtype is missing — it is a hard codegen error
(
fiber.rs:174). Those two are notwhat this issue is about.
The defect
The element owner — the tag that distinguishes a flat list from a nested one,
and a plain pointer from a
Resultpayload — is carried onValueasfiber_elem_owner/fiber_elem_payload_owner.FiberSigdoes not carry it:So a restored channel value passes the
gen_recvguard —fiber_elemisSome(LType::Ptr)— and is then unboxed as an untagged pointer. The nesteddescriptor is gone, with no diagnostic and no crash. This is the silent-wrong-
answer class, not the loud one.
Three routes reach it:
expr.rs:1171-1175sets the owner from
cg.fn_ret_owner(name), which callsowner_nameon theChanneltype itself and deliberately answersNone, then appliesFiberSig::restore, which does not supply one either.recv(makeNestedChannel())recovers "pointer" and loses the nested-list descriptor.
closure.rs:216binds every parameter with
incoming_param(cg, ..., None)— no owner at all,unlike the named-function path in
monofn.rs:242, which passesowner.clone().ObjFieldis(String, LType, Option<String>)(builder.rs:310) —there is no
FiberSigslot in a layout. The named-record repair(
ctor_field_handle+gen_field_access) recovers the metadata by lookingback at the declared field type of a named constructor. A generic
instantiation or an anonymous object field has no such declaration to look
back at, so the lookup is bypassed.
The common cause is that the metadata is reconstructed from declarations at each
consumer instead of travelling with the value.
Reproduction shape
The same shape through a lambda parameter (
|ch| => recv(ch)) and through ageneric record field (
type Box<t> = Box { slot: t }) exercises routes 2 and 3.What "fixed" means
Carry
FiberSigplus element owner and payload owner with the value throughevery ABI boundary — returns, aliases, captures, function-value parameters, and
every field layout including generic instantiations and anonymous objects.
Reconstructing from a declaration at the consumer is what leaves the holes.
Each route needs its own corpus program under
tests/regressions/, and per thebranch review's acceptance standard a "did not crash" or outer-
listLengthassertion is not enough. Each one must:
Routes still owed observable assertions: function return,
letalias,receive-before-send, function-value parameter, closure capture, generic record
field, anonymous object field.
Related
tests/core/collections/nested_generic_collections_fibers.test.osp.expectedoutputcarries an explicit, reasoned skip for a
Channel<collection>round trip. Thatskip is the visible tip of this issue; it can only be deleted once the routes
above are proven, not merely once one of them passes.