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
50 changes: 50 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,13 +673,58 @@ pub(super) fn init_static_fields_late(
continue;
}
let key = (c.name.clone(), sf.name.clone());
// Register the field in the runtime CLASS_DYNAMIC_PROPS side
// table (mirroring the StaticFieldSet lowering) so dynamic
// class-ref reads and `getOwnPropertyDescriptor(C, name)` see an
// own data property. Uninitialized fields (`static h;`) register
// `undefined` — per spec they are still own properties.
let emit_static_field_registration = |ctx: &mut crate::expr::FnCtx<'_>, value: &str| {
if let Some(&class_id) = ctx.class_ids.get(&c.name) {
if class_id != 0 {
let idx = ctx.strings.intern(&sf.name);
let entry = ctx.strings.entry(idx);
let bytes_ref = format!("@{}", entry.bytes_global);
let len_str = entry.byte_len.to_string();
let cid_str = class_id.to_string();
ctx.block().call_void(
"js_class_register_static_field",
&[
(crate::types::I32, &cid_str),
(crate::types::PTR, &bytes_ref),
(crate::types::I64, &len_str),
(DOUBLE, value),
],
);
}
}
};
let Some(global_name) = ctx.static_field_globals.get(&key).cloned() else {
continue;
};
if let Some(init_expr) = &sf.init {
if init_references_out_of_scope_local(init_expr) {
continue;
}
// Skip fields whose initializer the HIR already emitted as an
// inline `StaticFieldSet` at the class's source position (the
// spec evaluation point). Re-running it here would (a) fire
// initializer side effects twice and (b) clobber any user
// reassignment made between the class decl and end of module
// init. Mirrors the static-block dedup below. The inline
// lowering also registers the field in CLASS_DYNAMIC_PROPS.
let inline_initialized = hir.init.iter().any(|s| {
matches!(
s,
perry_hir::Stmt::Expr(perry_hir::Expr::StaticFieldSet {
class_name,
field_name,
..
}) if *class_name == c.name && *field_name == sf.name
)
});
if inline_initialized {
continue;
}
// `this` in a static field initializer is the class
// constructor (`static g = this.f + '262'`). Seed the same
// class-ref NaN-box a static method binds (see
Expand All @@ -698,6 +743,11 @@ pub(super) fn init_static_fields_late(
let v = v?;
let g_ref = format!("@{}", global_name);
crate::expr::emit_root_nanbox_store_on_block(ctx.block(), &v, &g_ref);
emit_static_field_registration(ctx, &v);
} else {
let undef =
crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
emit_static_field_registration(ctx, &undef);
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,18 @@ pub(super) fn compile_static_method(
let (this_slot, locals): (String, HashMap<u32, String>) = {
let blk = lf.block_mut(0).unwrap();
let this_slot = blk.alloca(DOUBLE);
blk.store(DOUBLE, &class_ref_lit, &this_slot);
// Receiver-sensitive `this`: dynamic dispatch paths (inherited
// `D.m()`, `C.m.call(x)` / `.apply(x)`) arm a one-shot override that
// this prologue call consumes; direct calls fall back to the lexical
// class-ref, preserving the prior `this === C` behavior. Needed so
// static private brand checks (`this.#x` in a static method) see the
// real receiver (test262 class/elements static-private-*).
let resolved_this = blk.call(
DOUBLE,
"js_static_this_resolve",
&[(DOUBLE, &class_ref_lit)],
);
blk.store(DOUBLE, &resolved_this, &this_slot);
let mut map = HashMap::new();
for p in &f.params {
let arg_name = format!("%arg{}", p.id);
Expand Down
19 changes: 16 additions & 3 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1459,9 +1459,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
return lower_runtime_property_get_by_name(ctx, object, property);
}
let getter_key = (class_name.clone(), format!("__get_{}", property));
if let Some(fn_name) = ctx.methods.get(&getter_key).cloned() {
let recv_box = lower_expr(ctx, object)?;
return Ok(ctx.block().call(DOUBLE, &fn_name, &[(DOUBLE, &recv_box)]));
// STATIC accessors are emitted with the static (no-`this`)
// calling convention under a `perry_static_…` symbol, so the
// instance direct-call ABI here would reference a symbol that
// is never emitted (`__get_get_#f` undefined-value link error
// for `static get #f()`). Route them through the dynamic
// by-name dispatch below, which hits CLASS_STATIC_ACCESSORS.
let is_static_accessor = ctx
.classes
.get(&class_name)
.map(|c| c.static_accessor_names.iter().any(|n| n == property))
.unwrap_or(false);
if !is_static_accessor {
if let Some(fn_name) = ctx.methods.get(&getter_key).cloned() {
let recv_box = lower_expr(ctx, object)?;
return Ok(ctx.block().call(DOUBLE, &fn_name, &[(DOUBLE, &recv_box)]));
}
}
// #1642: bound-method reference for Web Streams instance methods
// (`typeof rs.getReader === "function"`, `const f = rs.getReader;
Expand Down
27 changes: 18 additions & 9 deletions crates/perry-codegen/src/expr/property_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
return lower_runtime_property_set_by_name(ctx, object, property, value);
}
let setter_key = (class_name.clone(), format!("__set_{}", property));
if let Some(fn_name) = ctx.methods.get(&setter_key).cloned() {
let recv_box = lower_expr(ctx, object)?;
let val_double = lower_expr(ctx, value)?;
let _ = ctx.block().call(
DOUBLE,
&fn_name,
&[(DOUBLE, &recv_box), (DOUBLE, &val_double)],
);
return Ok(val_double);
// STATIC accessors compile under the static (no-`this`)
// convention — see the matching gate in property_get.rs.
let is_static_accessor = ctx
.classes
.get(&class_name)
.map(|c| c.static_accessor_names.iter().any(|n| n == property))
.unwrap_or(false);
if !is_static_accessor {
if let Some(fn_name) = ctx.methods.get(&setter_key).cloned() {
let recv_box = lower_expr(ctx, object)?;
let val_double = lower_expr(ctx, value)?;
let _ = ctx.block().call(
DOUBLE,
&fn_name,
&[(DOUBLE, &recv_box), (DOUBLE, &val_double)],
);
return Ok(val_double);
}
}
// Fast path: known class instance + plain instance field.
// The runtime guard checks the receiver's class/shape and
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-codegen/src/expr/static_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
crate::codegen::static_method_registry_key(method_name),
);
if let Some(fn_name) = ctx.methods.get(&key).cloned() {
// Inherited static (`D.f()` resolving to a parent's body): arm
// the one-shot static-`this` override with the DISPATCH base
// class-ref so the body's `js_static_this_resolve` prologue
// sees `this === D` (spec OrdinaryCallBindThis), not the
// lexical defining class. Own methods skip the arm — the
// prologue's lexical fallback is already the right receiver.
let owns_method = ctx
.classes
.get(class_name)
.map(|c| c.static_methods.iter().any(|m| m.name == *method_name))
.unwrap_or(true);
if !owns_method {
if let Some(&cid) = ctx.class_ids.get(class_name) {
let cid_str = cid.to_string();
ctx.block()
.call_void("js_static_this_arm_classref", &[(I32, &cid_str)]);
}
}
let mut lowered: Vec<String> = Vec::with_capacity(args.len());
for a in args {
lowered.push(lower_expr(ctx, a)?);
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/lower_call/console_promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,14 @@ pub fn try_lower_native_method_str_dispatch(
| "isPrototypeOf"
| "toLocaleString"
| "valueOf"
// Annex B §B.2.2 Object.prototype accessor helpers — handled
// by `js_native_call_method`; the static class-dispatch tower
// would read them as a non-callable property and throw
// (test262 elements/private-getter-is-not-a-own-property).
| "__lookupGetter__"
| "__lookupSetter__"
| "__defineGetter__"
| "__defineSetter__"
// #4795: the `using`-declaration disposability validator is not
// a class method — it must reach the runtime `js_native_call_method`
// handler (which checks symbol keys + the class vtable) rather
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-codegen/src/lower_call/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,21 @@ pub fn try_lower_property_get_method_call(
let prev_this =
ctx.block()
.call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &recv_box)]);
// Receiver-sensitive static `this` for plain class-ref receivers:
// `D.f()` resolving to a parent's body at compile time must run
// with `this === D` (the prologue's `js_static_this_resolve`
// consumes this one-shot arm). Dynamic-value receiver shapes
// (ClassExprFresh / factory Call / LocalGet) keep their prior
// implicit-this-only behavior to avoid disturbing effect's
// per-evaluation class-object statics.
let plain_class_receiver = matches!(
object.as_ref(),
Expr::ClassRef(_) | Expr::ExternFuncRef { .. }
);
if plain_class_receiver {
ctx.block()
.call_void("js_static_this_arm_value", &[(DOUBLE, &recv_box)]);
}
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|s| (DOUBLE, s.as_str())).collect();
let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices);
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,12 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
module.declare_function("js_implicit_this_get", DOUBLE, &[]);
module.declare_function("js_implicit_this_get_sloppy", DOUBLE, &[]);
module.declare_function("js_implicit_this_set", DOUBLE, &[DOUBLE]);
// Static-method prologue `this`: takes the one-shot receiver override
// armed by dynamic static dispatch / call/apply, else returns the
// lexical class-ref argument.
module.declare_function("js_static_this_resolve", DOUBLE, &[DOUBLE]);
module.declare_function("js_static_this_arm_classref", VOID, &[I32]);
module.declare_function("js_static_this_arm_value", VOID, &[DOUBLE]);
module.declare_function("js_ctor_return_override", DOUBLE, &[DOUBLE, DOUBLE, I32]);
module.declare_function("js_new_target_get", DOUBLE, &[]);
module.declare_function("js_new_target_set", DOUBLE, &[DOUBLE]);
Expand Down
124 changes: 124 additions & 0 deletions crates/perry-hir/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,130 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec<Local
///
/// Does NOT recurse into nested closures — those have their own `this`
/// binding and should keep referencing the outer class context.
/// Substitute every LEXICAL `this` in `expr` with `replacement` — including
/// inside arrow / `this`-capturing closure bodies (whose `this` is lexical),
/// but NOT inside ordinary function-expression closures (own dynamic `this`).
///
/// Used by the class-decl static-field-init inline emission: per
/// ClassDefinitionEvaluation a static initializer runs with `this` bound to
/// the class constructor, but the inline `StaticFieldSet` stmts evaluate in
/// module-init context where `this_stack` is empty and `Expr::This` would
/// read the module's implicit `this` (test262 class/elements
/// static-field-init-this-inside-arrow-function, class-name-static-initializer).
pub fn substitute_lexical_this_in_expr(expr: &mut Expr, replacement: &Expr) {
match expr {
Expr::This => *expr = replacement.clone(),
Expr::Closure {
body,
captures_this,
params,
..
} => {
if *captures_this {
for p in params.iter_mut() {
if let Some(d) = &mut p.default {
substitute_lexical_this_in_expr(d, replacement);
}
}
substitute_lexical_this_in_stmts(body, replacement);
// The body no longer reads `this`; drop the reserved capture
// slot so the closure-cache key doesn't include a stale
// implicit-this snapshot.
*captures_this = false;
}
}
_ => crate::walker::walk_expr_children_mut(expr, &mut |child| {
substitute_lexical_this_in_expr(child, replacement)
}),
}
}

pub fn substitute_lexical_this_in_stmts(stmts: &mut [Stmt], replacement: &Expr) {
for s in stmts {
substitute_lexical_this_in_stmt(s, replacement);
}
}

fn substitute_lexical_this_in_stmt(stmt: &mut Stmt, replacement: &Expr) {
let mut on_expr = |e: &mut Expr| substitute_lexical_this_in_expr(e, replacement);
match stmt {
Stmt::Let { init, .. } => {
if let Some(e) = init {
on_expr(e);
}
}
Stmt::Expr(e) | Stmt::Throw(e) => on_expr(e),
Stmt::Return(e) => {
if let Some(e) = e {
on_expr(e);
}
}
Stmt::If {
condition,
then_branch,
else_branch,
} => {
on_expr(condition);
substitute_lexical_this_in_stmts(then_branch, replacement);
if let Some(eb) = else_branch {
substitute_lexical_this_in_stmts(eb, replacement);
}
}
Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => {
on_expr(condition);
substitute_lexical_this_in_stmts(body, replacement);
}
Stmt::For {
init,
condition,
update,
body,
} => {
if let Some(i) = init {
substitute_lexical_this_in_stmt(i, replacement);
}
if let Some(c) = condition {
on_expr(c);
}
if let Some(u) = update {
on_expr(u);
}
substitute_lexical_this_in_stmts(body, replacement);
}
Stmt::Labeled { body, .. } => substitute_lexical_this_in_stmt(body, replacement),
Stmt::Try {
body,
catch,
finally,
} => {
substitute_lexical_this_in_stmts(body, replacement);
if let Some(c) = catch {
substitute_lexical_this_in_stmts(&mut c.body, replacement);
}
if let Some(f) = finally {
substitute_lexical_this_in_stmts(f, replacement);
}
}
Stmt::Switch {
discriminant,
cases,
} => {
on_expr(discriminant);
for case in cases {
if let Some(t) = &mut case.test {
substitute_lexical_this_in_expr(t, replacement);
}
substitute_lexical_this_in_stmts(&mut case.body, replacement);
}
}
Stmt::Break
| Stmt::Continue
| Stmt::LabeledBreak(_)
| Stmt::LabeledContinue(_)
| Stmt::PreallocateBoxes(_) => {}
}
}

pub fn replace_this_in_stmts(stmts: &mut Vec<Stmt>, this_id: LocalId) {
for s in stmts {
replace_this_in_stmt(s, this_id);
Expand Down
19 changes: 17 additions & 2 deletions crates/perry-hir/src/lower/for_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,23 @@ pub(crate) fn for_head_binding_stmts(
index: Box::new(lower_expr(ctx, &c.expr)?),
value: Box::new(source),
},
ast::MemberProp::PrivateName(_) => {
return Err(anyhow!("private member as for-loop head not supported"))
ast::MemberProp::PrivateName(p) => {
// `for (o.#f of iter)` — assign each iteration value to the
// private field, brand-guarding the receiver (write op) so a
// receiver without the field throws TypeError per spec
// (test262 elements/privatefieldset-typeerror-6/7).
let property = format!("#{}", p.name);
let object = crate::lower::expr_member::wrap_private_guard(
ctx,
object,
&property,
crate::lower::expr_member::PRIV_OP_WRITE,
);
Expr::PropertySet {
object,
property,
value: Box::new(source),
}
}
};
Ok(vec![Stmt::Expr(assign)])
Expand Down
Loading
Loading