fix(#957): CJS IIFE .call(this) + IndexUpdate codegen#959
Merged
Conversation
`import _ from "lodash"; _.add(1, 2)` resolved `_` to undefined under
`perry.compilePackages: ["lodash"]`. Two distinct bugs combined:
1. Inline `;(function() { ... }.call(this))` IIFE bodies never executed
— `Closure.call` fell through generic method dispatch — so the CJS
wrap's `module.exports = _` write was silently dropped. Fix:
rewrite `<FnExpr|ArrowExpr>.call(thisArg, ...args)` to a direct
call dropping the thisArg when the closure doesn't capture `this`.
2. `Expr::IndexUpdate` (`++arr[i]` / `obj[key]++`) bailed at codegen
with `not yet supported`, stubbing lodash entirely. Fix: lower
read/modify/write through `js_dyn_index_get` (extended for
string-key dispatch) and a new `js_dyn_index_set` runtime helper
that routes by gc_type.
Real lodash advances past the `_.add` undefined symptom; the next
runtime gap (`Function('return this')()` not callable, bare `global`
not truthy) is tracked separately.
3 tasks
4 tasks
proggeramlug
added a commit
that referenced
this pull request
May 17, 2026
…rs (#963) Closes two distinct module-init holes flagged in PR #959's commit message ("the next runtime gap") that kept real lodash throwing `TypeError: value is not a function` before any user code ran: 1. `var root = freeGlobal || freeSelf || Function('return this')();` The bare `Function` ident lowered to `Expr::GlobalGet(0)` (the no-resolution sentinel), so the inner call dispatched through `js_closure_call1` with a null handle. AST-match the two-call shape at HIR lower time and fold to a new `Expr::GlobalThisExpr` variant that lowers to `js_get_global_this()` — the same lazy singleton `globalThis[X] = V` already writes to (#611). 2. `var reHasEscapedHtml = RegExp(reEscapedHtml.source);` (~6 sites in lodash). Bare `RegExp(...)` (and `new RegExp(<non-literal>)`) hit the same null-callee path. Fold both to a new `Expr::RegExpDynamic { pattern, flags }` that lowers to the existing `js_regexp_new(pattern, flags)` runtime entrypoint — the same entry the static `/foo/g` arm uses. Real lodash advances past the IIFE-init crash; the next gap is `var Array = context.Array` against the empty globalThis singleton (lodash needs `globalThis.Array === Array` and friends), which is a separate architectural change. Regression test: test-files/test_lodash_function_return_this_regexp.ts (12 assertions, byte-for-byte match with `node --experimental-strip-types`).
proggeramlug
added a commit
that referenced
this pull request
May 25, 2026
…#1678) (#1776) Phase 0 of #1677 (AOT-first eval/new Function strategy). Establishes the single decision point every later phase builds on: a classifier that buckets each `new Function` / `Function(...)` / `eval(...)` site into 1. const-foldable — literal/substitution-free body (→ #1679) 2. known-library-codegen — from fast-json-stringify / ajv / find-my-way (the Fastify JIT trio; → #1680/#1681/#1682) 3. runtime-unknown — genuinely runtime-dynamic code string Only the runtime-unknown bucket is refused, with a precise diagnostic that names the surface, file:line, and originating package (or "user source"). Buckets 1 and 2 keep their existing placeholder lowering so the phases that own them can swap it in without a behaviour change here — Phase 0 is pure analysis + reporting, it never compiles, folds, or evaluates anything. Before this, both shapes silently fell through to broken lowerings (a bare `Function`/`eval` ident → GlobalGet(0) sentinel → runtime TypeError, and `new Function(...)` → an unknown-class class_id=0 empty-object placeholder) with no indication of why. New module crates/perry-hir/src/eval_classifier.rs (pure classification + diagnostic + instrumentation), hooked at the two Function-shape lowering sites (expr_new for `new Function`, expr_call for `Function(...)`/`eval`). The `Function('return this')()` globalThis fold (#957/#959) runs first and short-circuits, so it is unaffected. Instrumentation: PERRY_EVAL_DIAG=1 logs every classified site (surface, file:line, package, bucket, body preview) to stderr. Escape hatch: PERRY_ALLOW_EVAL=1 downgrades the bucket-3 refusal to the legacy fall-through for a one-off build (mirrors #503's PERRY_ALLOW_DYNAMIC_STDLIB). Tests: 9 unit tests covering each bucket + provenance + line resolution + preview truncation. Verified end-to-end on direct `new Function`/`eval` samples (refused, with file:line + provenance), a const-foldable sample (passes through), an ajv-path sample (known-library bucket), and the existing `Function('return this')()` fold (still works).
proggeramlug
added a commit
that referenced
this pull request
May 25, 2026
…#1679) (#1783) Phase 1 of #1677. When the Phase 0 classifier (#1678) would bucket a `new Function(...)` / `Function(...)` site as const-foldable — every argument is a compile-time-constant string — compile it to a real native function instead of leaving it to fall through. This is true ahead-of-time eval and builds the string→HIR plumbing Phase 3 will reuse. How: synthesize the equivalent `(function (<params>) { <body> })` source (joining all-but-last args as the param list, last arg as the body, per Node's `new Function` semantics), parse it via perry-parser, and lower it through the normal `lower_fn_expr` path — exactly as if the user had written the function literal. The body references only its own params plus globals, so it lowers to a capture-free closure (new Function has no enclosing-scope access). Also folds the `(0, eval)('this')` / `(0, eval)('globalThis')` indirect-eval idiom to Expr::GlobalThisExpr (indirect eval runs in global scope), the same singleton `Function('return this')()` folds to (#957/#959). New module crates/perry-hir/src/lower/const_fold_fn.rs, hooked at both Function-shape sites BEFORE the Phase 0 refusal: expr_new.rs (`new Function`) and expr_call/mod.rs (`Function(...)` / indirect eval). The `Function('return this')()` fold still runs first and is unaffected. Non-constant bodies still hit the Phase 0 refusal — no regression. A const body that parses but can't lower surfaces a clear, span-tagged compile error at the call site instead of the old broken placeholder. perry-parser promoted from dev- to regular dependency of perry-hir (no cycle; only adds swc_ecma_parser to the build) so lowering can parse the synthesized source. Tests: test-files/test_new_function_const_fold.ts (single-expression body, multi-arg param names, comma-joined params, no-param, multi-statement body referencing a global, the call form) and test-files/test_indirect_eval_globalthis.ts — both byte-for-byte parity vs `node --experimental-strip-types`. perry-hir suite green; fmt/clippy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
perry.nativeLibraryprebuilt archives inperry.lock#957 —import _ from "lodash"; _.add(1, 2)resolved_toundefinedunderperry.compilePackages. Two distinct bugs combined; the first is the inline-IIFE CJS-prelude pattern that every UMD package uses, the second is a missing codegen arm that stubbed the entire package.<FnExpr | ArrowExpr>.call(thisArg, ...args)(paren-unwrapped) to a direct call when the closure doesn't capturethis— drops thethisArg. Catches the;(function() { ... }.call(this))shape lodash + every UMD prelude uses; pre-fix the body never ran andmodule.exports = _was silently dropped inside the cjs_wrap IIFE.Expr::IndexUpdate(++obj[key]/obj[key]++/++arr[i]). Pre-fix it bailednot yet supported, which (underPERRY_ALLOW_UNIMPLEMENTED=1) linker-stubbed the whole lodash module so the default-import bound to nothing.js_dyn_index_getto dispatch string-keyed reads throughjs_object_get_field_by_name_f64, and addjs_dyn_index_set(gc_type-aware) so the read-modify-write codegen handles arrays, plain objects, and TypedArrays uniformly.Test plan
test-files/test_lodash_default_import_methods.tsbyte-matchesnode --experimental-strip-types(IIFE.call writes-outer + arrow.call(thisArg, args); prefix/postfix inc/dec on object string-key and array numeric index; prefix-returns-new vs postfix-returns-old).import _ from "lodash"; _.add(1, 2)) advances past the_.addundefined symptom. Next runtime gap inside lodash itself (Function('return this')(), bareglobal) is tracked separately — not a CJS-default-import issue.Out of scope
Real lodash still throws
TypeError: value is not a functionduring init becauserunInContext()callsroot.Object()whereroot = freeGlobal || freeSelf || Function('return this')()and Perry: (a) lowers bareglobal/selfto the0.0sentinel, (b) doesn't supportFunction(string)as a runtime function constructor, (c) returns a non-truthy stub forglobalThis. These are pre-existing gaps unrelated to CJS default-import resolution.