Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions crates/perry-hir/src/eval_classifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ pub fn const_string_of(expr: &ast::Expr) -> Option<String> {
.unwrap_or_else(|| q.raw.as_str().to_string())
})
}
// Constant string concatenation: `'a' + 'b' + 'c'`. Test262's
// procedurally-generated eval cases split a body across `+`-joined
// string literals (one segment per `switch` case / `if` branch), so
// the whole argument is still a constant the AOT eval fold can run.
// Only fold when BOTH operands are themselves constant strings — a
// numeric `+` (or a string + non-constant) is not a constant body.
ast::Expr::Bin(bin) if bin.op == ast::BinaryOp::Add => {
let mut left = const_string_of(&bin.left)?;
let right = const_string_of(&bin.right)?;
left.push_str(&right);
Some(left)
}
_ => None,
}
}
Expand Down Expand Up @@ -684,6 +696,38 @@ mod tests {
assert_eq!(c.body_preview.as_deref(), Some("return 7"));
}

#[test]
fn constant_string_concatenation_folds() {
// Test262's procedurally-generated eval cases split a body across
// `+`-joined string literals; the whole argument is still a constant.
let add = |l: ast::Expr, r: ast::Expr| {
ast::Expr::Bin(ast::BinExpr {
span: Span::new(BytePos(0), BytePos(0)),
op: ast::BinaryOp::Add,
left: Box::new(l),
right: Box::new(r),
})
};
// `'switch (1) {' + ' case 1:' + '}'`
let expr = add(
add(str_lit("switch (1) {"), str_lit(" case 1:")),
str_lit("}"),
);
assert_eq!(
const_string_of(&expr).as_deref(),
Some("switch (1) { case 1:}")
);
// A non-`+` operator, or a non-constant operand, does not fold.
let sub = ast::Expr::Bin(ast::BinExpr {
span: Span::new(BytePos(0), BytePos(0)),
op: ast::BinaryOp::Sub,
left: Box::new(str_lit("a")),
right: Box::new(str_lit("b")),
});
assert_eq!(const_string_of(&sub), None);
assert_eq!(const_string_of(&add(str_lit("a"), non_const())), None);
}

#[test]
fn line_resolved_from_installed_module_source() {
// Offset lands on line 3 (two newlines precede it).
Expand Down
58 changes: 43 additions & 15 deletions crates/perry-hir/src/lower/const_fold_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::eval_classifier::{const_string_of, eval_diag_enabled, EvalSurface};
use crate::ir::Expr;

use super::expr_function::lower_fn_expr;
use super::global_eval_hoist::apply_global_eval_hoist;
use super::lower_expr::lower_expr;
use super::LoweringContext;

Expand Down Expand Up @@ -746,25 +747,29 @@ pub(crate) fn try_indirect_eval_general(
// enclosing scope would wrongly resolve module/function-locals that real
// global eval cannot see, so defer those to the runtime global-`eval`
// thunk. (Only the parse-/early-error SyntaxError cases above are modeled.)
let module_top_global = super::lower_expr::global_script_this_enabled()
&& ctx.scope_depth == 0
&& ctx.current_class.is_none()
&& ctx.with_env_stack.is_empty()
&& !ctx.is_external_module;
// Only fold a *declaration-free* body. A scope-capturing IIFE places any
// `var`/`function`/`class`/`let`/`const` the body declares inside the
// wrapper, but real global eval routes them to the global var environment
// (`var`/`function`) or the eval's own fresh lexical environment
// (`let`/`const`/`class`) — and Perry additionally registers class names at
// module scope, so a folded `class C {}` would leak `C` to the top level
// (test262 language/eval-code/indirect/lex-env-distinct-cls expects it to
// stay invisible). A body with no declarations has no such binding to
// misplace; it only reads/assigns the globals it names, which the IIFE
// resolves correctly. Any declaration → defer to the runtime thunk.
let module_top_global = eval_is_module_top_global(ctx);
// A scope-capturing IIFE places any `var`/`function`/`class`/`let`/`const`
// the body declares inside the wrapper, but real global eval routes them to
// the global var environment (`var`/`function`) or the eval's own fresh
// lexical environment (`let`/`const`/`class`). A declaration-free body has no
// such binding to misplace; it only reads/assigns the globals it names,
// which the IIFE resolves correctly.
if module_top_global && !eval_body_declares_bindings(&body_stmts) {
let eval_strict = crate::lower_decl::body_has_use_strict(&body_stmts);
return build_eval_completion_iife(ctx, body_stmts, eval_strict, span);
}
// Annex B.3.3.3: a sloppy global (indirect) eval whose body declares
// `var`/`function` bindings hoists them into the global variable
// environment. Rewrite those to global assignments and fold; the rewrite
// bails (→ defer to the runtime thunk) on a `class` declaration — which
// Perry would otherwise register at module scope, leaking it past the eval
// (test262 language/eval-code/indirect/lex-env-distinct-cls expects it to
// stay invisible).
if module_top_global && !crate::lower_decl::body_has_use_strict(&body_stmts) {
if let Some(hoisted) = apply_global_eval_hoist(&body_stmts) {
return build_eval_completion_iife(ctx, hoisted, false, span);
}
}
let _ = span;
Ok(None)
}
Expand Down Expand Up @@ -1478,9 +1483,32 @@ fn try_const_fold_eval(
// plain assignment. (test262 language/eval-code/direct/strictness-override)
let eval_strict = ctx.current_strict || crate::lower_decl::body_has_use_strict(&body_stmts);

// Annex B.3.3.3: a *sloppy global* direct eval routes the `var`/`function`
// declarations of its body into the global variable environment, so they
// survive after the eval returns. Rewrite them to global assignments before
// folding (otherwise the completion IIFE traps them as arrow-locals). Strict
// eval keeps its own variable environment (the IIFE already models that).
if !eval_strict && eval_is_module_top_global(ctx) {
if let Some(hoisted) = apply_global_eval_hoist(&body_stmts) {
return build_eval_completion_iife(ctx, hoisted, eval_strict, span);
}
}

build_eval_completion_iife(ctx, body_stmts, eval_strict, span)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Is the current eval call site at module top level in global-script mode,
/// where the enclosing variable environment *is* the global object — the only
/// place the Annex B.3.3.3 global var-scoped hoisting ([`apply_global_eval_hoist`])
/// applies? (Mirrors the `module_top_this`/`module_top_global` guards.)
fn eval_is_module_top_global(ctx: &LoweringContext) -> bool {
super::lower_expr::global_script_this_enabled()
&& ctx.scope_depth == 0
&& ctx.current_class.is_none()
&& ctx.with_env_stack.is_empty()
&& !ctx.is_external_module
}

/// Build the completion-tracking IIFE that runs an eval body AOT and yields its
/// ECMAScript completion value: `(() => { var __perry_cv; <tracked body>; return
/// __perry_cv })()`. Shared by direct eval and global (indirect) eval. `strict`
Expand Down
Loading
Loading