Skip to content

Commit f8c259c

Browse files
proggeramlugRalphclaude
authored
fix(hir): #5579 — publish module-top global eval's top-level var/function as configurable global bindings (#5725)
* fix(hir): #5579 — publish module-top global eval's top-level var/function 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> * fix(hir): #5579 — CreateGlobalFunctionBinding for eval'd top-level functions (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> --------- Co-authored-by: Ralph <ralph@skelpo.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9fb1940 commit f8c259c

2 files changed

Lines changed: 422 additions & 35 deletions

File tree

crates/perry-hir/src/lower/const_fold_fn.rs

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -778,16 +778,31 @@ pub(crate) fn try_indirect_eval_general(
778778
let eval_strict = crate::lower_decl::body_has_use_strict(&body_stmts);
779779
return build_eval_completion_iife(ctx, body_stmts, eval_strict, span);
780780
}
781-
// Annex B.3.3.3: a sloppy global (indirect) eval whose body declares
782-
// `var`/`function` bindings hoists them into the global variable
783-
// environment. Rewrite those to global assignments and fold; the rewrite
784-
// bails (→ defer to the runtime thunk) on a `class` declaration — which
785-
// Perry would otherwise register at module scope, leaking it past the eval
786-
// (test262 language/eval-code/indirect/lex-env-distinct-cls expects it to
787-
// stay invisible).
788-
if module_top_global && !crate::lower_decl::body_has_use_strict(&body_stmts) {
789-
if let Some(hoisted) = apply_global_eval_hoist(&body_stmts) {
790-
return build_eval_completion_iife(ctx, hoisted, false, span);
781+
if module_top_global {
782+
let eval_strict = crate::lower_decl::body_has_use_strict(&body_stmts);
783+
// Annex B.3.3.3: a *sloppy* global (indirect) eval whose body declares
784+
// `var`/`function` bindings hoists them into the global variable
785+
// environment. Rewrite those to global assignments and fold; the rewrite
786+
// bails (→ falls through below) on a `class` declaration — which Perry
787+
// would otherwise register at module scope, leaking it past the eval
788+
// (test262 language/eval-code/indirect/lex-env-distinct-cls expects it to
789+
// stay invisible). Strict eval keeps its own variable environment, which
790+
// the completion IIFE already models, so it skips the hoist.
791+
if !eval_strict {
792+
if let Some(hoisted) = apply_global_eval_hoist(&body_stmts) {
793+
return build_eval_completion_iife(ctx, hoisted, false, span);
794+
}
795+
}
796+
// No nested function to hoist (top-level declarations only, or a strict
797+
// body). Still fold the body to the completion IIFE so it runs for its
798+
// side effects and yields its ECMAScript completion value — the runtime
799+
// eval thunk otherwise returns `undefined` *without executing the body*,
800+
// dropping both. Guarded by `eval_body_iife_foldable`, which keeps a
801+
// class-declaring body on the runtime thunk (the class would leak to
802+
// module scope when lowered in the IIFE). (test262 language/eval-code/
803+
// indirect/cptn-nrml-* with declarations, var-env-var-* completion.)
804+
if eval_body_iife_foldable(&body_stmts) {
805+
return build_eval_completion_iife(ctx, body_stmts, eval_strict, span);
791806
}
792807
}
793808
let _ = span;
@@ -854,6 +869,60 @@ fn stmt_declares_binding(stmt: &ast::Stmt) -> bool {
854869
}
855870
}
856871

872+
/// Can a declaration-bearing (indirect) eval body be folded to the completion
873+
/// IIFE — executing it for its side effects and completion value — without an
874+
/// observably wrong result? The one disqualifier is a *class declaration*: Perry
875+
/// registers class names at module scope when lowering them inside the IIFE,
876+
/// which would leak the class past the eval (real global eval discards the
877+
/// eval's own lexical environment, so the class is invisible afterward — test262
878+
/// `language/eval-code/indirect/lex-env-distinct-cls`). `var`/`function` only
879+
/// fail to *publish* to the global var environment (trapped as arrow-locals),
880+
/// which is no worse than the runtime thunk not executing the body at all; and
881+
/// `let`/`const` correctly stay arrow-local (matching the eval's fresh, discarded
882+
/// lexical environment). Scans recursively, mirroring [`stmt_declares_binding`].
883+
fn eval_body_iife_foldable(stmts: &[ast::Stmt]) -> bool {
884+
!stmts.iter().any(stmt_has_class_decl)
885+
}
886+
887+
fn stmt_has_class_decl(stmt: &ast::Stmt) -> bool {
888+
use ast::Stmt;
889+
match stmt {
890+
Stmt::Decl(ast::Decl::Class(_)) => true,
891+
Stmt::Decl(_) => false,
892+
Stmt::Block(b) => b.stmts.iter().any(stmt_has_class_decl),
893+
Stmt::Labeled(l) => stmt_has_class_decl(&l.body),
894+
Stmt::If(i) => {
895+
stmt_has_class_decl(&i.cons) || i.alt.as_deref().is_some_and(stmt_has_class_decl)
896+
}
897+
Stmt::For(f) => stmt_has_class_decl(&f.body),
898+
Stmt::ForIn(f) => stmt_has_class_decl(&f.body),
899+
Stmt::ForOf(f) => stmt_has_class_decl(&f.body),
900+
Stmt::While(w) => stmt_has_class_decl(&w.body),
901+
Stmt::DoWhile(d) => stmt_has_class_decl(&d.body),
902+
Stmt::With(w) => stmt_has_class_decl(&w.body),
903+
Stmt::Try(t) => {
904+
t.block.stmts.iter().any(stmt_has_class_decl)
905+
|| t.handler
906+
.as_ref()
907+
.is_some_and(|h| h.body.stmts.iter().any(stmt_has_class_decl))
908+
|| t.finalizer
909+
.as_ref()
910+
.is_some_and(|f| f.stmts.iter().any(stmt_has_class_decl))
911+
}
912+
Stmt::Switch(s) => s
913+
.cases
914+
.iter()
915+
.any(|c| c.cons.iter().any(stmt_has_class_decl)),
916+
Stmt::Expr(_)
917+
| Stmt::Empty(_)
918+
| Stmt::Debugger(_)
919+
| Stmt::Return(_)
920+
| Stmt::Break(_)
921+
| Stmt::Continue(_)
922+
| Stmt::Throw(_) => false,
923+
}
924+
}
925+
857926
pub(crate) fn try_indirect_eval_globalthis(
858927
ctx: &LoweringContext,
859928
call: &ast::CallExpr,
@@ -1593,3 +1662,63 @@ fn build_eval_completion_iife(
15931662
byte_offset: 0,
15941663
}))
15951664
}
1665+
1666+
#[cfg(test)]
1667+
mod foldable_tests {
1668+
use super::eval_body_iife_foldable;
1669+
use swc_ecma_ast as ast;
1670+
1671+
fn parse(src: &str) -> Vec<ast::Stmt> {
1672+
perry_parser::parse_typescript(src, "<eval body>.cjs")
1673+
.expect("parses")
1674+
.body
1675+
.into_iter()
1676+
.filter_map(|item| match item {
1677+
ast::ModuleItem::Stmt(s) => Some(s),
1678+
_ => None,
1679+
})
1680+
.collect()
1681+
}
1682+
1683+
#[test]
1684+
fn declaration_bearing_non_class_bodies_are_foldable() {
1685+
// The bodies that regressed to `undefined` because they declare a
1686+
// binding (so the declaration-free fast path skipped them) yet have no
1687+
// nested function to hoist — now fold to the completion IIFE.
1688+
for src in [
1689+
"var a = 1; 42",
1690+
"initial = x; var x = 9;",
1691+
"let y = 2; y + 1",
1692+
"const z = 3; ({})",
1693+
"{ var nested; } 7",
1694+
"for (var i = 0; i < 1; i++) {} 5",
1695+
"'use strict'; var s = 1; s",
1696+
] {
1697+
assert!(
1698+
eval_body_iife_foldable(&parse(src)),
1699+
"expected foldable: {src:?}"
1700+
);
1701+
}
1702+
}
1703+
1704+
#[test]
1705+
fn class_declaring_bodies_are_not_foldable() {
1706+
// A class declaration (top-level or nested) would leak to module scope
1707+
// when lowered in the IIFE, so it stays on the runtime thunk.
1708+
for src in [
1709+
"class C {}",
1710+
"{ class C {} }",
1711+
"if (true) { class C {} }",
1712+
"switch (1) { case 1: class C {} }",
1713+
"try { class C {} } catch (e) {}",
1714+
] {
1715+
assert!(
1716+
!eval_body_iife_foldable(&parse(src)),
1717+
"expected NOT foldable: {src:?}"
1718+
);
1719+
}
1720+
// A class *expression* binds through `var`/`let` (no module-scope
1721+
// registration) and stays foldable.
1722+
assert!(eval_body_iife_foldable(&parse("var x = class {}; x")));
1723+
}
1724+
}

0 commit comments

Comments
 (0)