Skip to content

fix(codegen): default-export-of-named-fn wrapper forwards to body (#967)#968

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-ab81aa6309cb749bb
May 17, 2026
Merged

fix(codegen): default-export-of-named-fn wrapper forwards to body (#967)#968
proggeramlug merged 1 commit into
mainfrom
worktree-agent-ab81aa6309cb749bb

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

function add(a,b){…}; export default add; is the canonical npm-package
barrel idiom (ramda, date-fns, lodash-es, hundreds more). When that
shape was compiled as a native perry.compilePackages module, the
consumer's const fn = add; fn(2,3) returned undefined instead of
5. Direct add(2,3) worked — the bug was specific to closure-form
access (alias to local, callback passing, indirect call).

Root cause: codegen's sub-bug-B wrapper-alias loop emitted a NO-OP
__perry_wrap_perry_fn_<src>__default wrapper for every
Export::Named { local, exported } where local == exported and
local wasn't a HIR function name. The branch's original purpose was
to make import * as z; export { z }; (re-exported namespace) and
export { SomeClass } link by emitting an → undefined thunk. But the
default-export shape ALSO satisfies local == exported == "default"
with func_by_local_name.contains_key("default") == false — because
the HIR function's name is add, not default. So the no-op fired
and short-circuited every closure-form call through the default
import.

The HIR lowerer at perry-hir/src/lower.rs:5601 correctly registers
the alias in module.exported_functions.push(("default", add_id)).
The fix consults that map: when a local==exported no-function-by-
local-name export resolves to an aliased real function, emit a
forwarding wrapper (closure_ptr → tail-call perry_fn_<src>__add)
instead of the no-op. Mirrors the local != exported branch at L2792
which already handles renamed exports correctly.

Test plan

  • New test-files/test_default_import_as_value.ts +
    _helper.ts cover four value-position shapes: local alias,
    function-parameter pass-through, closure-capture, direct call.
    Output matches node --experimental-strip-types byte-for-byte
    (5 / 30 / 15 / 5).
  • test-files/test_lodash_default_import_methods.ts (issue feat(security): #498 — pin SHA-256 of perry.nativeLibrary prebuilt archives in perry.lock #957
    regression) still passes.
  • test-files/test_issue_678_reexport_default.ts still passes.
  • CI: lint / cargo-test / parity / compile-smoke /
    api-docs-drift / security-audit once green.

Out of scope (related but separate bugs)

With this fix applied, two packages from the original compat sweep
still don't fully work:

  • ramda R.sum([1..5]) returns [object Object] because
    Function.prototype.apply has no runtime handler — fn.apply(this, args) returns the receiver as a stub instead of invoking fn.
    Triggers inside ramda's _curry1/_curry2/_curry3 machinery.
    Untriaged.
  • date-fns format(new Date(), 'yyyy-MM-dd') throws
    RangeError: Invalid time value because Date.prototype.constructor
    returns undefined, breaking constructFrom's
    new date.constructor(value) cloning. Untriaged.

Both packages exercise the default-import-as-value shape internally
many times, so this fix is a prerequisite for any future work on
them — without it the closure-dispatch would short-circuit before
reaching the apply / constructor failures.

Version

0.5.980 → 0.5.981

When `function add(a,b){…}; export default add;` is compiled as a native
package, the consumer's `const fn = add; fn(2,3)` returned undefined
because sub-bug-B's wrapper-alias loop emitted a no-op
`__perry_wrap_perry_fn_<src>__default` for every `local==exported`
named export whose local wasn't a HIR function name — but the canonical
default-export shape lowers to `Export::Named { local: "default",
exported: "default" }` where the function's HIR name is `add`. Consult
`hir.exported_functions` for an alias entry before falling through;
when found, emit a forwarding wrapper to the real function body.

This is the npm-package barrel idiom used by ramda, date-fns, lodash-es,
and many others. The fix unblocks closure-form access of those default
imports (callback passing, alias-then-call). Direct calls already worked
via the separate `perry_fn_<src>__default` alias loop, so this fixes the
"function returns undefined when called through a local" wedge specifically.
@proggeramlug proggeramlug force-pushed the worktree-agent-ab81aa6309cb749bb branch from e64bade to cf611b6 Compare May 17, 2026 23:15
@proggeramlug proggeramlug merged commit 7586a92 into main May 17, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-ab81aa6309cb749bb branch May 17, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant