Skip to content

fix(#665): compilePackages override + 3 follow-ups to make rate-limiter-flexible work end-to-end - #735

Merged
proggeramlug merged 4 commits into
mainfrom
worktree-issue-665-resolver-opt-in
May 13, 2026
Merged

fix(#665): compilePackages override + 3 follow-ups to make rate-limiter-flexible work end-to-end#735
proggeramlug merged 4 commits into
mainfrom
worktree-issue-665-resolver-opt-in

Conversation

@proggeramlug

@proggeramlug proggeramlug commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Four-commit stack closing #665's investigation. After this PR, the issue's original repro import { RateLimiterMemory } from "rate-limiter-flexible"; new RateLimiterMemory({...}) produces an instance whose methods are reachable as function values (no more typeof limiter.consume === "number").

v0.5.892 — resolver opt-in. perry.compilePackages now overrides the built-in NATIVE_MODULES routing. Two-site change in resolve.rs + collect_modules.rs.

v0.5.893 — codegen side-map consistency. Three side maps in compile_module used last-writer-wins .insert() while class_table used first-writer-wins. When two classes are both default-imported into the same file, the mismatch produced phantom method symbols (@perry_method_..._RateLimiterRes_js__RateLimiterRes__delete etc.). Flipped to .entry().or_insert().

v0.5.894 — setTimeout trailing args. setTimeout(fn, delay, ...args) only had a 2-arg codegen arm; 3+ arg calls (e.g. setTimeout(resolve, delay, res) inside Promise executors) fell through and emitted a bare setTimeout symbol the linker couldn't resolve. Added js_set_timeout_callback_args runtime entry + dispatch via js_closure_call0..9.

v0.5.895 — HIR native-instance override. is_native_module only checked the NATIVE_MODULES manifest, so a compile-package-overridden class still got lowered as a native instance. That made instance.method (bare property read) emit NativeMethodCall { method, args: [] } — a zero-arg FFI getter call that returned 0.0 because the binding had no matching FFI entry, so typeof instance.method returned "number". Fixed via thread-local COMPILE_PACKAGES_OVERRIDE set by the compiler driver around each HIR lowering.

End-to-end validation

$ ./probe   # rate-limiter-flexible
typeof limiter: object
typeof limiter.consume: function          ← was "number" before v0.5.895
limiter.consume: [Function (anonymous)]
limiter.points: 3
limiter.duration: 60

Test plan

  • cargo build --release -p perry -p perry-runtime -p perry-stdlib — clean
  • cargo test --release -p perry — 152 passed
  • 37 cjs_wrap unit tests pass unchanged
  • cargo test --release --workspace --exclude perry-ui-{ios,tvos,watchos,visionos,android,windows,gtk4} — exit 0, 30 buckets, 0 FAILED
  • rate-limiter-flexible: full chain compiles, links, runs; typeof limiter.consume === "function"
  • 2-arg setTimeout(fn, delay) and 3-arg setTimeout(fn, delay, ...args) byte-equal to Bun
  • CI: lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit

Scope

Closes all four layers of #665 that this investigation surfaced. Not in scope:

  • Calling await limiter.consume(...) end-to-end through Promise resolution — bound-method dispatch via CLASS_VTABLE_REGISTRY works for the drizzle/hono fixtures so it should work here, but I haven't probed the full chain.
  • Filling out perry-ext-ratelimit's native surface for users who'd prefer the FFI path over compilePackages.
  • Support for >9 trailing args to setTimeout (would need a variadic-dispatcher closure-call helper).

Refs #665.

@proggeramlug proggeramlug changed the title feat(resolve): #665 — compilePackages overrides NATIVE_MODULES routing fix(#665): compilePackages override + default-import codegen collision May 13, 2026
@proggeramlug proggeramlug changed the title fix(#665): compilePackages override + default-import codegen collision fix(#665): compilePackages override + default-import codegen collision + setTimeout trailing args May 13, 2026
@proggeramlug proggeramlug changed the title fix(#665): compilePackages override + default-import codegen collision + setTimeout trailing args fix(#665): compilePackages override + 3 follow-ups to make rate-limiter-flexible work end-to-end May 13, 2026
@proggeramlug
proggeramlug force-pushed the worktree-issue-665-resolver-opt-in branch from 39ac69c to 7b649e7 Compare May 13, 2026 05:48
…g (v0.5.895)

Two-site override (resolve.rs::resolve_import + collect_modules.rs) so a
package listed in `perry.compilePackages` is compiled from its
node_modules source instead of routed to the built-in Rust FFI binding.
Unblocks users of packages whose native binding is incomplete (e.g.
`rate-limiter-flexible`, where `perry-ext-ratelimit` only wires `consume`
through codegen and the codegen-declared `js_ratelimit_create(I64)`
doesn't match the real two-arg `js_ratelimit_new(points, duration_secs)`
impl).

Default behavior unchanged: stock projects without `compilePackages` go
through the exact same path. Opt-in and package-scoped.

Refs #665. Renumbered from v0.5.892 → v0.5.895 (parallel PRs #734/#736/#737
landed on main). See CHANGELOG.md for full root-cause notes.
… cross-class method symbols (v0.5.896)

When two different classes are both default-imported in the same file,
the `class_ids` / `imported_class_prefix` / `imported_class_source_name`
side maps in `compile_module` overwrote each other (last-writer-wins)
while `class_table` kept the first stub (first-writer-wins). The
mismatch produced method symbols mangled with FIRST stub's method list
+ LAST writer's class name + LAST writer's source prefix — names the
linker can't resolve since Phase F only declares each ic's REAL
methods. Flip the three side-map `.insert()` calls to `.entry().or_insert()`
so all four maps agree on first-writer-wins.

Fixes the `@perry_method_..._RateLimiterRes_js__RateLimiterRes__delete`
/ `__get` / `__set` / `__block` undefined-value errors that surfaced
in rate-limiter-flexible's 5 leaf files once v0.5.895's routing opt-in
let them reach codegen.

Refs #665. See CHANGELOG.md for full root-cause walkthrough.
…trailing args (v0.5.897)

Existing `"setTimeout" if args.len() == 2` arm only handled the 2-arg
shape; 3+ arg call sites (e.g. `setTimeout(resolve, delay, res)` inside
Promise executors per ECMA-262 §27.5.4.1) fell through to the generic
ExternFuncRef fallthrough and emitted `call @settimeout(...)` against a
symbol nothing in stdlib defines. Discovering call site:
`RateLimiterMemory.consume()`'s Promise executor in rate-limiter-flexible.

Adds:
- `CallbackTimer::args: Vec<f64>` (empty for 2-arg shape — back-compat).
- `js_set_timeout_callback_args(callback, delay_ms, args_ptr, n_args)`
  runtime entry that copies the buffer in.
- `js_callback_timer_tick` dispatch via `match args.len()` to
  `js_closure_call0..js_closure_call9`, clamping at 9 trailing args.
- Codegen `"setTimeout" if args.len() >= 3` arm: alloca a stack buffer
  of doubles for the trailing args, GEP for the base, call the new
  runtime entry.

Refs #665. See CHANGELOG.md for full root-cause + scope notes.
…ce lowering (v0.5.898)

HIR's `is_native_module` only consulted the NATIVE_MODULES manifest, so
`rate-limiter-flexible` was lowered as a native import regardless of the
user's `compilePackages` opt-in. That cascaded:

  register_native_module(local, "rate-limiter-flexible", ...)
    → new RateLimiterMemory(...) triggers
  register_native_instance(limiter, "rate-limiter-flexible", "RateLimiterMemory")
    → limiter.consume in expr_member::lower_member emits
  NativeMethodCall { module, object: Some(limiter), method: "consume", args: [] }
    → codegen routes to js_native_call_method → no FFI entry → 0.0

So `typeof limiter.consume === "number"`. For genuine native bindings
(req.method on http.IncomingMessage) this zero-arg-getter lowering is
correct; for compile-package-overridden classes it's wrong because
`consume` is a real method, not a getter.

Fix: add a thread-local `COMPILE_PACKAGES_OVERRIDE: HashSet<String>` in
perry-hir, set by the compiler driver via
`set_compile_packages_override(ctx.compile_packages.clone())` before
each `lower_module_full` invocation and cleared after.
`is_native_module` parses the package name (supports `node:` prefix +
`@scope/pkg` scoped form) and returns false when the package is in the
override even if it appears in NATIVE_MODULES.

Validation: rate-limiter-flexible probe now reports `typeof
limiter.consume: function` (was "number"). Workspace tests green.

Refs #665. See CHANGELOG.md for full root-cause walkthrough.
@proggeramlug
proggeramlug force-pushed the worktree-issue-665-resolver-opt-in branch from 7b649e7 to 5cbade5 Compare May 13, 2026 06:17
@proggeramlug
proggeramlug merged commit a449dc3 into main May 13, 2026
9 checks passed
@proggeramlug
proggeramlug deleted the worktree-issue-665-resolver-opt-in branch May 13, 2026 06:49
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