Skip to content

fix(hir): #5579 — publish module-top global eval's top-level var/function as configurable global bindings#5725

Merged
proggeramlug merged 2 commits into
mainfrom
worktree-fix-5579-eval-residual
Jun 27, 2026
Merged

fix(hir): #5579 — publish module-top global eval's top-level var/function as configurable global bindings#5725
proggeramlug merged 2 commits into
mainfrom
worktree-fix-5579-eval-residual

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Residual of the #5579 regression tracker: the var-env-* eval-code cluster ("x/f should be an own property"). A sloppy global (direct or indirect) eval whose body declares a top-level var or function must create a configurable own binding on the global object (CreateGlobalVarBinding / CreateGlobalFunctionBinding) that survives after the eval returns.

Perry folds an eval body into a scope-capturing arrow IIFE, which trapped top-level var/function as arrow-locals, so eval('var x; …') / eval('function f(){}') never published x/f to globalThis. #5636 already handled nested block functions; this extends the same hoist to the eval body's own top level.

Changes

  • global_eval_hoist: a top-level function is renamed to a hidden binding and published at instantiation via a CreateGlobalFunctionBinding block (Object.defineProperty(globalThis, …) — writable/enumerable/configurable for an absent/configurable binding; value-only for a non-configurable one); a top-level var x = init becomes a create-if-absent global slot plus void (x = init) (the void preserves the declaration's empty completion value). Non-simple (destructuring) declarators, or a body that rebinds globalThis/Object, bail to the unmodified fold.
  • const_fold_fn: a declaration-bearing indirect eval body at module-top global scope that has nothing to hoist now still folds to the completion IIFE (running its side effects + yielding the completion value) instead of deferring to the runtime eval thunk, which returns undefined without executing the body. Guarded by eval_body_iife_foldable, which keeps a class-declaring body on the runtime thunk (Perry would otherwise leak the class to module scope — lex-env-distinct-cls).

Validation

test262 language/eval-code + annexB/language/eval-code (pinned sha, Node v26.3.0 vm.runInThisContext oracle):

pass runtime-fail parity
before 753 55 93.2%
after 766 42 94.8%

Zero regressions (verified by diffing the full failure path-sets). Newly passing (13): var-env-{var,func}-init-global-new (direct+indirect), var-env-func-init-multi (direct+indirect), var-env-func-init-global-update-{non-,}configurable (indirect / direct+indirect), and the non-definable-function-with-{function,variable} negatives (direct+indirect).

New unit tests cover top-level var/function publishing, the bare-var slot-only path, the CreateGlobalFunctionBinding block, and eval_body_iife_foldable (class vs non-class bodies).

The remaining eval-code residual (descriptor-preserving var updates, function-scope B.3.3.3 / #5657, non-definable-global-* which needs NaN/undefined modeled as non-configurable globalThis own props, and the if(x) function f(){} Annex-B parser gap) is out of scope here.

Refs #5579.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved optimized indirect eval folding to avoid unsafe transformations when the eval body contains class declarations.
    • Refined sloppy global eval so hoisted top-level function/var behavior matches correct completion semantics.
  • New Features
    • Enhanced support for publishing/reflecting top-level function and simple identifier var declarations into the global during optimized eval.
  • Tests
    • Added coverage for foldability with class declarations and for global publishing/slot-creation behavior for top-level function and var.

@coderabbitai

coderabbitai Bot commented Jun 27, 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: 966bda30-ed16-46e3-89d6-df286b2316b2

📥 Commits

Reviewing files that changed from the base of the PR and between 4f0a66b and 768466b.

📒 Files selected for processing (1)
  • crates/perry-hir/src/lower/global_eval_hoist.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-hir/src/lower/global_eval_hoist.rs

📝 Walkthrough

Walkthrough

This PR changes indirect eval lowering to skip completion-IIFE folding for eval bodies containing class declarations, and it rewrites top-level function and var declarations into global publish statements with void-wrapped assignments.

Changes

Indirect eval lowering

Layer / File(s) Summary
Fold gating
crates/perry-hir/src/lower/const_fold_fn.rs
try_indirect_eval_general now checks eval_body_iife_foldable before building the completion IIFE, and the helper scans for any class declaration in the eval body.
Foldability tests
crates/perry-hir/src/lower/const_fold_fn.rs
foldable_tests covers declaration-bearing non-class bodies, class-declaring bodies, and class expressions.
Global publish rewriting
crates/perry-hir/src/lower/global_eval_hoist.rs
GlobalEvalHoist tracks top-level publish state, rewrites safe top-level function and var declarations into void-wrapped publishes, and assembles the prelude and rewritten body.
Publish tests
crates/perry-hir/src/lower/global_eval_hoist.rs
The test suite checks top-level function publishing, top-level var publishing, and bare var slot creation.

Sequence Diagram(s)

sequenceDiagram
  participant try_indirect_eval_general
  participant apply_global_eval_hoist
  participant build_eval_completion_iife
  try_indirect_eval_general->>apply_global_eval_hoist: rewrite sloppy global eval body
  try_indirect_eval_general->>build_eval_completion_iife: fold only when class-free
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5627: Also changes try_indirect_eval_general in crates/perry-hir/src/lower/const_fold_fn.rs to alter when module-top indirect eval bodies are folded.
  • PerryTS/perry#5636: Also updates the indirect-eval lowering path around apply_global_eval_hoist(...) and related folding behavior.

Poem

A rabbit hopped through eval light,
with class-free code and globals bright.
void made each publish softly land,
while hoists and folds went paw in hand.
🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: publishing top-level var/function from global eval as configurable global bindings.
Description check ✅ Passed It includes summary, changes, related issue, and validation, though the template's checklist-style test plan is not fully filled out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 worktree-fix-5579-eval-residual

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/const_fold_fn.rs`:
- Around line 791-805: The eval lowering in
const_fold_fn::build_eval_completion_iife is treating every None from
apply_global_eval_hoist as “safe to fold,” but that also includes unsafe cases
like var-scoped globals, with, destructuring var, and globalThis rebinding.
Change apply_global_eval_hoist to distinguish hoisted, safe-no-hoist, and
unsafe-bail outcomes, then only call build_eval_completion_iife for strict eval
or bodies confirmed not to need global publication; otherwise keep the runtime
thunk path. Update the relevant eval tests in the const_fold_fn test area so for
(var i = ...) asserts the correct global publication/runtime behavior.

In `@crates/perry-hir/src/lower/global_eval_hoist.rs`:
- Around line 646-655: The hoisting logic in global_eval_hoist::fold is treating
nested var declarations as IIFE-local, but sloppy global eval requires all
var-declared names to be published globally even when they appear inside blocks,
cases, or loops. Update the handling around
ast::Stmt::Decl(ast::Decl::Var(var_decl)) so eval-scope var declarations are
rewritten/hoisted regardless of nesting, or emit an unsafe-bail signal that
prevents raw IIFE folding when the rewrite cannot preserve global
VarDeclaredNames semantics.
🪄 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: 1c6ac2ac-4228-4fbc-91be-0001bb276e26

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb1940 and ee18f99.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower/const_fold_fn.rs
  • crates/perry-hir/src/lower/global_eval_hoist.rs

Comment thread crates/perry-hir/src/lower/const_fold_fn.rs
Comment thread crates/perry-hir/src/lower/global_eval_hoist.rs
…tion as configurable global bindings

EvalDeclarationInstantiation: a sloppy *global* (direct or indirect) eval whose
body declares a top-level `var` or `function` must create a configurable own
binding on the global object (CreateGlobalVarBinding / CreateGlobalFunctionBinding)
that survives after the eval returns. Perry folds an eval body into a
scope-capturing arrow IIFE, which trapped top-level `var`/`function` as
arrow-locals, so `eval('var x; …')` / `eval('function f(){}')` never published
`x`/`f` to globalThis (test262 reason: "x/f should be an own property"). #5636
already handled *nested* block functions; this extends the same hoist to the eval
body's own top level.

- global_eval_hoist: a top-level `function` is renamed to a hidden binding and
  published with its value at instantiation via `void (f = <hidden>)` (the `void`
  keeps the declaration's empty completion value); a top-level `var x = init`
  becomes a create-if-absent global slot plus `void (x = init)`. A non-simple
  (destructuring) declarator bails the rewrite.
- const_fold_fn: a declaration-bearing *indirect* eval body at module-top global
  with nothing to hoist now still folds to the completion IIFE (running its side
  effects + yielding the completion value) instead of deferring to the runtime
  eval thunk, which returns `undefined` without executing the body. Guarded by
  `eval_body_iife_foldable`, which keeps a class-declaring body on the runtime
  thunk (Perry would otherwise leak the class to module scope —
  language/eval-code/indirect/lex-env-distinct-cls).

test262 language/eval-code + annexB/language/eval-code: 753 → 764 passing
(parity 93.2% → 94.6%), zero regressions. Fixes var-env-{var,func}-init-global-new
(direct+indirect), var-env-func-init-multi (direct+indirect),
var-env-func-init-global-update-non-configurable, and the
non-definable-function-with-{function,variable} negatives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug force-pushed the worktree-fix-5579-eval-residual branch from ee18f99 to 4f0a66b Compare June 27, 2026 07:19
…nctions (descriptor-correct global publish)

Follow-up to the top-level var/function eval publishing: a top-level function in
a sloppy global eval is now published with the spec's CreateGlobalFunctionBinding
descriptor rules instead of a bare `void (f = <hidden>)` assignment. An absent or
configurable global binding is (re)defined as a writable, enumerable, configurable
data property; a non-configurable one keeps its attributes and only takes the new
value. This makes the published descriptor correct when the eval redefines an
existing *configurable* global whose attributes differ (e.g. a non-enumerable,
non-writable `f`).

`apply_global_eval_hoist` now also bails (→ unmodified fold) when the eval body
rebinds `Object` at function scope, since the publish reads `Object.defineProperty`
/ `Object.getOwnPropertyDescriptor`.

test262 language/eval-code + annexB/language/eval-code: 764 → 766 passing
(parity 94.6% → 94.8%), zero regressions. Fixes
var-env-func-init-global-update-configurable (direct + indirect).

(The non-definable-global-{function,generator} negatives remain: Perry does not
model `NaN`/`undefined` as non-configurable own properties of globalThis, so the
illegal redefinition does not throw — a separate globalThis-modeling gap.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug merged commit f8c259c into main Jun 27, 2026
15 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-5579-eval-residual branch June 27, 2026 08:06
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