Skip to content

fix(#957): CJS IIFE .call(this) + IndexUpdate codegen#959

Merged
proggeramlug merged 1 commit into
mainfrom
fix-lodash-default-import-957
May 17, 2026
Merged

fix(#957): CJS IIFE .call(this) + IndexUpdate codegen#959
proggeramlug merged 1 commit into
mainfrom
fix-lodash-default-import-957

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • Fixes feat(security): #498 — pin SHA-256 of perry.nativeLibrary prebuilt archives in perry.lock #957import _ from "lodash"; _.add(1, 2) resolved _ to undefined under perry.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.
  • HIR: rewrite <FnExpr | ArrowExpr>.call(thisArg, ...args) (paren-unwrapped) to a direct call when the closure doesn't capture this — drops the thisArg. Catches the ;(function() { ... }.call(this)) shape lodash + every UMD prelude uses; pre-fix the body never ran and module.exports = _ was silently dropped inside the cjs_wrap IIFE.
  • Codegen: implement Expr::IndexUpdate (++obj[key] / obj[key]++ / ++arr[i]). Pre-fix it bailed not yet supported, which (under PERRY_ALLOW_UNIMPLEMENTED=1) linker-stubbed the whole lodash module so the default-import bound to nothing.
  • Runtime: extend js_dyn_index_get to dispatch string-keyed reads through js_object_get_field_by_name_f64, and add js_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.ts byte-matches node --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).
  • Reproduction (import _ from "lodash"; _.add(1, 2)) advances past the _.add undefined symptom. Next runtime gap inside lodash itself (Function('return this')(), bare global) is tracked separately — not a CJS-default-import issue.
  • Sample gap-suite tests still pass (array methods, class advanced).

Out of scope

Real lodash still throws TypeError: value is not a function during init because runInContext() calls root.Object() where root = freeGlobal || freeSelf || Function('return this')() and Perry: (a) lowers bare global / self to the 0.0 sentinel, (b) doesn't support Function(string) as a runtime function constructor, (c) returns a non-truthy stub for globalThis. These are pre-existing gaps unrelated to CJS default-import resolution.

`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.
@proggeramlug proggeramlug merged commit 6ec9074 into main May 17, 2026
14 of 18 checks passed
@proggeramlug proggeramlug deleted the fix-lodash-default-import-957 branch May 17, 2026 21:01
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.
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