Skip to content

fix(codegen): #248 ArrayPushSpread + V8 interop codegen arms#251

Merged
proggeramlug merged 3 commits into
mainfrom
worktree-issue-248-array-push-spread
Apr 28, 2026
Merged

fix(codegen): #248 ArrayPushSpread + V8 interop codegen arms#251
proggeramlug merged 3 commits into
mainfrom
worktree-issue-248-array-push-spread

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

@proggeramlug proggeramlug commented Apr 28, 2026

Closes #248 — both halves.

Phase 1 (commit 8e512d8): ArrayPushSpread

arr.push(...src) was rejected with expression ArrayPushSpread not yet supported. HIR has lowered the variant since v0.5.x (crates/perry-hir/src/lower/expr_call.rs:2077); only the codegen arm in crates/perry-codegen/src/expr.rs was missing. The WASM backend's emit-side handled it (crates/perry-codegen-wasm/src/emit.rs:5441) and analysis helpers all knew about the variant, but expr.rs had no match arm so codegen fell through to the catch-all bail at line 8434.

Single new arm mirroring Expr::ArrayPush at line 3016 — same three receiver storage cases (LocalGet, boxed-captured, plain), same realloc-aware writeback. No new runtime helper needed: js_array_concat(dst, src) already existed at crates/perry-runtime/src/array.rs:1011, comment-as-spec'd "reserved for the internal push-spread desugaring path".

Phase 2 (commit ae1fd6c): V8 / perry-jsruntime interop codegen arms

Adds 8 codegen arms in crates/perry-codegen/src/expr.rs for the Js* HIR family that perry-hir/src/js_transform.rs::transform_js_imports produces whenever a .ts entry imports from a .js module the resolver classifies as JS-runtime-loaded (crates/perry/src/commands/compile/collect_modules.rs:73 — extension-driven; the user's bun i @codehz/pipeline repro pulls index.js through this path).

Pre-fix the LLVM backend bailed for JsLoadModule, JsGetExport, JsCallFunction, JsCallMethod, JsGetProperty, JsSetProperty, JsNew, JsNewFromHandle. Only WASM and JS backends had emit-side handling.

The new arms call into the existing perry-jsruntime/src/interop.rs FFI surface — all eight FFIs were already declared in runtime_decls.rs except js_call_method (added here). New shared helper lower_js_args_array marshals already-lowered NaN-boxed args into a stack alloca'd [N x double] via the issue-#167 alloca_entry_array pattern.

Module handle representation: V8 module ids are u64; codegen returns them as f64 via bitcast_i64_to_double to fit lower_expr's return-type contract, then consumers bitcast back to i64 before the FFI call. JS value handles are NaN-boxed f64 with V8-handle tag 0x7FFB — handled internally by perry-jsruntime's v8_to_native / native_to_v8 helpers.

Runtime bootstrap: new needs_js_runtime: bool field on CrossModuleCtx, threaded from CompileOptions, wired into compile_module_entry so the entry main's prelude calls js_runtime_init() between js_gc_init and user code. Without this, every js_load_module site bailed at runtime with [js_load_module] no JS runtime state!.

JsCreateCallback deliberately deferred to Phase 2B follow-up: the runtime FFI js_create_callback expects func_ptr to have signature (closure_env: i64, args_ptr: *const f64, args_len: i64) -> f64 (see native_callback_trampoline at crates/perry-jsruntime/src/interop.rs:993), but Perry closure bodies have (closure_ptr, arg0, arg1, ...) per arity — no direct call-compatible mapping. Wiring this needs either codegen-emitted per-arity adapter thunks (one _perry_jscb_thunk_<n> per param_count), or a runtime-side closure-array dispatcher. For now the arm bails with a clear message so users see exactly what's blocked.

Test plan

Phase 1:

  • cargo build --release -p perry-runtime -p perry-stdlib -p perry — clean
  • Original [1,2].push(...[3,4,5]) repro: pre-fix bailed; post-fix compiles to 700K binary, prints 5 matching Node
  • New regression test test-files/test_issue_248_array_push_spread.ts (10 cases) — byte-for-byte against node --experimental-strip-types

Phase 2:

  • cargo build --release -p perry-runtime -p perry-stdlib -p perry-jsruntime -p perry — clean
  • cargo test --release -p perry-codegen --lib — 22/0
  • User's exact pipeline() repro from node_modules/@codehz/pipeline/index.js at /tmp/issue248/test.ts: pre-fix bailed with JsCallFunction not yet supported; post-fix compiles + links + runs, exit 0
  • Method-call shape (gp.run({...})) at /tmp/issue248/test_method_no_cb.ts: compiles + links (39MB binary, pulls in V8) + runs, exit 0
  • New regression test test-files/test_issue_248_phase2_js_interop.ts + fixture test-files/fixtures/issue_248_jsmod.js — verifies compile + link + clean exit

Common:

  • Gap tests 25/28 = baseline (the 3 failing — array_methods / console_methods / typed_arrays — are pre-existing categorical/CI gaps unrelated to this change)

Known limitations / follow-ups

  • JsCreateCallback (Phase 2B): closures passed to JS-imported functions still bail. The user's gameLoop.use((ctx) => {...}) shape requires the closure-marshaling work described above.
  • End-to-end V8 execution: in some shapes the perry-runtime weak stubs at crates/perry-runtime/src/closure.rs:651-659 win the link-order race against perry-jsruntime's strong impls (Mach-O first-wins behavior), so simple tests like pipeline()-then-typeof see the stub return value (0.0 NaN-boxed → typeof === "number"). Method calls (no stub) pull V8 in correctly. This is a pre-existing tangle in the link orchestration that's separate from Unexpected error: JsCallFunction not yet supported in js source #248's codegen scope.
  • typeof on V8 handles: returns "number" because Perry's typeof doesn't recognize the V8-handle tag 0x7FFB. Separate issue.

….366)

`arr.push(...src)` was rejected with "expression ArrayPushSpread not
yet supported". HIR has lowered the variant for some time; only the
codegen arm in crates/perry-codegen/src/expr.rs was missing.

Mirrors the existing Expr::ArrayPush arm at line 3016 — same three
receiver-storage cases (LocalGet, boxed-captured, plain), same
realloc-aware writeback. Reuses the existing js_array_concat runtime
helper (already declared in runtime_decls.rs:189; comment-as-spec'd
"reserved for the internal push-spread desugaring path"). Set sources
work transparently via the SET_REGISTRY check inside js_array_concat.

New regression test test_issue_248_array_push_spread.ts covers 10
cases (number/string/object arrays, empty src/dst, array-literal
spread, chained push-spread, post-spread .indexOf/.length, push-spread
inside a loop that grows past initial 16-cap, mixed push+push-spread)
— all byte-for-byte against `node --experimental-strip-types`.

Phase 2 of #248 (JsCallFunction/JsCallMethod/JsLoadModule family for
the LLVM backend's JS-runtime interop, surfacing when a .ts entry
imports from a node_modules/<pkg>/index.js) is intentionally separate.
…ms (v0.5.368)

Adds 8 codegen arms in crates/perry-codegen/src/expr.rs for the Js*
HIR family that perry-hir/src/js_transform.rs::transform_js_imports
produces whenever a `.ts` entry imports from a `.js` module the
resolver classified as JS-runtime-loaded. Pre-fix the LLVM backend
bailed at the catch-all `bail!("Phase 2: expression {} not yet
supported")` for JsLoadModule, JsGetExport, JsCallFunction,
JsCallMethod, JsGetProperty, JsSetProperty, JsNew, JsNewFromHandle.

The new arms call into the existing perry-jsruntime/src/interop.rs
FFI surface; all eight FFIs were already declared in runtime_decls.rs
except js_call_method (added here). New shared helper
lower_js_args_array marshals already-lowered NaN-boxed args into a
stack alloca'd `[N x double]` via the issue-#167 alloca_entry_array
pattern; empty input returns ("null", "0") for the FFI's null fallback.

Module handle representation: V8 module ids are u64; codegen returns
them as f64 via bitcast_i64_to_double to fit lower_expr's return-type
contract, then consumers bitcast back to i64 before the FFI call. JS
value handles are NaN-boxed f64 with V8-handle tag 0x7FFB — handled
internally by perry-jsruntime's v8_to_native / native_to_v8 helpers.

Runtime bootstrap: new `needs_js_runtime: bool` field on
CrossModuleCtx, threaded from CompileOptions, wired into
compile_module_entry so the entry main's prelude calls js_runtime_init()
between js_gc_init and user code. Without this, every js_load_module
site bailed at runtime with `[js_load_module] no JS runtime state!`.

JsCreateCallback deliberately deferred to Phase 2B: the runtime FFI
js_create_callback expects func_ptr to have signature
(closure_env: i64, args_ptr: *const f64, args_len: i64) -> f64, but
Perry closure bodies have (closure_ptr, arg0, arg1, ...) per arity —
no direct call-compatible mapping. Wiring this needs either
codegen-emitted per-arity adapter thunks or a runtime-side
js_closure_call_array dispatcher. For now the arm bails with a clear
message so users see exactly what's blocked.

New regression test test-files/test_issue_248_phase2_js_interop.ts +
fixture test-files/fixtures/issue_248_jsmod.js exercises JsLoadModule
+ JsCallFunction. The user's exact pipeline() repro from
/tmp/issue248/test.ts now compiles + links + runs.
@proggeramlug proggeramlug changed the title fix(codegen): #248 ArrayPushSpread codegen arm for LLVM backend fix(codegen): #248 ArrayPushSpread + V8 interop codegen arms Apr 28, 2026
…ray-push-spread

# Conflicts:
#	CLAUDE.md
#	Cargo.lock
#	Cargo.toml
@proggeramlug proggeramlug merged commit 5384707 into main Apr 28, 2026
1 check passed
@proggeramlug proggeramlug deleted the worktree-issue-248-array-push-spread branch April 28, 2026 17:17
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.

Unexpected error: JsCallFunction not yet supported in js source

1 participant