Skip to content

fix(modules): exports aliasing a declared function are functions, not var getters - #6962

Merged
proggeramlug merged 1 commit into
mainfrom
fix/t3code-effect-runtime-main
Jul 29, 2026
Merged

fix(modules): exports aliasing a declared function are functions, not var getters#6962
proggeramlug merged 1 commit into
mainfrom
fix/t3code-effect-runtime-main

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The bug

export const alias = impl (where impl is a declared function) lands in both exported_objects and exported_functions — HIR records the alias with the origin's FuncId. The var-vs-function classification in run_pipeline only excluded declaration names, so the alias was classified as an exported variable, whose cross-module convention is a zero-arg getter. But origin-name resolution points perry_fn_<mod>__<alias> at the #460 forwarding wrapper — the function body — so the "getter" call actually invoked the function.

Two user-visible failures:

// inner.ts
export function impl(x: number): number { return x * 2 }
export const alias = impl

// main.ts
import * as NS from "./inner.ts"
console.log(typeof NS.alias)   // "number"  (expected "function")
console.log(NS.alias(21))      // TypeError: value is not a function

…and the source module's own namespace populator hits the same path while building its namespace object, so the aliased function runs during module init with zeroed arguments.

The fix

In run_pipeline:

  • exclude exported_functions alias names from exported_var_names (the consumer-side binding read), and
  • resolve alias entries in the namespace-entry builder to the origin function's wrap symbol — LocalFunction same-module, ForeignFunction (under the origin name) cross-module — instead of falling through to the ForeignVar getter.

Tests

crates/perry/tests/namespace_alias_export_of_function.rs — two cases (namespace member, named import). Both fail on main (alias-typeof: numberTypeError: value is not a function) and pass with the fix. Full repro suite for the surrounding campaign also green, no regressions.

Provenance

Found compiling the t3 Code server to native with Effect 4.0.0-beta.78, whose SchemaParser.ts is built almost entirely out of this shape (export const decodeSync = decodeUnknownSync, decodeEffect = decodeUnknownEffect, …). Diagnosed by disassembling that module's __init_body: sibling plain exports lowered to js_closure_alloc_singleton, while the aliases lowered to a direct bl perry_fn_…__decodeEffect with zeroed argument registers.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed aliased exported functions being incorrectly executed when accessed through dynamic namespaces or named imports.
    • Aliased functions now remain callable function values and are not invoked during module initialization or import binding.
  • Tests
    • Added regression coverage for namespace and named imports, including function type checks, return values, and initialization behavior.

… var getters

`export const alias = impl` (where `impl` is a declared function) lands in
BOTH `exported_objects` and `exported_functions` — HIR records the alias
with the origin's FuncId. The var-vs-function classification only excluded
*declaration* names, so the alias was classified as an exported VARIABLE,
whose cross-module convention is a zero-arg getter. But origin-name
resolution points `perry_fn_<mod>__<alias>` at the #460 forwarding wrapper
— the function BODY — so the "getter" call actually INVOKED the function.

Two user-visible failures, both covered by the new test:

- Reading the binding yielded `impl(<zeroed args>)`'s return value instead
  of the closure: `typeof NS.alias` was `number`, and calling it threw
  `TypeError: value is not a function`.
- The source module's own namespace populator hit the same path while
  building its namespace object, so the function RAN during module init.

Fix, in `run_pipeline`:
- exclude `exported_functions` alias names from `exported_var_names`
  (the consumer-side binding read), and
- resolve alias entries in the namespace-entry builder to the ORIGIN
  function's wrap symbol — `LocalFunction` for same-module,
  `ForeignFunction` (under the origin name) for cross-module — instead of
  falling through to the `ForeignVar` getter.

Found compiling the t3 Code server (github.com/pingdotgg/t3code) to native
with Effect 4.0.0-beta.78, whose `SchemaParser.ts` is built almost entirely
out of this shape (`export const decodeSync = decodeUnknownSync`,
`decodeEffect = decodeUnknownEffect`, …). Diagnosed by disassembling the
module's `__init_body`: sibling exports lowered to
`js_closure_alloc_singleton` while the aliases lowered to a direct
`bl perry_fn_…__decodeEffect` with zeroed argument registers.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The compiler now preserves aliases of declared functions as function bindings during export classification and dynamic namespace construction. A regression test verifies local and named namespace imports remain callable without executing the origin function during binding reads.

Changes

Namespace alias function exports

Layer / File(s) Summary
Export classification
crates/perry/src/commands/compile/run_pipeline.rs
Export aliases for declared functions are excluded from exported_var_names, preventing getter-style import handling.
Namespace function resolution and regression coverage
crates/perry/src/commands/compile/run_pipeline.rs, crates/perry/tests/namespace_alias_export_of_function.rs
Local and cross-module aliases resolve to function closure singletons, and tests verify namespace and named imports remain callable without initialization-time execution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: alias exports of declared functions now behave as functions, not variable getters.
Description check ✅ Passed The description is detailed and covers the bug, fix, tests, and provenance, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/t3code-effect-runtime-main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry/src/commands/compile/run_pipeline.rs (1)

1886-1909: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated alias-lookup logic between local and cross-module branches.

Both branches repeat the same "find name in exported_functions, then resolve the FuncId to a Function" lookup (.find(|(n, _)| n == &fe.source_local || n == &fe.name).and_then(|(_, fid)| ...functions.iter().find(|f| f.id == *fid))). Extracting a small helper (e.g. fn resolve_function_alias<'a>(hir: &'a Module, source_local: &str, name: &str) -> Option<&'a Function>) would keep both call sites in sync if this resolution logic needs a future fix.

♻️ Suggested helper
+fn resolve_function_alias<'a>(
+    hir: &'a perry_hir::Module,
+    source_local: &str,
+    name: &str,
+) -> Option<&'a perry_hir::Function> {
+    hir.exported_functions
+        .iter()
+        .find(|(n, _)| n == source_local || n == name)
+        .and_then(|(_, fid)| hir.functions.iter().find(|f| f.id == *fid))
+}

Also applies to: 1936-1951

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/run_pipeline.rs` around lines 1886 - 1909,
Extract the duplicated exported-function-to-Function lookup into a shared
helper, such as resolve_function_alias, accepting the target Module and both
source_local and name values. Replace the inline lookup in the shown branch and
the corresponding cross-module branch around the other alias-resolution block,
preserving the existing matching order and FuncId resolution behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry/src/commands/compile/run_pipeline.rs`:
- Around line 1886-1909: Extract the duplicated exported-function-to-Function
lookup into a shared helper, such as resolve_function_alias, accepting the
target Module and both source_local and name values. Replace the inline lookup
in the shown branch and the corresponding cross-module branch around the other
alias-resolution block, preserving the existing matching order and FuncId
resolution behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 635adff2-1f48-4991-a89d-21d491bf47a9

📥 Commits

Reviewing files that changed from the base of the PR and between df68b83 and d6ff819.

📒 Files selected for processing (2)
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/tests/namespace_alias_export_of_function.rs

@proggeramlug
proggeramlug merged commit 22411bd into main Jul 29, 2026
31 of 33 checks passed
@proggeramlug
proggeramlug deleted the fix/t3code-effect-runtime-main branch July 29, 2026 04:53
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