Skip to content

BUG: a closure RETURNED from a module runs against the caller's chunk — silent truncation at top level #696

Description

@Masterplanner25

Summary

A closure returned out of an imported module carries an address into the module's chunk, and nothing marks it. Calling it runs the caller's instructions at that address.

This is the mirror of #691. #691 was a caller's closure passed into a module; that direction is fixed in #695. This is a module's closure handed out, and it is untouched by that fix — the fix resolves a closure's origin from contexts that are recorded somewhere reachable (a _ClosureProxy, a cross-module frame, a caller VM), and nothing records a module's context on the way out. VM._foreign_closure_origin correctly returns None here, which is its documented "leave the call as it was" case.

The worst case is silent, and it does not need a workflow. Plain top-level code, one module, one returned closure.

Reproduction

m.nd:

fn plain_maker() { return fn() { return 99i } }
fn make_adder(n) { return fn(x) { return x + n } }
fn pad_a() { return 1i }
fn pad_b() { return 2i }

1. Silent truncation at top level — the worst case

import "./m.nd" as m
fn main() {
    let f = m.plain_maker()
    print("no-upvalue at top level: \(f())")
}
$ nodus run r_top.nd
$

No output. No error. Exit 0. The print never ran.

2. Stack underflow in a step body

import "./m.nd" as m
workflow w {
    step a { let f = m.plain_maker(); print("in step: \(f())"); return "ok" }
}
fn main() { let r = run_workflow(w); print("failed: \(r["failed"])") }
Runtime error at r_step.nd:3:72: Stack underflow
  at __anon_3 (r_step.nd:3:72)
  called from __anon_1 (r_step.nd:3:58)

3. A returned closure with an upvalue

import "./m.nd" as m
fn main() {
    let g = m.make_adder(3i)
    print("upvalue at top level: \(g(10i))")
}
Type error at r_upv.nd:3:26: Method calls are only supported on records
  at __anon_1 (r_upv.nd:3:26)
  called from main (r_upv.nd:4:38)

As in #691, the three symptoms are not three defects — the symptom is whatever instruction happens to sit at the returned closure's address in the caller's chunk. Vary the padding functions in m.nd and the symptom changes.

Expected behavior

m.plain_maker()() should return 99. A factory function is an ordinary thing for a module to export, and nodus-flow-style builder APIs are exactly this shape.

Whatever else, the silent case has to become either correct or an error. A print that does not run, with exit 0, is the failure mode #691 was filed for.

Not a regression

Confirmed identical on main before #695 and after it. The construct is not new, so this is likely present in every release.

Fix direction

The two directions are one question — "which chunk was this closure compiled against?" — and #695 answers it for values crossing in. What is missing is a record for values crossing out.

Two ways in, so probably two ways out to cover:

  1. NodusModule.invoke_function's return value — the detached-VM path.
  2. _op_return popping a frame that carries a cross_module_ctx — the in-VM path.

Both should mark a returned Closure local to the chunk being left, so VM._foreign_closure_origin can find it. _ClosureProxy already does exactly this in the other direction (it captures origin_ctx at the boundary it crosses), so the shape is established; the open question is whether to reuse it or add a lighter-weight carrier, and whether closures nested inside a returned list/map/record must be walked too — #339 says they must, since that is the case the argument direction missed the first time.

Check the reverse before calling it done. #476 is the cautionary one: the asymmetry ran in both directions and fixing one left the other. Here the pair is "into a module" (#691, fixed) and "out of a module" (this), and a third case worth probing is a closure that goes in, is stored, and comes back out.

Affected versions

Confirmed on main at 5.8.0+. Almost certainly every earlier release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingseverity:highMajor feature broken or significantly wrongsubsystem:runtime

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions