Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/10197-namespace-spread-call-var.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- `ns.member(...spread)` on a `const`-bound rest-parameter export (`export const mergeAll = (...ctxs) => …`, e.g. effect's `Context.mergeAll(...contexts)` inside `Layer.mergeAll`) now calls the closure; the spread fast path treated the export's value getter as the function body and evaluated to the function instead (#10197).
19 changes: 18 additions & 1 deletion crates/perry-codegen/src/expr/call_spread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
} else {
ctx.imported_func_has_rest.contains(property)
};
if ctx.namespace_imports.contains(ns_name) && has_rest && declared_count == 1 {
// #10197: a `let`/`const`-bound export (`export const
// mergeAll = (...ctxs) => …`) has no callable
// `perry_fn_<src>__<name>` — that symbol is its zero-arg
// VALUE getter — so bundling the spread into it returns
// the closure instead of calling it (effect's
// `Context.mergeAll(...contexts)` in `Layer.mergeAll`).
// Mirror `try_lower_namespace_member_call`: vars take the
// closure-callee path below, which reads the value and
// applies the spread through
// `js_closure_call_apply_with_spread`.
let is_var = ctx
.imported_vars
.contains(&crate::namespace_member_var_key(ns_name, property));
if ctx.namespace_imports.contains(ns_name)
&& has_rest
&& declared_count == 1
&& !is_var
{
let source_prefix_opt = ctx
.namespace_member_prefixes
.get(&(ns_name.clone(), property.clone()))
Expand Down
2 changes: 2 additions & 0 deletions crates/perry/tests/source_graph_export_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,3 +956,5 @@ fn mixed_type_and_value_specifier_import_keeps_runtime_edge() {
}
#[path = "source_graph_export_regressions/issue_10160.rs"]
mod issue_10160;
#[path = "source_graph_export_regressions/issue_10197.rs"]
mod issue_10197;
41 changes: 41 additions & 0 deletions crates/perry/tests/source_graph_export_regressions/issue_10197.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! `ns.member(...spread)` on a `const`-bound rest-parameter export must call
//! the closure, not evaluate to it (#10197).

use super::{compile_and_run, write};

fn lib(dir: &std::path::Path) {
write(
dir,
"lib.ts",
"export const count = (...xs: any[]) => xs.length\n\
export const mergeAll = (...ctxs: any[]) => {\n\
\x20 const m = new Map()\n\
\x20 for (let i = 0; i < ctxs.length; i++) ctxs[i].mapUnsafe.forEach((v: any, k: any) => m.set(k, v))\n\
\x20 return { mapUnsafe: m }\n\
}\n\
export function countFn(...xs: any[]) { return xs.length }\n\
export const mk = (k: string) => ({ mapUnsafe: new Map([[k, k.length]]) })\n\
export const fill = (n: number, f: (i: number) => any) => { const out = new Array(n); for (let i = 0; i < n; i++) out[i] = f(i); return out }\n",
);
}

#[test]
fn namespace_member_spread_call_invokes_const_rest_exports() {
let dir = tempfile::tempdir().unwrap();
lib(dir.path());
write(
dir.path(),
"main.ts",
"import * as Lib from \"./lib\"\n\
import { fill, mk } from \"./lib\"\n\
const other = fill(3, (i) => mk(\"k\".repeat(i + 1)))\n\
console.log(Lib.count(...other), Lib.count(0, ...other), Lib.countFn(...other), Lib.countFn(0, ...other))\n\
console.log(Lib.mergeAll(...other).mapUnsafe.size, (Lib as any)[\"mergeAll\"](...other).mapUnsafe.size)\n\
const inner = (context: any) => Lib.mergeAll(...(context as any))\n\
console.log(inner(other).mapUnsafe.size, Lib.mk(...([\"zz\"] as [string])).mapUnsafe.size)\n",
);
assert_eq!(
compile_and_run(dir.path(), "main.ts"),
"3 4 3 4\n3 3\n3 1\n"
);
}
Loading