Skip to content

perf(codegen): entry resolution reaches Any-typed callback params — __commonJS-shape calls 7.4 → 4.5 ns (typed parity) - #9109

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/callee-resolution-any-params
Aug 30, 2026
Merged

perf(codegen): entry resolution reaches Any-typed callback params — __commonJS-shape calls 7.4 → 4.5 ns (typed parity)#9109
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/callee-resolution-any-params

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What

Entry resolution (#9071) reaches Any-typed callback parameters — the
esbuild __commonJS/__esm factory shape, where every module-init callback
erases to Any and paid the full js_closure_callN dispatcher per call:

  • the emission gate in emit_callee_binding_resolutions admits a PARAMETER
    whose hint is absent or Any (a binding hinted as something
    non-function stays out — its resolution could only return null);
  • the consuming arm's gate in early_branches.rs accepts a callee with a
    resolved-target map entry even when the Function hint is missing — the
    arm's runtime behavior (checked unbox, null-guarded direct diamond, full
    dispatcher fallback) was always hint-independent; the hint only selected
    who enters.

A resolution over a non-closure value (a number, undefined, a bound
function, a generator) returns null at entry and every call keeps the exact
dispatcher path.

Numbers

Single-shape 50M-call probes, quiet Linux, same-build
PERRY_CALLEE_BINDING_RESOLUTION=0 A/B:

shape off on node
Any-param callback loop (__commonJS shape) 7.4 4.5 0.5
typed-param control (#9071 path) 8.1 4.5 0.5
Any-param, conditionally called 7.3 4.6

The Any shape reaches exact parity with the typed shape. This is the
"partial until the param-hint gate lifts" band flagged in #9105 and in the
claude-code startup profile split — the cc --help re-profile is gated on
this PR landing (plus #9105 and the #9106 fix).

Semantics

Differential vs node identical: non-function/undefined through the Any
param (TypeError at the call), the param REASSIGNED mid-body (collector
excludes; the new value is observed), bound functions and ordinary functions
(this === undefined) through the Any param, arity-mismatch and rest
arrows, capture mutation between calls, generator functions. Kill-switch build
output-identical; the #9105 corpora re-run clean (the one diff is the
documented pre-existing no-TDZ-for-globals TypeError vs ReferenceError
line, gate-independent).

Testing

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of direct calls through untyped or broadly typed callback parameters.
    • Enhanced compatibility with bundled module factory patterns.
    • Improved closure call performance by using faster resolution paths when safe.
    • Preserved existing runtime behavior with fallback safeguards for other callback types.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 04611381-6975-424e-a6dd-484a3b5ea323

📥 Commits

Reviewing files that changed from the base of the PR and between 7babd8e and fff0166.

📒 Files selected for processing (2)
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/lower_call/early_branches.rs

📝 Walkthrough

Walkthrough

The codegen now resolves eligible untyped or Any parameter bindings, routes matching callback targets through guarded closure-call lowering, and selects direct-call guards based on binding and feedback state.

Changes

Callee resolution

Layer / File(s) Summary
Entry binding resolution
crates/perry-codegen/src/codegen/helpers.rs
emit_callee_binding_resolutions now admits parameter bindings with absent or Any type hints. Other type hints remain excluded.
Guarded closure-call lowering
crates/perry-codegen/src/lower_call/early_branches.rs
try_lower_closure_typed_local_call now accepts matching entries in resolved_arrow_callback_targets, including bindings whose type hint is Any.
Direct-call guard paths
crates/perry-codegen/src/lower_call/early_branches.rs
Guard-free bindings bypass guards. When typed feedback is disabled, other bindings use an inline closure identity probe. Typed feedback continues to use the runtime guard.

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

Merge Risk: ⚪ Minimal · up to 7babd

This narrowly broadens a callback-call optimization while preserving existing behavior for unresolved or non-function values. No actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: jdalton, thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. 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 clearly identifies the primary codegen change and its measured performance impact for Any-typed callback parameters. It is specific and related to the changeset.
Description check ✅ Passed The description provides detailed change scope, behavior, benchmarks, semantics, and test results. It does not use the template headings or include the checklist, but the required information is mostl…
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.
Full details: Description check

Explanation

The description provides detailed change scope, behavior, benchmarks, semantics, and test results. It does not use the template headings or include the checklist, but the required information is mostly present, including references to related issues and test verification.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@proggeramlug
proggeramlug force-pushed the perf/callee-resolution-any-params branch from 7babd8e to fff0166 Compare August 30, 2026 01:05
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged, after resolving a conflict against main — details at the end.

The reasoning that makes this safe is the sentence in the second hunk:

The arm's runtime behavior is hint-independent (checked unbox, resolved-target diamond, full-dispatcher fallback); the hint only ever selected who enters it.

That's the right framing. Widening entry to an Any-typed parameter doesn't weaken any check — it lets a binding reach a guarded arm that was already going to validate it. And scoping to param_ids rather than all Any locals is what keeps it to the shape you're targeting.

So the thing worth testing is what happens when an Any parameter is not a closure, since that's the case the hint used to exclude. 21 shapes, byte-identical to node v26.5.1:

Any param holds node
5–10 5, null, undefined, {}, "str", [] TypeError in all six
11 a class constructor (callable, not [[Call]]-able) TypeError
12, 13 a native (Math.abs), a bound method 3, 33
14, 15 a generator function, an async function "0[object Generator]…", "string"
16, 17 two different closures at one site; the value reassigned mid-loop [2,100], [1,2,20,30]
19, 20 throw from inside; recursion through the param RangeError, 40320
1, 2, 21 the __commonJS and __esm factory shapes, memoised [42,42], [1,1,1], ["init"]

Cases 5–11 are the ones that would misdispatch if the guarded arm's fallback were hint-dependent; all six throw exactly as node does. 17 is the polymorphic-reassignment case.

The conflict, and why it mattered. The branch didn't merge cleanly against main: #9105 had added an early continue for ctx.local_closure_func_ids in the same block your hunk restructures. I kept both — the statically-known-callee skip first (a known func_id is better served by the direct inlinable call than by an entry-resolved indirect one), then your function_hinted || any_param gate. They compose correctly: the skip removes bindings that shouldn't be in the map at all, and your predicate widens who gets in among the rest.

Worth flagging how that first presented: my initial git merge left conflict markers in the tree, the build failed with error: mismatched closing delimiter, and my probe then ran a stale binary and reported "byte-identical to node" — a green result from a build that never happened. The numbers above are from the rebased tree after the resolution.

I also removed five rustc-ice-*.txt dumps rustc left in the worktree when it choked on the conflict-marked source.

Validation: codegen 1347 passed, runtime 2819 (RUST_TEST_THREADS=1, exit 0, no aborts), perry --bins 1066, fmt clean, run_lint_gates.sh all 60 gates passed; 2 CI-only skipped. git diff origin/main --diff-filter=D empty.

@proggeramlug
proggeramlug merged commit 1908269 into PerryTS:main Aug 30, 2026
24 of 29 checks passed
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