Skip to content
Closed
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
9 changes: 9 additions & 0 deletions crates/perry-codegen/src/expr/this_super_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)),
};
lower_event_emitter_subclass_init(ctx, &this_box);
bind_derived_this_after_super(ctx);
let current_class_name =
ctx.class_stack.last().cloned().unwrap_or_default();
crate::lower_call::apply_field_initializers_recursive(
Expand Down Expand Up @@ -1162,6 +1163,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&this_box,
&lowered_args,
);
// The native base initialized the provisional receiver, so a
// successful super() must now initialize the derived `this`
// binding before field initializers or the remaining
// constructor body can observe it. Without this, an indirect
// chain such as Counter -> B -> EventEmitter installed the
// emitter surface but the next `this.seen = ...` still threw
// the pre-super ReferenceError.
bind_derived_this_after_super(ctx);
// Spec: derived-class field initializers run AFTER `super()`
// returns. The native base is the chain root and has no TS
// fields, so everything after it still needs initializing —
Expand Down
26 changes: 26 additions & 0 deletions crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,32 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
// via `js_new_function_construct` — see
// `perry-codegen/src/lower_call/new.rs`.
}
// A named native-module export whose public name is not a class
// name still has a real runtime function value. Route `new` over
// that value through the dynamic constructor check instead of the
// static `Expr::New { class_name }` fallback, which would merely
// allocate an empty placeholder. This is where Node distinguishes
// constructable JavaScript wrappers (`repl.start`, `events.init`)
// from native non-constructors (`path.toNamespacedPath`). The
// runtime's explicit export metadata makes that decision. Keep
// capitalized class exports on the specialized paths below.
if let Some((module, Some(export))) = ctx.lookup_native_module(&class_name) {
if export
.chars()
.next()
.is_some_and(|first| !first.is_uppercase())
{
return Ok(Expr::NewDynamic {
callee: Box::new(Expr::PropertyGet {
byte_offset: 0,
object: Box::new(Expr::NativeModuleRef(module.to_string())),
property: export.to_string(),
}),
args,
byte_offset: new_byte_offset,
});
}
}
// #wall: an ALIASED named import of a native built-in class
// (`import { BlockList as Wj4 } from "net"; new Wj4()`) must
// construct exactly like the un-aliased form. The bare-ident
Expand Down
276 changes: 250 additions & 26 deletions crates/perry-hir/src/lower/lower_module_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
//! reach them via `crate::lower::lower_module*` (or the `lib.rs`
//! re-exports — `pub use lower::{lower_module, ...}`).

use crate::types::Type;
use crate::types::{LocalId, Type};
use anyhow::Result;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use swc_ecma_ast as ast;

use super::*;
Expand All @@ -18,6 +18,242 @@ use crate::lower_types::hoisted_text_codec::{
infer_hoisted_text_codec_var_type, require_literal_specifier,
};

fn reflect_script_var_initializers(
stmts: Vec<Stmt>,
script_vars: &HashMap<LocalId, String>,
next_local_id: &mut LocalId,
) -> Vec<Stmt> {
let mut reflected = Vec::with_capacity(stmts.len());
for mut stmt in stmts {
match &mut stmt {
Stmt::If {
then_branch,
else_branch,
..
} => {
*then_branch = reflect_script_var_initializers(
std::mem::take(then_branch),
script_vars,
next_local_id,
);
if let Some(branch) = else_branch {
*branch = reflect_script_var_initializers(
std::mem::take(branch),
script_vars,
next_local_id,
);
}
}
Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
*body = reflect_script_var_initializers(
std::mem::take(body),
script_vars,
next_local_id,
);
}
Stmt::For {
init, update, body, ..
} => {
if let Some(init_stmt) = init.take() {
let mut expanded = reflect_script_var_initializers(
vec![*init_stmt],
script_vars,
next_local_id,
);
if expanded.len() == 1 {
*init = expanded.pop().map(Box::new);
} else {
// A script `var` initializer can be hoisted out of the
// `for` init slot without changing its one-shot order.
// This makes room for the immediately-following global
// mirror, since HIR's init field holds only one Stmt.
reflected.append(&mut expanded);
}
}
if let Some(update_expr) = update.take() {
let mut update_temps = Vec::new();
*update = Some(reflect_script_var_update_expr(
update_expr,
script_vars,
next_local_id,
&mut update_temps,
));
reflected.append(&mut update_temps);
}
*body = reflect_script_var_initializers(
std::mem::take(body),
script_vars,
next_local_id,
);
}
Stmt::Labeled { body, .. } => {
let inner = std::mem::replace(body, Box::new(Stmt::Break));
let mut expanded =
reflect_script_var_initializers(vec![*inner], script_vars, next_local_id);
*body = if expanded.len() > 1 && matches!(expanded.last(), Some(Stmt::For { .. })) {
// A reflected `for (var ...)` init expands to the init,
// its global mirror, and the loop. Keep those one-shot
// statements immediately before the labeled loop: wrapping
// the whole expansion in `do { ... } while (false)` would
// make `continue label` target the wrapper instead of the
// original `for` statement.
let loop_stmt = expanded.pop().expect("reflected labeled for statement");
reflected.append(&mut expanded);
Box::new(loop_stmt)
} else if expanded.len() == 1 {
Box::new(expanded.pop().expect("one reflected labeled statement"))
} else {
// Non-loop labels are already represented as run-once
// do/while statements. Keep the same representation if a
// direct labeled declaration expands to declaration +
// mirror so `break label` still targets one statement.
Box::new(Stmt::DoWhile {
body: expanded,
condition: Expr::Bool(false),
})
};
}
Stmt::Try {
body,
catch,
finally,
} => {
*body = reflect_script_var_initializers(
std::mem::take(body),
script_vars,
next_local_id,
);
if let Some(catch) = catch {
catch.body = reflect_script_var_initializers(
std::mem::take(&mut catch.body),
script_vars,
next_local_id,
);
}
if let Some(finally) = finally {
*finally = reflect_script_var_initializers(
std::mem::take(finally),
script_vars,
next_local_id,
);
}
}
Stmt::Switch { cases, .. } => {
for case in cases {
case.body = reflect_script_var_initializers(
std::mem::take(&mut case.body),
script_vars,
next_local_id,
);
}
}
Stmt::Let { .. }
| Stmt::Expr(_)
| Stmt::Return(_)
| Stmt::Break
| Stmt::Continue
| Stmt::LabeledBreak(_)
| Stmt::LabeledContinue(_)
| Stmt::Throw(_)
| Stmt::PreallocateBoxes(_)
| Stmt::PreallocateTdzBoxes(_)
| Stmt::ReleaseBoxes(_) => {}
}

let global_var = match &stmt {
Stmt::Let { id, name, .. } if script_vars.contains_key(id) => Some((*id, name.clone())),
_ => None,
};
reflected.push(stmt);
if let Some((id, name)) = global_var {
reflected.push(Stmt::Expr(Expr::PropertySet {
object: Box::new(Expr::GlobalThisExpr),
property: name,
value: Box::new(Expr::LocalGet(id)),
}));
}
}
reflected
}

fn reflect_script_var_update_expr(
mut expr: Expr,
script_vars: &HashMap<LocalId, String>,
next_local_id: &mut LocalId,
temp_decls: &mut Vec<Stmt>,
) -> Expr {
// A loop update is an arbitrary expression tree, not necessarily a bare
// assignment or a top-level comma sequence. Rewrite children first in
// their evaluation order so writes nested in call arguments, computed
// keys, conditionals, etc. are mirrored at the instant they execute.
crate::walker::walk_expr_children_mut(&mut expr, &mut |child| {
let original = std::mem::replace(child, Expr::Undefined);
*child = reflect_script_var_update_expr(original, script_vars, next_local_id, temp_decls);
});

match expr {
Expr::LocalSet(id, value) if script_vars.contains_key(&id) => Expr::Sequence(vec![
Expr::LocalSet(id, value),
script_var_mirror_expr(id, script_vars),
]),
Expr::Update {
id,
op,
prefix: true,
} if script_vars.contains_key(&id) => Expr::Sequence(vec![
Expr::Update {
id,
op,
prefix: true,
},
script_var_mirror_expr(id, script_vars),
]),
Expr::Update {
id,
op,
prefix: false,
} if script_vars.contains_key(&id) => {
// The mirror must run after the update, while postfix `x++` must
// still evaluate to the old value for its parent expression. Save
// that result in a compiler-only local, publish the new binding,
// then restore the expression result.
let temp_id = *next_local_id;
*next_local_id += 1;
temp_decls.push(Stmt::Let {
id: temp_id,
name: format!("__perry_script_var_postfix_{temp_id}"),
ty: Type::Any,
mutable: true,
init: None,
});
Expr::Sequence(vec![
Expr::LocalSet(
temp_id,
Box::new(Expr::Update {
id,
op,
prefix: false,
}),
),
script_var_mirror_expr(id, script_vars),
Expr::LocalGet(temp_id),
])
}
expr => expr,
}
}

fn script_var_mirror_expr(id: LocalId, script_vars: &HashMap<LocalId, String>) -> Expr {
Expr::PropertySet {
object: Box::new(Expr::GlobalThisExpr),
property: script_vars
.get(&id)
.expect("script var mirror requires a script var")
.clone(),
value: Box::new(Expr::LocalGet(id)),
}
}

fn should_enable_react_automatic_jsx(name: &str, ast_module: &ast::Module) -> bool {
let is_jsx_source = name.ends_with(".tsx")
|| name.ends_with(".jsx")
Expand Down Expand Up @@ -1226,33 +1462,21 @@ pub fn lower_module_full(
let is_esm_entry =
!module.imports.is_empty() || !module.exports.is_empty() || module.has_top_level_await;
if ctx.is_entry_module && !is_esm_entry && module.references_global_this {
let script_var_decl_ids: HashSet<_> = ctx
let script_vars: HashMap<_, _> = ctx
.script_var_decl_names
.iter()
.filter_map(|name| ctx.lookup_local(name))
.filter_map(|name| ctx.lookup_local(name).map(|id| (id, name.clone())))
.collect();
// Script `var` bindings are properties of the global object. Insert
// the mirror immediately after each top-level initializer so code
// later in the same script observes the initialized value through
// `globalThis` (ES modules keep their lexical/module binding only).
let mut reflected = Vec::with_capacity(module.init.len());
for stmt in std::mem::take(&mut module.init) {
let global_var = match &stmt {
Stmt::Let { id, name, .. } if script_var_decl_ids.contains(id) => {
Some((*id, name.clone()))
}
_ => None,
};
reflected.push(stmt);
if let Some((id, name)) = global_var {
reflected.push(Stmt::Expr(Expr::PropertySet {
object: Box::new(Expr::GlobalThisExpr),
property: name,
value: Box::new(Expr::LocalGet(id)),
}));
}
}
module.init = reflected;
// Script `var` bindings are properties of the global object. Mirror
// every write at its actual execution point, including declarations
// nested in blocks, loops, switch arms and try/catch/finally. Matching
// by LocalId prevents a same-named lexical shadow from leaking onto
// globalThis (ES modules keep their module binding only).
module.init = reflect_script_var_initializers(
std::mem::take(&mut module.init),
&script_vars,
&mut ctx.next_local_id,
);
}
if ctx.is_entry_module && !is_esm_entry {
const RESTRICTED_GLOBAL_NAMES: [&str; 3] = ["undefined", "NaN", "Infinity"];
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,33 @@ fn test_native_module_binding_value_named_import() {
}
}

#[test]
fn new_named_native_function_routes_through_runtime_constructor_check() {
let source = r#"
import { toNamespacedPath } from "node:path";
new toNamespacedPath();
"#;
let module = perry_parser::parse_typescript(source, "native-new.ts").expect("source parses");
let hir = super::lower_module(&module, "native-new", "native-new.ts").expect("source lowers");
assert!(
hir.init.iter().any(|stmt| matches!(
stmt,
Stmt::Expr(crate::ir::Expr::NewDynamic { callee, .. })
if matches!(
callee.as_ref(),
crate::ir::Expr::PropertyGet { object, property, .. }
if property == "toNamespacedPath"
&& matches!(
object.as_ref(),
crate::ir::Expr::NativeModuleRef(module) if module == "path"
)
)
)),
"new over a named native function must construct its runtime export value: {:#?}",
hir.init
);
}

#[test]
fn test_native_module_binding_value_os_eol() {
// `import { EOL } from 'os'` resolves to the OsEOL intrinsic value, whether
Expand Down
Loading
Loading