diff --git a/crates/perry-codegen/src/codegen.rs b/crates/perry-codegen/src/codegen.rs index 8b437ddb..5ac05a5d 100644 --- a/crates/perry-codegen/src/codegen.rs +++ b/crates/perry-codegen/src/codegen.rs @@ -82,6 +82,19 @@ pub struct CompileOptions { /// `perry_fn___`. Built by the CLI driver /// from each module's `hir.imports` table. pub import_function_prefixes: std::collections::HashMap, + /// Issue #678: for imports that traverse a re-export rename + /// (e.g. `export { default as render } from './render.js'`), maps the + /// consumer-visible name (`render`) to the actual export name in the + /// origin module (`default`). Used by every `perry_fn___` + /// symbol-construction site so the suffix matches what the origin + /// module actually emits. Absent entries (the common case) mean the + /// name in origin matches the consumer's imported name; callers should + /// treat a missing entry as identity. Without this, `import { Box } + /// from "ink"` lowered to `perry_fn___Box` but the + /// origin module emitted `perry_fn___default` (Box.js + /// has `const Box = ...; export default Box`), and the linker failed + /// with `Undefined symbols: _perry_fn_..._Box`. + pub import_function_origin_names: std::collections::HashMap, /// Issue #680: per-namespace member resolution. Keyed by /// `(namespace_local_name, member_name)` → `source_prefix`. Used by /// the namespace-member access lowering paths in `expr.rs` and @@ -384,6 +397,10 @@ pub(crate) struct CrossModuleCtx { pub local_async_funcs: std::collections::HashSet, pub type_aliases: std::collections::HashMap, pub imported_func_param_counts: std::collections::HashMap, + /// Issue #678: see `CompileOptions::import_function_origin_names`. + /// Cloned from the same field so codegen helpers reachable via + /// `CrossModuleCtx` can resolve the origin name without an extra arg. + pub import_function_origin_names: std::collections::HashMap, /// Issue #608 — imported function names whose source-side signature /// has a trailing `...rest` parameter. Used by the cross-module call /// site in `lower_call.rs` to pack trailing args into a rest array. @@ -1183,6 +1200,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> local_async_funcs, type_aliases: opts.type_aliases, imported_func_param_counts: opts.imported_func_param_counts, + import_function_origin_names: opts.import_function_origin_names.clone(), imported_func_has_rest: opts.imported_func_has_rest, imported_func_return_types: opts.imported_func_return_types, method_param_counts, @@ -2716,7 +2734,15 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> llmod.add_internal_constant(&global_name, "{ ptr, i32, i32 }", &init); continue; } - let target_name = format!("perry_fn_{}__{}", source_prefix, name); + // Issue #678: when a re-export rename routes this name to an + // origin export with a different suffix (`export { default as + // render }`), call into the origin's real symbol — `perry_fn_< + // src>__default`, not `perry_fn___render`. The local + // wrapper still uses the consumer-visible name so this + // module's own callers can find it. + let origin_suffix = + crate::expr::import_origin_suffix(&cross_module.import_function_origin_names, name); + let target_name = format!("perry_fn_{}__{}", source_prefix, origin_suffix); // Look up the param count from the import metadata. Fall back // to 0 if missing — emits a no-arg wrapper, which is wrong // for nonzero-arity functions but won't break compilation. @@ -3146,6 +3172,7 @@ fn compile_function( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures: HashMap::new(), current_closure_ptr: None, enums, @@ -3523,6 +3550,7 @@ fn compile_closure( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures, current_closure_ptr: Some("%this_closure".to_string()), enums, @@ -3758,6 +3786,7 @@ fn compile_method( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures: HashMap::new(), current_closure_ptr: None, enums, @@ -4231,6 +4260,7 @@ fn compile_module_entry( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures: HashMap::new(), current_closure_ptr: None, enums, @@ -4585,6 +4615,7 @@ fn compile_module_entry( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures: HashMap::new(), current_closure_ptr: None, enums, @@ -5326,6 +5357,7 @@ fn compile_static_method( methods, module_globals, import_function_prefixes, + import_function_origin_names: &cross_module.import_function_origin_names, closure_captures: HashMap::new(), current_closure_ptr: None, enums, diff --git a/crates/perry-codegen/src/expr.rs b/crates/perry-codegen/src/expr.rs index abcda626..1ec7366f 100644 --- a/crates/perry-codegen/src/expr.rs +++ b/crates/perry-codegen/src/expr.rs @@ -48,6 +48,24 @@ pub(crate) fn nanbox_bigint_inline(blk: &mut LlBlock, ptr_i64: &str) -> String { blk.bitcast_i64_to_double(&tagged) } +/// Issue #678: resolve the actual symbol-suffix for an imported name. +/// +/// Re-export renames like `export { default as render } from './render.js'` +/// mean the consumer sees `render` while the origin module emits +/// `perry_fn___default`. The `import_function_origin_names` map +/// records this rename so every `perry_fn___` construction +/// site can pick the right suffix instead of the consumer-visible name. +/// +/// Returns the override when present, falling back to `name` (the common +/// case where origin name == consumer name). Used by every codegen +/// path that builds a `perry_fn___` extern symbol. +pub(crate) fn import_origin_suffix<'a>( + origin_names: &'a std::collections::HashMap, + name: &'a str, +) -> &'a str { + origin_names.get(name).map(String::as_str).unwrap_or(name) +} + /// If `callee` is a `new`-target whose class name is statically /// known, return that name. Used by the `Expr::NewDynamic` lowering /// to reroute statically-resolvable shapes to the regular `lower_new` @@ -222,6 +240,15 @@ pub(crate) struct FnCtx<'a> { /// `ExternFuncRef` lowering in `lower_call` to generate scoped /// cross-module calls. pub import_function_prefixes: &'a std::collections::HashMap, + /// Issue #678: Imported function name → original export name in the + /// origin module. Set when the import traverses a re-export rename + /// (`export { default as render } from './render.js'`). Looked up at + /// every `perry_fn___` construction site to + /// pick the right suffix. Absent entries (the common case) mean the + /// origin name matches the consumer's imported name; callers should + /// treat a missing entry as identity by calling + /// `import_origin_suffix(import_function_origin_names, name)`. + pub import_function_origin_names: &'a std::collections::HashMap, /// Closure capture map: when lowering inside a closure body, this /// holds `LocalId → capture_index`. `LocalGet`/`LocalSet`/`Update` /// of an id in this map routes through the runtime @@ -3663,12 +3690,16 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // body. The body only runs later when the consumer // actually calls `HashMap.keySet(self)`, by which time // both modules have finished `__init`. + // Issue #678: re-export renames mean the suffix in the + // origin module differs from the consumer-visible name. + let origin_suffix = + import_origin_suffix(ctx.import_function_origin_names, property); if ctx.imported_vars.contains(property) { - let getter = format!("perry_fn_{}__{}", source_prefix, property); + let getter = format!("perry_fn_{}__{}", source_prefix, origin_suffix); ctx.pending_declares.push((getter.clone(), DOUBLE, vec![])); return Ok(ctx.block().call(DOUBLE, &getter, &[])); } - let target_name = format!("perry_fn_{}__{}", source_prefix, property); + let target_name = format!("perry_fn_{}__{}", source_prefix, origin_suffix); let wrap_name = format!("__perry_wrap_{}", target_name); let param_count = ctx .imported_func_param_counts @@ -3699,7 +3730,11 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // object stored in the module's export global. if let Expr::ExternFuncRef { name, .. } = object.as_ref() { if let Some(source_prefix) = ctx.import_function_prefixes.get(name).cloned() { - let getter = format!("perry_fn_{}__{}", source_prefix, name); + // Issue #678: re-export renames mean the suffix in the + // origin module differs from the consumer-visible name. + let origin_suffix = + import_origin_suffix(ctx.import_function_origin_names, name); + let getter = format!("perry_fn_{}__{}", source_prefix, origin_suffix); ctx.pending_declares.push((getter.clone(), DOUBLE, vec![])); let obj_val = ctx.block().call(DOUBLE, &getter, &[]); // Now do property access on the actual object. @@ -6060,7 +6095,11 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if ctx.namespace_imports.contains(class_name) { if let Some(source_prefix) = ctx.import_function_prefixes.get(method_name).cloned() { - let fn_name = format!("perry_fn_{}__{}", source_prefix, method_name); + // Issue #678: namespace member resolved through a re-export + // rename uses the origin name as the symbol suffix. + let origin_suffix = + import_origin_suffix(ctx.import_function_origin_names, method_name); + let fn_name = format!("perry_fn_{}__{}", source_prefix, origin_suffix); let mut lowered: Vec = Vec::with_capacity(args.len()); for a in args { lowered.push(lower_expr(ctx, a)?); @@ -11164,12 +11203,15 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return Ok(double_literal(f64::from_bits(bits))); } if let Some(source_prefix) = ctx.import_function_prefixes.get(name).cloned() { + // Issue #678: re-export renames mean the origin's symbol uses + // the *origin* name as the suffix, not the consumer-visible one. + let origin_suffix = import_origin_suffix(ctx.import_function_origin_names, name); // Imported VARIABLES (exported consts/lets) need to be // called through their getter to fetch the value, not // wrapped as closures. Without this, `let v = HONE_VERSION` // creates a closure wrapper instead of the actual string. if ctx.imported_vars.contains(name) { - let fname = format!("perry_fn_{}__{}", source_prefix, name); + let fname = format!("perry_fn_{}__{}", source_prefix, origin_suffix); ctx.pending_declares.push((fname.clone(), DOUBLE, vec![])); return Ok(ctx.block().call(DOUBLE, &fname, &[])); } @@ -11200,7 +11242,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `__perry_extern_closure___` global is no // longer referenced and link-time DCE strips it. // Refs #645 deeper followup / #488 drizzle-sqlite. - let target_name = format!("perry_fn_{}__{}", source_prefix, name); + let target_name = format!("perry_fn_{}__{}", source_prefix, origin_suffix); let wrap_name = format!("__perry_wrap_{}", target_name); // Declare the source's wrapper so LLVM accepts the // `@` reference. Signature mirrors the diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index 57da47b2..b318a89e 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -560,7 +560,14 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R .cloned() .or_else(|| ctx.import_function_prefixes.get(property).cloned()) { - let symbol = format!("perry_fn_{}__{}", source_prefix, property); + // Issue #678: re-exported names (e.g. `export { default as + // render }`) emit `perry_fn___default` in the origin — + // resolve the actual origin suffix before forming the symbol. + let origin_suffix = crate::expr::import_origin_suffix( + ctx.import_function_origin_names, + property, + ); + let symbol = format!("perry_fn_{}__{}", source_prefix, origin_suffix); if ctx.imported_vars.contains(property) { // Var-shaped export: fetch closure via zero-arg // getter, then closure-call with the user args. @@ -1077,7 +1084,14 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R return Ok(ctx.block().call(DOUBLE, name, &arg_slices)); } }; - let fname = format!("perry_fn_{}__{}", source_prefix, name); + // Issue #678: re-export rename (`export { default as render } from + // './render.js'`) means the origin module emits the symbol under + // the *origin* name (`default`), not the consumer-visible name + // (`render`). Look up the actual origin suffix before forming the + // extern. + let origin_suffix = + crate::expr::import_origin_suffix(ctx.import_function_origin_names, name); + let fname = format!("perry_fn_{}__{}", source_prefix, origin_suffix); // Issue #493 followup: when the imported binding is a VARIABLE // holding a closure value (e.g. `var mergePath = (b, s, ...r) => …` // exported from another module), `perry_fn___` is the diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index b1e90801..ef2528dc 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -2250,6 +2250,16 @@ pub fn run_with_parse_cache( // Build a map of all exports from all modules: module_path -> HashMap // This is used for namespace imports (`import * as X from './module'`) to resolve all exports let mut all_module_exports: BTreeMap> = BTreeMap::new(); + // Issue #678: parallel map carrying the *origin name* alongside the + // origin path. When `ink/build/index.js` says `export { default as + // render } from './render.js'`, `all_module_exports[ink_path]["render"] + // = render_js_path` and `all_module_export_origin_names[ink_path] + // ["render"] = "default"`. The codegen consumer of an import that + // resolves through this chain forms `perry_fn___default` + // instead of `perry_fn___render` — without it the linker + // fails on the missing `_perry_fn___render` symbol. + let mut all_module_export_origin_names: BTreeMap> = + BTreeMap::new(); for (path, hir_module) in &ctx.native_modules { let path_str = path.to_string_lossy().to_string(); let exports = all_module_exports @@ -2288,7 +2298,13 @@ pub fn run_with_parse_cache( // Propagate exports through ExportAll and ReExport chains loop { - let mut new_export_entries: Vec<(String, String, String)> = Vec::new(); // (module_path, export_name, origin_path) + // (module_path, export_name, origin_path, origin_name_in_origin). + // The fourth tuple element drives Issue #678's per-export + // origin-name map: when a re-export renames a name across a hop + // (`export { default as render } from './render.js'`), the + // consumer must use the *origin* name (`default`) as the symbol + // suffix, not the consumer-visible one (`render`). + let mut new_export_entries: Vec<(String, String, String, String)> = Vec::new(); for (path, hir_module) in &ctx.native_modules { let path_str = path.to_string_lossy().to_string(); for export in &hir_module.exports { @@ -2309,10 +2325,23 @@ pub fn run_with_parse_cache( .map(|e| e.contains_key(name)) .unwrap_or(false); if !already_exists { + // `export * from "src"` doesn't + // rename — origin_name == export_name. + // But if `src` itself remapped this + // name (e.g. `export { default as + // foo } from './x.js'`), propagate + // the deeper origin name across this + // transitive hop. + let deep_origin_name = all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(name)) + .cloned() + .unwrap_or_else(|| name.clone()); new_export_entries.push(( path_str.clone(), name.clone(), origin.clone(), + deep_origin_name, )); } } @@ -2340,10 +2369,23 @@ pub fn run_with_parse_cache( .map(|v| v == origin) .unwrap_or(false); if !already_correct { + // Walk one more hop: if `src` itself + // remapped `imported` to a deeper + // origin name (`src` did its own + // `export { default as imported } + // from "..."`), record THAT deeper + // name so the consumer's symbol-suffix + // resolution skips both hops. + let deep_origin_name = all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(imported)) + .cloned() + .unwrap_or_else(|| imported.clone()); new_export_entries.push(( path_str.clone(), exported.clone(), origin.clone(), + deep_origin_name, )); } } @@ -2385,10 +2427,19 @@ pub fn run_with_parse_cache( .map(|v| v == origin) .unwrap_or(false); if !already_correct { + let deep_origin_name = + all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(&imported_name)) + .cloned() + .unwrap_or_else(|| { + imported_name.clone() + }); new_export_entries.push(( path_str.clone(), exported.clone(), origin.clone(), + deep_origin_name, )); } } @@ -2405,11 +2456,22 @@ pub fn run_with_parse_cache( if new_export_entries.is_empty() { break; } - for (module_path, name, origin) in new_export_entries { + for (module_path, name, origin, origin_name) in new_export_entries { all_module_exports - .entry(module_path) + .entry(module_path.clone()) .or_insert_with(BTreeMap::new) - .insert(name, origin); + .insert(name.clone(), origin); + // Only record the origin-name entry when it actually differs + // from the export name (the common identity case is implicit — + // the codegen helper falls back to the imported name when no + // entry is present). This keeps the map sparse and easy to + // reason about. + if origin_name != name { + all_module_export_origin_names + .entry(module_path) + .or_insert_with(BTreeMap::new) + .insert(name, origin_name); + } } } @@ -3273,6 +3335,17 @@ pub fn run_with_parse_cache( // to generate `perry_fn___`. let mut import_function_prefixes: std::collections::HashMap = std::collections::HashMap::new(); + // Issue #678: parallel to `import_function_prefixes`. When the + // import traverses a re-export rename (`export { default as render + // } from './render.js'`), the consumer sees `render` but the + // origin module emits the symbol with its own export name + // (`default`). This map captures the consumer-name → origin-name + // override so every `perry_fn___` construction site + // can pick the right suffix. Absent entries (the common case) + // mean no rename — the consumer name is the origin name. + let mut import_function_origin_names: + std::collections::HashMap = + std::collections::HashMap::new(); // Issue #680: per-namespace member resolution. Disambiguates // `random.make` vs `tracer.make` when multiple namespaces // export the same member name. Keyed by `(namespace_local, @@ -3391,6 +3464,21 @@ pub fn run_with_parse_cache( compute_module_prefix(origin_path, &ctx.project_root); import_function_prefixes .insert(export_name.clone(), origin_prefix.clone()); + // Issue #678: surface origin-name overrides + // for namespace-imported members too. A + // member reached via a re-export rename + // (`export { default as foo }`) needs the + // codegen to call `perry_fn___default` + // when the consumer writes `ns.foo()`. + if let Some(origin_name) = all_module_export_origin_names + .get(&resolved_path_str) + .and_then(|m| m.get(export_name)) + { + if origin_name != export_name { + import_function_origin_names + .insert(export_name.clone(), origin_name.clone()); + } + } // Issue #680: also register under the // per-namespace key so `random.make` and // `tracer.make` can be disambiguated. @@ -3541,6 +3629,17 @@ pub fn run_with_parse_cache( compute_module_prefix(origin_path, &ctx.project_root); import_function_prefixes .insert(export_name.clone(), origin_prefix.clone()); + // Issue #678: surface origin-name overrides + // for the NamespaceReExport branch too. + if let Some(origin_name) = all_module_export_origin_names + .get(&ns_target_str) + .and_then(|m| m.get(export_name)) + { + if origin_name != export_name { + import_function_origin_names + .insert(export_name.clone(), origin_name.clone()); + } + } let key = (origin_path.clone(), export_name.clone()); if let Some(¶m_count) = exported_func_param_counts.get(&key) @@ -3660,6 +3759,30 @@ pub fn run_with_parse_cache( .insert(local_name.clone(), effective_prefix.clone()); } + // Issue #678: if the import chain renames through a + // re-export (`export { default as render } from + // './render.js'`), the symbol in the origin module + // is `perry_fn___default`, not + // `perry_fn___render`. Surface the deeper + // origin name via `import_function_origin_names` so + // the codegen can pick the right suffix when forming + // the extern symbol. The map is sparse — entries are + // only inserted when origin_name != exported_name. + let resolved_origin_name = all_module_export_origin_names + .get(&resolved_path_str) + .and_then(|m| m.get(&exported_name)) + .cloned(); + if let Some(ref origin_name) = resolved_origin_name { + if origin_name != &exported_name { + import_function_origin_names + .insert(exported_name.clone(), origin_name.clone()); + if local_name != exported_name { + import_function_origin_names + .insert(local_name.clone(), origin_name.clone()); + } + } + } + // Imported variables (not functions) — ExternFuncRef-as-value // should call the getter, not wrap as closure. Look up by the // ORIGIN path (where the `Let X = ...` actually lives), not @@ -3670,7 +3793,23 @@ pub fn run_with_parse_cache( // return value AS the call result — pgTable("users", cols) // returned the closure handle (typeof === "function") with no // pgTable body actually invoked. - if exported_var_names.contains(&origin_key) { + // + // Issue #678 followup: when a re-export rename routes + // the import through `export default `, the origin + // module's `exported_objects` carries the synthetic + // "default" entry (the only thing exported at that + // shape) — not the consumer-visible name. Probe both + // keys so the var-vs-function classification fires + // even when re-export renaming is in play. + let origin_key_under_origin_name = resolved_origin_name + .as_ref() + .map(|n| (origin_path.clone(), n.clone())); + if exported_var_names.contains(&origin_key) + || origin_key_under_origin_name + .as_ref() + .map(|k| exported_var_names.contains(k)) + .unwrap_or(false) + { imported_vars.insert(exported_name.clone()); if local_name != exported_name { imported_vars.insert(local_name.clone()); @@ -4410,6 +4549,7 @@ pub fn run_with_parse_cache( is_entry_module: is_entry, non_entry_module_prefixes, import_function_prefixes, + import_function_origin_names, namespace_member_prefixes, emit_ir_only: bitcode_link, namespace_imports, diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index 4562201d..139cb667 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -255,6 +255,22 @@ pub fn compute_object_cache_key( h.field("import_fn_prefixes", &s); } + // Issue #678: include the origin-name overrides in the cache key. + // Without this, two builds where the same module imports the same + // names but with different re-export shapes (e.g. a downstream + // package renamed its barrel exports) would share a cached `.o` and + // silently emit the stale symbol suffix. + { + let mut v: Vec<(&String, &String)> = opts.import_function_origin_names.iter().collect(); + v.sort_by(|a, b| a.0.cmp(b.0)); + let s: String = v + .iter() + .map(|(k, vv)| format!("{}={}", k, vv)) + .collect::>() + .join(","); + h.field("import_fn_origin_names", &s); + } + // Imported classes — sort by name. Serialize every field that codegen // reads so a changed constructor arity or new method on a re-exported // class invalidates consumers. @@ -553,6 +569,7 @@ mod object_cache_tests { is_entry_module: false, non_entry_module_prefixes: Vec::new(), import_function_prefixes: std::collections::HashMap::new(), + import_function_origin_names: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), emit_ir_only: false, namespace_imports: Vec::new(), diff --git a/test-files/fixtures/issue_678_pkg/index.ts b/test-files/fixtures/issue_678_pkg/index.ts new file mode 100644 index 00000000..02556cb4 --- /dev/null +++ b/test-files/fixtures/issue_678_pkg/index.ts @@ -0,0 +1,9 @@ +// Re-exporter barrel for issue #678. The `default as renamed` shape is +// the canonical npm-barrel pattern (ink does `export { default as Box } +// from './components/Box.js'`, etc.). Pre-fix the consumer's codegen +// formed `perry_fn___renamedArrow` and `perry_fn___renamedPlain` +// even though the origin module emits the symbol under `default` / +// `plain`. + +export { default as renamedArrow } from "./sub/inner.ts"; +export { plain as renamedPlain } from "./sub/inner.ts"; diff --git a/test-files/fixtures/issue_678_pkg/sub/inner.ts b/test-files/fixtures/issue_678_pkg/sub/inner.ts new file mode 100644 index 00000000..9a3f29ba --- /dev/null +++ b/test-files/fixtures/issue_678_pkg/sub/inner.ts @@ -0,0 +1,26 @@ +// Origin module for the issue #678 regression fixture. +// +// Three export shapes the symbol-suffix resolver has to round-trip +// through `export { default as X } from "./inner.ts"`: +// +// 1. `const arrow = (x) => x + 1; export default arrow;` +// Origin emits `perry_fn___default` as the zero-arg getter; +// consumer must classify the import as `imported_vars` so the call +// site goes through `js_closure_callN` after fetching the value. +// +// 2. `export default function namedFn(x) { return x * 2; }` +// Origin emits `perry_fn___default` AND `perry_fn___namedFn`; +// consumer must call `__default` (not `__namedFn`) per ES spec when +// the importing module names the binding via the re-export rename. +// +// 3. `export function plain(x) { return x + 100; }` +// Plain function export — sanity check that non-rename imports still +// resolve to `perry_fn___plain` correctly with the new +// origin-name lookup in place. + +const arrow = (x: number): number => x + 1; +export default arrow; + +export function plain(x: number): number { + return x + 100; +} diff --git a/test-files/test_issue_678_reexport_default.ts b/test-files/test_issue_678_reexport_default.ts new file mode 100644 index 00000000..14b24a08 --- /dev/null +++ b/test-files/test_issue_678_reexport_default.ts @@ -0,0 +1,32 @@ +// Issue #678: consumer imports through a `export { default as X } from +// "./inner.ts"` re-export rename. Pre-fix the codegen formed +// `perry_fn___renamedArrow` even though `inner.ts` emits the +// symbol under `default`, and the linker failed with `Undefined +// symbols: _perry_fn_..._renamedArrow`. +// +// The fix tracks the origin export name across the re-export chain so +// every `perry_fn___` construction site picks the correct +// suffix; this file exercises the call-site, the value-as-arg site, and +// the plain non-rename path. + +import { renamedArrow, renamedPlain } from "./fixtures/issue_678_pkg/index.ts"; + +// Call site through the rename — origin export is `default` (a const +// arrow), consumer wrote `renamedArrow`. +console.log("arrow-call:", renamedArrow(41)); + +// Value-as-arg shape: hand the imported binding to another function so +// the codegen takes the `js_closure_alloc_singleton(@__perry_wrap_*)` +// path. Without the origin-name plumb-through, the wrapper symbol +// referenced here would also point at `__renamedArrow` and link-fail. +function apply(fn: (n: number) => number, value: number): number { + return fn(value); +} +console.log("arrow-as-value:", apply(renamedArrow, 99)); + +// Sanity check the non-default rename path: `export { plain as +// renamedPlain }` still resolves to `perry_fn___plain` rather +// than `__renamedPlain`. +console.log("plain-call:", renamedPlain(5)); + +console.log("done");