Skip to content

fix(hir): spread call of a native crypto method must not collapse the spread - #6670

Merged
proggeramlug merged 2 commits into
mainfrom
fix/6668-spread-native-dispatch
Jul 19, 2026
Merged

fix(hir): spread call of a native crypto method must not collapse the spread#6670
proggeramlug merged 2 commits into
mainfrom
fix/6668-spread-native-dispatch

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6668 — a spread call of a native crypto method
(crypto.hkdf(...args, cb)) silently no-ops: no dispatch, no throw, the
callback never fires, and the call evaluates to undefined.

Root cause

The bug is in HIR lowering, not the runtime spread bridge the issue
suspected. The crypto fast-path (lower_crypto_passthrough, reached from both
the dotted crypto.hkdf(...) form in module_static.rs and the named-import
import { hkdf } from "crypto" form in globals.rs) collapses the call into a
flat Expr::Call/Expr::NativeMethodCall whose args are the lowered AST
arguments verbatim — a spread operand (...args) is passed as the array
itself
, not expanded. Codegen's arm_crypto_hkdf_async_alg then sees
args.len() < 6 and returns undefined without ever dispatching, so the
callback never runs.

Confirmed by dumping HIR: crypto.hkdf(...args, cb) lowered to
Call { callee: PropertyGet{NativeModuleRef("crypto"), "hkdf"}, args: [<args array>, cb] }
— 2 positional args instead of 6.

The value-read form (const f = crypto.hkdf; f(...)) already works because it
dispatches through the bound-native path
(js_native_call_valuedispatch_bound_methodjs_native_call_method).

Fix

When a spread argument is present, the crypto fast-paths decline so the
generic lowering tail builds an Expr::CallSpread instead. That CallSpread
routes through js_closure_call_apply_with_spread
dispatch_bound_method("hkdf")js_native_call_method — the exact
bound-native dispatch the value-read form uses (and which the runtime crypto
dispatcher already handles for every callable export by name).

  • module_static.rs — gate the dotted-form crypto passthrough on !has_spread.
  • globals.rs — decline named-import crypto method calls that carry a spread.

Both are one-line guards; no runtime changes. Scoped to crypto (the reported
module), whose runtime dispatcher resolves every callable export by name.

Verification

test-files/test_crypto_hkdf_spread_6668.ts (new) — byte-identical to
node --experimental-strip-types across five forms:

hkdfSync-spread = 64      # sync spread, returns a value
direct = 64               # non-spread fast-path (regression guard)
dotted-interleaved = 64   # crypto.hkdf("sha256", ikm, ...mid, 64, cb)
dotted-spread = 64        # crypto.hkdf(...args, cb)
named-spread = 64         # import { hkdf }; hkdf(...args, cb)

Before the fix, dotted-spread, dotted-interleaved, and named-spread were
missing entirely (silent no-op).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed crypto.hkdf/hkdfSync behavior when invoked with spread arguments, preventing incorrect simplification.
    • Ensured spread-based HKDF calls are lowered and dispatched correctly so callbacks don’t get skipped.
  • Tests
    • Added a regression test for crypto.hkdf spread-call variants to verify deterministic synchronous output and reliable asynchronous callback execution.

… spread

`crypto.hkdf(...args, cb)` (dotted) and `hkdf(...args, cb)` (named import)
lowered through the crypto fast-path, which collapses the call into a flat
`Expr::Call` / `Expr::NativeMethodCall` whose args are the lowered AST arguments
verbatim — a spread operand is passed as the array itself, not expanded. The
codegen fast-path (`arm_crypto_hkdf_async_alg`) then saw too few positional args
and returned `undefined` without dispatching, so the callback never fired: a
silent no-op.

Gate the crypto passthrough (dotted form, `module_static.rs`) and the crypto
named-import path (`globals.rs`) on `!has_spread`. A spread call now falls
through to the generic `CallSpread` lowering, which routes through
`js_closure_call_apply_with_spread` -> `dispatch_bound_method` ->
`js_native_call_method` — the same bound-native dispatch the value-read form
(`const f = crypto.hkdf; f(...)`) already uses, and which the runtime crypto
dispatcher handles for every callable export by name.

Adds `test-files/test_crypto_hkdf_spread_6668.ts`, byte-identical to
`node --experimental-strip-types` across dotted / interleaved / named / sync
spread forms plus a non-spread regression guard.

Fixes #6668
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4411c264-e9d3-4d93-b085-6247390e6ca3

📥 Commits

Reviewing files that changed from the base of the PR and between 17e0ded and 1fb9d7d.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower/expr_call/module_static.rs
  • test-files/test_crypto_hkdf_spread_6668.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test-files/test_crypto_hkdf_spread_6668.ts
  • crates/perry-hir/src/lower/expr_call/module_static.rs

📝 Walkthrough

Walkthrough

Crypto call lowering detects spread arguments and bypasses specialized native-module and crypto passthrough paths. A regression test covers synchronous and asynchronous hkdf calls through module, dotted, named, spread, and direct invocation forms.

Changes

Crypto spread-call dispatch

Layer / File(s) Summary
Bypass specialized crypto lowering
crates/perry-hir/src/lower/expr_call/globals.rs, crates/perry-hir/src/lower/expr_call/module_static.rs
Spread arguments are detected, causing crypto native-module calls to skip per-module fast paths and crypto passthrough lowering.
Validate HKDF spread calls
test-files/test_crypto_hkdf_spread_6668.ts
Regression coverage checks synchronous spread output and records callback results across asynchronous crypto.hkdf, named hkdf, and direct invocation forms.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main fix: preventing spread calls on native crypto methods from collapsing.
Description check ✅ Passed The description covers the summary, root cause, fix, and verification, and references the issue, though it uses custom headings.
Linked Issues check ✅ Passed The change addresses #6668 by routing spread crypto calls away from the fast-path so they dispatch correctly instead of no-oping.
Out of Scope Changes check ✅ Passed The edits stay within crypto lowering and a focused regression test, with no obvious unrelated changes.
✨ 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/6668-spread-native-dispatch

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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@crates/perry-hir/src/lower/expr_call/module_static.rs`:
- Around line 972-987: Update the crypto handling in the module-static call
lowering block so any has_spread call bypasses the method_name passthrough check
and returns through the generic spread-call path. Align this guard with the
crypto behavior in globals.rs, ensuring non-passthrough methods such as sha256
and md5 do not enter the method_name match and receive the spread array as a
normal argument; preserve existing handling for non-spread calls.

In `@test-files/test_crypto_hkdf_spread_6668.ts`:
- Around line 19-20: Update the argument arrays sargs, a1, mid, and a3 used by
the crypto.hkdfSync and hkdf spread calls to be inferred as tuples, using const
assertions or explicit tuple types, so the spread operands satisfy the expected
parameter signatures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad2ce898-8594-4716-819f-87f9e5a5f7f5

📥 Commits

Reviewing files that changed from the base of the PR and between f315681 and 17e0ded.

📒 Files selected for processing (3)
  • crates/perry-hir/src/lower/expr_call/globals.rs
  • crates/perry-hir/src/lower/expr_call/module_static.rs
  • test-files/test_crypto_hkdf_spread_6668.ts

Comment thread crates/perry-hir/src/lower/expr_call/module_static.rs Outdated
Comment thread test-files/test_crypto_hkdf_spread_6668.ts Outdated
Address CodeRabbit review on #6670.

module_static.rs previously gated only `is_passthrough_method` on `!has_spread`,
so a spread call to a non-passthrough crypto method (`crypto.sha256(...args)`,
`crypto.md5(...args)`, `crypto.getRandomValues(...args)`) still fell into the
manual `match method_name` arms and had its spread array extracted as a single
argument. Bail out of the entire crypto block when `has_spread`, mirroring the
crypto guard already in globals.rs, so every crypto spread call routes through
the generic CallSpread / bound-native dispatch path.

Also make the spread arg lists in the regression test tuples (`as const`) so the
spreads satisfy the parameter signatures under `tsc` (TS2556); runtime output is
unchanged and still byte-identical to node.
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.

Spread call of a native-module method silently no-ops (js_closure_call_apply_with_spread bypasses bound-native dispatch)

1 participant