Skip to content

Channel element ABI metadata is lost on returns, function-value parameters and generic/anonymous fields — recv() silently drops the nested descriptor #227

Description

@MelbourneDeveloper

[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:

  1. 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.
  2. 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().
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions