From db4cafe4350beb746cd4fb370d813b8e5a280d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 10 Jun 2026 15:18:25 +0200 Subject: [PATCH] fix(hir,runtime): non-class expression-semantics remnant test262 parity Five surgical fixes across language/expressions, +9 test262, 0 regressions (verified same-mode against the branch-parent baseline): - super. / super[''] data-property reads in a class method now resolve through the *declared* prototype object (stable heap identity), so a property added to a parent prototype after the class declaration is found. Previously the older overloaded CLASS_PROTOTYPE_OBJECTS table held a distinct synthetic prototype that never saw such writes -> undefined. (super/prop-dot-cls-val, prop-dot-cls-val-from-arrow) - super[''] in a class method routes to the ident-form super read/call (SuperPropertyGet / SuperMethodCall) instead of the this[index] approximation that read the CHILD instance and shadowed the parent value. (super/prop-expr-cls-val, prop-expr-cls-val-from-arrow; guarded against a computed super-method-call regression via SuperMethodCall routing) - x instanceof undefined now evaluates the RHS and throws TypeError (RHS is not an object) instead of folding to false. (instanceof/S11.8.6_A3) - is_iterable() recognizes generator objects (own next/return/throw closures) and built-in iterator objects, so call / new / super spread of a generator (f(...g()), new C(...g())) and new Map/Set(g()) drive the iterator protocol instead of falling through to the array-reinterpret garbage path. Errors thrown by the generator body now propagate. (call/spread-err-*-expr-throws, new/spread-err-*-expr-throws) --- crates/perry-hir/src/lower/expr_call/mod.rs | 14 ++++++++++++++ crates/perry-hir/src/lower/expr_misc.rs | 15 ++++++++++++++- crates/perry-hir/src/lower/lower_expr.rs | 10 ++++++++++ crates/perry-runtime/src/collection_iter.rs | 18 ++++++++++++++++++ .../perry-runtime/src/object/property_key.rs | 12 +++++++++++- 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/crates/perry-hir/src/lower/expr_call/mod.rs b/crates/perry-hir/src/lower/expr_call/mod.rs index a6677af6d9..56f926a860 100644 --- a/crates/perry-hir/src/lower/expr_call/mod.rs +++ b/crates/perry-hir/src/lower/expr_call/mod.rs @@ -366,6 +366,20 @@ fn lower_call_inner(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Result { - let index = Box::new(lower_expr(ctx, &computed.expr)?); if let Some(home_id) = ctx.object_super_home_stack.last().copied() { + let index = Box::new(lower_expr(ctx, &computed.expr)?); Ok(Expr::ObjectSuperPropertyGet { home: Box::new(Expr::LocalGet(home_id)), key: index, receiver: Box::new(Expr::This), }) + } else if let Some(key) = match computed.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str().map(|s| s.to_string()), + _ => None, + } { + // `super['fromA']` in a CLASS method with a string-literal key: + // route through the same parent-prototype-chain lookup as the + // ident form `super.fromA` (Expr::SuperPropertyGet). The previous + // `this[index]` fallback read the property off the CHILD instance, + // shadowing the parent value (test262 + // super/prop-expr-cls-val{,-from-arrow}). A truly dynamic computed + // key (not a literal) still falls back below. + Ok(Expr::SuperPropertyGet { property: key }) } else { + let index = Box::new(lower_expr(ctx, &computed.expr)?); Ok(Expr::IndexGet { object: Box::new(Expr::This), index, diff --git a/crates/perry-hir/src/lower/lower_expr.rs b/crates/perry-hir/src/lower/lower_expr.rs index 7b8ac57b85..80269eeeb9 100644 --- a/crates/perry-hir/src/lower/lower_expr.rs +++ b/crates/perry-hir/src/lower/lower_expr.rs @@ -672,6 +672,16 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result< let ty_expr = match bin.right.as_ref() { ast::Expr::Ident(ident) => { let name = ident.sym.as_ref(); + // `x instanceof undefined`: `undefined` is the primitive + // value, never a class name. Codegen would resolve `ty = + // "undefined"` to class_id 0 and silently return `false`; + // ECMAScript requires evaluating the RHS and throwing a + // TypeError because it is not an object (test262 + // instanceof/S11.8.6_A3 #4). Lower it to the undefined + // value so it routes through `js_instanceof_dynamic`. + if name == "undefined" { + Some(Box::new(Expr::Undefined)) + } else // A local holding a class ref (drizzle's `is(value, type)`), // OR a top-level ES5 function constructor (`function Foo(){…}` // used as `x instanceof Foo`). The latter has no class entry, diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index c9d053f5fa..8c9d47cb89 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -115,6 +115,24 @@ pub(crate) fn is_iterable(value: f64) -> bool { if crate::map::is_registered_map(addr) || crate::set::is_registered_set(addr) { return true; } + // A built-in iterator object — an array/map/set/string/buffer/iterator- + // helper iterator — IS already an iterator and is therefore iterable + // (`[Symbol.iterator]()` returns itself). Mirrors the equivalent + // `is_builtin_iterator_class_id` arm in `js_get_iterator`. + if crate::array::is_builtin_iterator_class_id(addr) { + return true; + } + // A generator object (`g()` from `function* g(){}`) is lowered by Perry to a + // plain object with own closure-valued `next`/`return`/`throw` methods and + // NO `[Symbol.iterator]` symbol property, so the generic check below misses + // it. Generators ARE iterable (`[Symbol.iterator]()` returns themselves), so + // without this `js_array_like_to_array` — call / `new` / super spread of a + // generator (`f(...g())`, `new C(...g())`) and `new Map/Set(g())` — fell + // through to the array-reinterpret garbage path. `js_get_iterator` returns + // the generator unchanged (its own `.next()` drives the state machine). + if crate::object::js_util_types_is_generator_object(value).to_bits() == crate::value::TAG_TRUE { + return true; + } // Generic object: iterable iff it exposes a callable `[Symbol.iterator]`. if !crate::object::is_valid_obj_ptr(raw as *const u8) { return false; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index bb47e425f5..79b3903e9c 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -189,7 +189,17 @@ pub unsafe extern "C" fn js_super_accessor_get( } } } - let proto = crate::object::class_prototype_object(parent_class_id); + // Prefer the *declared* prototype object (stable heap identity). A dynamic + // write `Parent.prototype.foo = v` lands on that object, whereas the older + // overloaded `CLASS_PROTOTYPE_OBJECTS` table may hold a distinct synthetic + // prototype that never sees such writes — so reading through it returned + // `undefined` for data properties added to a parent prototype after the + // class declaration (test262 super/prop-{dot,expr}-cls-val). Falls back to + // the older table for synthetic-prototype sources that lack a decl entry. + let mut proto = crate::object::class_decl_prototype_object(parent_class_id); + if proto.is_null() { + proto = crate::object::class_prototype_object(parent_class_id); + } if !proto.is_null() { let target = crate::value::js_nanbox_pointer(proto as i64); return js_object_get_property_key(target, key);