From 4f0fb1a5b022b070245f07a73000f85ee75e982a Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Fri, 5 Jun 2026 01:17:58 +0200 Subject: [PATCH] js: implement class extends/super (isu issue 353) `super` was parsed as a bare identifier and the class compiler ignored `super_class` entirely, so `super()` threw, the subclass prototype chain was never linked (inherited methods were not found), and `super.method()` was unsupported. This wires up derived classes end to end: - Parser emits a dedicated `ExprKind::Super` (no longer a fake identifier). - New bytecode ops `SetHomeObject` and `LoadSuperBase` track each method's [[HomeObject]] and resolve the "super base" (the home object's prototype). - `emit_class_body` sets `Ctor.prototype.constructor`, records the constructor's home object, and for `extends` links both chains: `Ctor.__proto__ = Super` (static inheritance) and `Ctor.prototype.__proto__ = Super.prototype` (instance inheritance). - `super(...)` invokes the parent constructor (resolved via the super base's `.constructor`) with the current `this`; `super.m(...)` calls an inherited method with the current `this` as receiver. - A derived class with no explicit constructor gets a synthetic `constructor() { super(); }`. - Function objects gain a `[[Prototype]]` slot so subclass constructors inherit static members (walked in `get_function_property` and `gc_get_property`); `SetPrototype` now accepts function targets. - The home object propagates into call frames (and GC roots); the baseline JIT bails on the two new ops. Home object / function-prototype links are stored as non-enumerable internal slots (`__home_object__`, `__class_parent__`), matching the codebase's existing reserved-key convention, so the 35 `FunctionData` literals are untouched. Tests in crates/js/tests/class_super.rs cover the issue repro, inherited and overridden methods, `super.method`/`super` getter access, argument forwarding, default derived constructors, multi-level chains, static inheritance, prototype linkage, and class expressions. Co-Authored-By: Claude Opus 4.8 --- .isu/issues.json | 2 +- crates/js/src/ast.rs | 4 + crates/js/src/bytecode.rs | 20 +++ crates/js/src/compiler.rs | 282 ++++++++++++++++++++++++++------- crates/js/src/jit/compiler.rs | 6 +- crates/js/src/module.rs | 1 + crates/js/src/parser.rs | 2 +- crates/js/src/vm.rs | 190 ++++++++++++++++++++-- crates/js/tests/class_super.rs | 161 +++++++++++++++++++ 9 files changed, 594 insertions(+), 74 deletions(-) create mode 100644 crates/js/tests/class_super.rs diff --git a/.isu/issues.json b/.isu/issues.json index c0f0064..80087d6 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -4319,7 +4319,7 @@ ], "assigned": [], "author": "piefev", - "state": "open", + "state": "closed", "created_at": "2026-06-04T22:45:35Z" }, { diff --git a/crates/js/src/ast.rs b/crates/js/src/ast.rs index 4e213e2..5233de2 100644 --- a/crates/js/src/ast.rs +++ b/crates/js/src/ast.rs @@ -270,6 +270,10 @@ pub enum ExprKind { }, /// `new.target` NewTarget, + /// The `super` keyword. Only valid as the callee of a call (`super(...)`) + /// or the object of a member access (`super.method`). Used standalone it + /// evaluates to the home object's prototype (the "super base"). + Super, /// `object.property` or `object[property]` Member { object: Box, diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs index cd8629e..4aabda2 100644 --- a/crates/js/src/bytecode.rs +++ b/crates/js/src/bytecode.rs @@ -261,6 +261,13 @@ pub enum Op { /// install/merge a getter or setter accessor on a class constructor or its /// prototype. DefineAccessor = 0x67, + /// SetHomeObject fn_reg, obj_reg — record `obj` as the [[HomeObject]] of the + /// method/constructor function in `fn_reg`, so `super` references inside it + /// resolve against `obj`'s prototype. + SetHomeObject = 0x68, + /// LoadSuperBase dst — load the prototype of the current function's + /// [[HomeObject]] (the object `super` property lookups start from). + LoadSuperBase = 0x69, // ── Misc ──────────────────────────────────────────────── /// Delete dst, obj_reg, key_reg @@ -361,6 +368,8 @@ impl Op { 0x65 => Some(Op::SetPropertyByName), 0x66 => Some(Op::DefineMethod), 0x67 => Some(Op::DefineAccessor), + 0x68 => Some(Op::SetHomeObject), + 0x69 => Some(Op::LoadSuperBase), 0x70 => Some(Op::Delete), 0x71 => Some(Op::LoadInt8), 0x72 => Some(Op::ForInInit), @@ -1052,6 +1061,15 @@ impl Function { let k = if kind == 0 { "get" } else { "set" }; format!("DefineAccessor r{obj}, @{idx}(\"{name}\"), {k}, r{f}") } + Op::SetHomeObject => { + let f = read_reg_operand(code, &mut pc); + let obj = read_reg_operand(code, &mut pc); + format!("SetHomeObject r{f}, r{obj}") + } + Op::LoadSuperBase => { + let dst = read_reg_operand(code, &mut pc); + format!("LoadSuperBase r{dst}") + } Op::Delete => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); @@ -1219,6 +1237,8 @@ mod tests { Op::SetPropertyByName, Op::DefineMethod, Op::DefineAccessor, + Op::SetHomeObject, + Op::LoadSuperBase, Op::Delete, Op::LoadInt8, Op::ForInInit, diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 265c159..6f7ae4d 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -2496,6 +2496,36 @@ fn emit_class_body( fc.builder .emit_get_prop_name(proto_reg, ctor_reg, proto_name); + // `Ctor.prototype.constructor === Ctor` (non-enumerable). Beyond matching + // spec semantics, this is what `super()` resolution relies on: the derived + // constructor's super base is `Super.prototype`, whose `.constructor` is the + // parent constructor. + let ctor_key = fc.builder.add_name("constructor"); + fc.builder.emit_define_method(proto_reg, ctor_key, ctor_reg); + + // Record the constructor's [[HomeObject]] (its prototype) so `super(...)` + // and `super.x` inside the constructor resolve. + fc.builder + .emit_reg_reg(Op::SetHomeObject, ctor_reg, proto_reg); + + // `extends`: link the prototype chains so inherited members resolve. + if let Some(super_expr) = &class_def.super_class { + let super_reg = fc.alloc_reg(); + compile_expr(fc, super_expr, super_reg)?; + // `Ctor.__proto__ = Super` — inherit static members. + fc.builder + .emit_reg_reg(Op::SetPrototype, ctor_reg, super_reg); + // `Ctor.prototype.__proto__ = Super.prototype` — inherit instance members. + let super_proto_reg = fc.alloc_reg(); + let super_proto_name = fc.builder.add_name("prototype"); + fc.builder + .emit_get_prop_name(super_proto_reg, super_reg, super_proto_name); + fc.builder + .emit_reg_reg(Op::SetPrototype, proto_reg, super_proto_reg); + fc.free_reg(super_proto_reg); + fc.free_reg(super_reg); + } + for member in &class_def.body { match &member.kind { ClassMemberKind::Method { @@ -2564,24 +2594,45 @@ fn emit_class_body( /// instance is created. Classes without an explicit constructor get a synthetic /// empty one. fn build_constructor_def(class_def: &ClassDef) -> FunctionDef { - let mut ctor = class_def - .body - .iter() - .find_map(|m| match &m.kind { - ClassMemberKind::Method { - value, - kind: MethodKind::Constructor, - .. - } => Some(value.clone()), - _ => None, - }) - .unwrap_or(FunctionDef { - id: None, - params: Vec::new(), - body: Vec::new(), - is_async: false, - is_generator: false, - }); + let explicit = class_def.body.iter().find_map(|m| match &m.kind { + ClassMemberKind::Method { + value, + kind: MethodKind::Constructor, + .. + } => Some(value.clone()), + _ => None, + }); + let has_explicit = explicit.is_some(); + let mut ctor = explicit.unwrap_or(FunctionDef { + id: None, + params: Vec::new(), + body: Vec::new(), + is_async: false, + is_generator: false, + }); + + // A derived class with no explicit constructor gets a synthetic + // `constructor() { super(); }` so the parent constructor still runs. + // (Arguments are not forwarded; subclasses that need parent arguments + // should declare their own constructor.) + if !has_explicit { + if let Some(super_expr) = &class_def.super_class { + let span = super_expr.span; + ctor.body.push(Stmt { + kind: StmtKind::Expr(Expr { + kind: ExprKind::Call { + callee: Box::new(Expr { + kind: ExprKind::Super, + span, + }), + arguments: Vec::new(), + }, + span, + }), + span, + }); + } + } let mut field_inits = Vec::new(); for member in &class_def.body { @@ -2661,6 +2712,107 @@ fn number_key_to_string(n: f64) -> String { } } +/// Emit a call to `func_reg` with the current `this` (`this_reg`) as receiver, +/// compiling `arguments` into the contiguous block starting at the current +/// register top. Saves/restores the `this` global around the call so the callee +/// observes `this_reg`. The caller owns `func_reg`/`this_reg` and frees them +/// after this returns (this helper frees only the args and the saved-`this` +/// temporary it allocates). +fn emit_call_with_receiver( + fc: &mut FunctionCompiler, + func_reg: Reg, + this_reg: Reg, + arguments: &[Expr], + dst: Reg, +) -> Result<(), JsError> { + let this_ni = fc.builder.add_name("this"); + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } + let saved_this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(saved_this_reg, this_ni); + fc.builder.emit_store_global(this_ni, this_reg); + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + fc.builder.emit_store_global(this_ni, saved_this_reg); + fc.free_reg(saved_this_reg); + for _ in 0..arg_count { + fc.next_reg -= 1; + } + Ok(()) +} + +/// Compile `super(...)`: resolve the parent constructor (the super base's +/// `.constructor`) and invoke it with the current `this` as receiver so the +/// parent constructor initializes the same instance. +fn compile_super_call( + fc: &mut FunctionCompiler, + arguments: &[Expr], + dst: Reg, +) -> Result<(), JsError> { + let base_reg = fc.alloc_reg(); + fc.builder.emit_reg(Op::LoadSuperBase, base_reg); + let func_reg = fc.alloc_reg(); + let ctor_ni = fc.builder.add_name("constructor"); + fc.builder.emit_get_prop_name(func_reg, base_reg, ctor_ni); + + let this_ni = fc.builder.add_name("this"); + let this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(this_reg, this_ni); + + emit_call_with_receiver(fc, func_reg, this_reg, arguments, dst)?; + + fc.free_reg(this_reg); + fc.free_reg(func_reg); + fc.free_reg(base_reg); + Ok(()) +} + +/// Compile `super.method(...)`: look the method up on the super base (the home +/// object's prototype) and call it with the current `this` as receiver. +fn compile_super_method_call( + fc: &mut FunctionCompiler, + property: &Expr, + computed: bool, + arguments: &[Expr], + dst: Reg, +) -> Result<(), JsError> { + let base_reg = fc.alloc_reg(); + fc.builder.emit_reg(Op::LoadSuperBase, base_reg); + let func_reg = fc.alloc_reg(); + if !computed { + if let ExprKind::Identifier(name) = &property.kind { + let ni = fc.builder.add_name(name); + fc.builder.emit_get_prop_name(func_reg, base_reg, ni); + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder + .emit_reg3(Op::GetProperty, func_reg, base_reg, key_reg); + fc.free_reg(key_reg); + } + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder + .emit_reg3(Op::GetProperty, func_reg, base_reg, key_reg); + fc.free_reg(key_reg); + } + + let this_ni = fc.builder.add_name("this"); + let this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(this_reg, this_ni); + + emit_call_with_receiver(fc, func_reg, this_reg, arguments, dst)?; + + fc.free_reg(this_reg); + fc.free_reg(func_reg); + fc.free_reg(base_reg); + Ok(()) +} + // ── Export ─────────────────────────────────────────────────── fn compile_export( @@ -3052,6 +3204,14 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), fc.builder.emit_reg(Op::LoadUndefined, dst); } + ExprKind::Super => { + // Bare `super` is only meaningful as `super(...)` or `super.x`, + // which are handled in the Call/Member arms. Used elsewhere it + // evaluates to the super base (the home object's prototype), which + // is also what a `super.x` member read or store target needs. + fc.builder.emit_reg(Op::LoadSuperBase, dst); + } + ExprKind::Binary { op, left, right } => { let lhs = fc.alloc_reg(); compile_expr(fc, left, lhs)?; @@ -3260,22 +3420,38 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Call { callee, arguments } => { - // Detect method calls (obj.method()) to set `this`. - if let ExprKind::Member { + // `super(...)`: invoke the parent constructor on the current `this`. + if matches!(callee.kind, ExprKind::Super) { + compile_super_call(fc, arguments, dst)?; + } + // `super.method(...)`: call an inherited method with the current + // `this` as receiver (not the super base). + else if let ExprKind::Member { object, property, computed, } = &callee.kind { - // Layout: [obj_reg] [func_reg] [arg0] [arg1] ... - // We keep obj_reg alive so we can set `this` before the call. - let obj_reg = fc.alloc_reg(); - compile_expr(fc, object, obj_reg)?; - let func_reg = fc.alloc_reg(); - if !computed { - if let ExprKind::Identifier(name) = &property.kind { - let ni = fc.builder.add_name(name); - fc.builder.emit_get_prop_name(func_reg, obj_reg, ni); + if matches!(object.kind, ExprKind::Super) { + compile_super_method_call(fc, property, *computed, arguments, dst)?; + } else { + // Detect method calls (obj.method()) to set `this`. + // Layout: [obj_reg] [func_reg] [arg0] [arg1] ... + // We keep obj_reg alive so we can set `this` before the call. + let obj_reg = fc.alloc_reg(); + compile_expr(fc, object, obj_reg)?; + let func_reg = fc.alloc_reg(); + if !computed { + if let ExprKind::Identifier(name) = &property.kind { + let ni = fc.builder.add_name(name); + fc.builder.emit_get_prop_name(func_reg, obj_reg, ni); + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder + .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg); + fc.free_reg(key_reg); + } } else { let key_reg = fc.alloc_reg(); compile_expr(fc, property, key_reg)?; @@ -3283,38 +3459,32 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg); fc.free_reg(key_reg); } - } else { - let key_reg = fc.alloc_reg(); - compile_expr(fc, property, key_reg)?; - fc.builder - .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg); - fc.free_reg(key_reg); - } - let args_start = fc.next_reg; - let arg_count = arguments.len().min(255) as u8; - for arg in arguments { - let arg_reg = fc.alloc_reg(); - compile_expr(fc, arg, arg_reg)?; - } + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } - // Set `this` after compiling arguments so nested method calls - // inside the argument list cannot clobber the receiver. - let this_ni = fc.builder.add_name("this"); - let saved_this_reg = fc.alloc_reg(); - fc.builder.emit_load_global(saved_this_reg, this_ni); - fc.builder.emit_store_global(this_ni, obj_reg); + // Set `this` after compiling arguments so nested method calls + // inside the argument list cannot clobber the receiver. + let this_ni = fc.builder.add_name("this"); + let saved_this_reg = fc.alloc_reg(); + fc.builder.emit_load_global(saved_this_reg, this_ni); + fc.builder.emit_store_global(this_ni, obj_reg); - fc.builder.emit_call(dst, func_reg, args_start, arg_count); - fc.builder.emit_store_global(this_ni, saved_this_reg); + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + fc.builder.emit_store_global(this_ni, saved_this_reg); - // Free in LIFO order: saved this, args, func_reg, obj_reg. - fc.free_reg(saved_this_reg); - for _ in 0..arg_count { - fc.next_reg -= 1; + // Free in LIFO order: saved this, args, func_reg, obj_reg. + fc.free_reg(saved_this_reg); + for _ in 0..arg_count { + fc.next_reg -= 1; + } + fc.free_reg(func_reg); + fc.free_reg(obj_reg); } - fc.free_reg(func_reg); - fc.free_reg(obj_reg); } else { let func_reg = fc.alloc_reg(); compile_expr(fc, callee, func_reg)?; diff --git a/crates/js/src/jit/compiler.rs b/crates/js/src/jit/compiler.rs index b2baf15..37f3159 100644 --- a/crates/js/src/jit/compiler.rs +++ b/crates/js/src/jit/compiler.rs @@ -641,6 +641,8 @@ impl BaselineJit { | Op::Delete | Op::DefineMethod | Op::DefineAccessor + | Op::SetHomeObject + | Op::LoadSuperBase | Op::ForInInit | Op::ForInNext | Op::SetPrototype @@ -785,6 +787,7 @@ fn instruction_operand_size(op: Op) -> usize { | Op::CreateObject | Op::CreateArray | Op::NewCell + | Op::LoadSuperBase | Op::BuildArguments => 2, // 4 bytes: two registers, or one register plus one u16 upvalue index @@ -802,7 +805,8 @@ fn instruction_operand_size(op: Op) -> usize { | Op::Await | Op::Spread | Op::SetPrototype - | Op::GetPrototype => 4, + | Op::GetPrototype + | Op::SetHomeObject => 4, // 3 bytes: one register plus i8 Op::LoadInt8 => 3, diff --git a/crates/js/src/module.rs b/crates/js/src/module.rs index e10191b..1a2c4fb 100644 --- a/crates/js/src/module.rs +++ b/crates/js/src/module.rs @@ -226,6 +226,7 @@ fn tla_in_expr(expr: &Expr) -> bool { | ExprKind::Identifier(_) | ExprKind::This | ExprKind::NewTarget + | ExprKind::Super | ExprKind::RegExp { .. } => false, ExprKind::Unary { argument, .. } | ExprKind::Update { argument, .. } => { tla_in_expr(argument) diff --git a/crates/js/src/parser.rs b/crates/js/src/parser.rs index cb254e6..a3faf77 100644 --- a/crates/js/src/parser.rs +++ b/crates/js/src/parser.rs @@ -2101,7 +2101,7 @@ impl Parser { TokenKind::Super => { self.advance(); Ok(Expr { - kind: ExprKind::Identifier("super".into()), + kind: ExprKind::Super, span: self.span_from(start), }) } diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 357bd65..b8712d5 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -996,7 +996,12 @@ fn gc_get_property(gc: &Gc, obj_ref: GcRef, key: &str, shapes: &Shap } return Value::Undefined; } - None + // Walk the function [[Prototype]] chain so subclass constructors + // inherit static members from their parent (`class B extends A`). + match fdata.properties.get(CLASS_PARENT_KEY).map(|p| &p.value) { + Some(Value::Function(r)) | Some(Value::Object(r)) => Some(*r), + _ => None, + } } _ => return Value::Undefined, } @@ -1044,6 +1049,15 @@ fn function_call_native(args: &[Value], ctx: &mut NativeContext) -> Result, + /// The [[HomeObject]] of the function running in this frame, if it is a + /// class method or constructor. `super` references resolve against this + /// object's prototype. + home_object: Option, /// Actual argument values passed at the call site, preserved so the /// `arguments` object (materialized via `Op::BuildArguments`) can include /// every passed value — not just the named parameters. Empty for the @@ -1924,6 +1942,7 @@ impl Vm { exception_handlers: Vec::new(), upvalues: Vec::new(), construct_this: None, + home_object: None, args: Vec::new(), }); @@ -2208,6 +2227,7 @@ impl Vm { exception_handlers: Vec::new(), upvalues, construct_this: None, + home_object: function_home_object(&self.gc, func_ref), args: args.to_vec(), }); @@ -2865,6 +2885,7 @@ impl Vm { exception_handlers, upvalues, construct_this: None, + home_object: None, args, }); @@ -2991,6 +3012,7 @@ impl Vm { exception_handlers, upvalues, construct_this: None, + home_object: None, args, }); @@ -3575,6 +3597,9 @@ impl Vm { if let Some(this_ref) = frame.construct_this { roots.push(this_ref); } + if let Some(home) = frame.home_object { + roots.push(home); + } } // Built-in prototype roots. if let Some(r) = self.object_prototype { @@ -4056,6 +4081,12 @@ impl Vm { FunctionKind::Bytecode(bc) => CallInfo::Bytecode( Box::new(bc.func.clone()), fdata.upvalues.clone(), + match fdata.properties.get(HOME_OBJECT_KEY).map(|p| &p.value) { + Some(Value::Object(r)) | Some(Value::Function(r)) => { + Some(*r) + } + _ => None, + }, ), }, _ => { @@ -4424,7 +4455,7 @@ impl Vm { } } } - CallInfo::Bytecode(callee_func, callee_upvalues) => { + CallInfo::Bytecode(callee_func, callee_upvalues, callee_home) => { let callee_func = *callee_func; // Async function: create generator + promise, drive async. if callee_func.is_async && !callee_func.is_generator { @@ -4514,6 +4545,7 @@ impl Vm { exception_handlers: Vec::new(), upvalues: callee_upvalues.clone(), construct_this, + home_object: callee_home, args: args.clone(), }); @@ -4567,6 +4599,7 @@ impl Vm { exception_handlers: Vec::new(), upvalues: callee_upvalues, construct_this, + home_object: callee_home, args, }); } @@ -5125,6 +5158,14 @@ impl Vm { let base = self.frames[fi].base; let name = self.frames[fi].func.names[name_idx].clone(); let method = self.registers[base + fn_r as usize].clone(); + // Record the target as the method's [[HomeObject]] so `super` + // inside the method resolves against the target's prototype. + let target = self.registers[base + obj_r as usize].clone(); + if let Value::Function(method_ref) = method { + if let Value::Object(_) | Value::Function(_) = target { + self.set_function_home_object(method_ref, target.clone()); + } + } let prop = Property { value: method, getter: None, @@ -5133,7 +5174,7 @@ impl Vm { enumerable: false, configurable: true, }; - match self.registers[base + obj_r as usize] { + match target { Value::Object(obj_ref) => { if let Some(HeapObject::Object(data)) = self.gc.get_mut(obj_ref) { data.insert_property(name, prop, &mut self.shapes); @@ -5158,7 +5199,13 @@ impl Vm { Value::Function(r) => Some(r), _ => None, }; - match self.registers[base + obj_r as usize] { + let target = self.registers[base + obj_r as usize].clone(); + if let (Some(fn_ref), Value::Object(_) | Value::Function(_)) = + (accessor_fn, &target) + { + self.set_function_home_object(fn_ref, target.clone()); + } + match target { Value::Object(obj_ref) => { // Merge with an existing accessor on the same object so // a `get x` / `set x` pair shares one property. @@ -5296,15 +5343,33 @@ impl Vm { let obj_r = Self::read_reg(&mut self.frames[fi]); let proto_r = Self::read_reg(&mut self.frames[fi]); let base = self.frames[fi].base; - let proto = match &self.registers[base + proto_r as usize] { - Value::Object(r) => Some(*r), - Value::Null => None, + let proto_val = self.registers[base + proto_r as usize].clone(); + let proto = match &proto_val { + Value::Object(r) | Value::Function(r) => Some(*r), _ => None, }; - if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { - if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { - data.prototype = proto; + match self.registers[base + obj_r as usize] { + Value::Object(gc_ref) => { + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { + data.prototype = proto; + } } + // A function's [[Prototype]] is not a plain field; store it + // as an internal slot so static-member inheritance works + // (`class B extends A` makes `B.__proto__ === A`). + Value::Function(fn_ref) => match proto { + Some(_) => self.set_function_internal_slot( + fn_ref, + CLASS_PARENT_KEY, + proto_val.clone(), + ), + None => { + if let Some(HeapObject::Function(fdata)) = self.gc.get_mut(fn_ref) { + fdata.properties.remove(CLASS_PARENT_KEY); + } + } + }, + _ => {} } } Op::GetPrototype => { @@ -5322,6 +5387,43 @@ impl Vm { }; self.registers[base + dst as usize] = proto; } + Op::SetHomeObject => { + let fn_r = Self::read_reg(&mut self.frames[fi]); + let obj_r = Self::read_reg(&mut self.frames[fi]); + let base = self.frames[fi].base; + let home = self.registers[base + obj_r as usize].clone(); + if let Value::Function(fn_ref) = self.registers[base + fn_r as usize] { + if matches!(home, Value::Object(_) | Value::Function(_)) { + self.set_function_home_object(fn_ref, home); + } + } + } + Op::LoadSuperBase => { + let dst = Self::read_reg(&mut self.frames[fi]); + let base = self.frames[fi].base; + // `super` resolves against the prototype of the running + // function's [[HomeObject]]: for an instance method the home + // object is the class prototype (so the base is the parent's + // prototype); for a static method it is the constructor (so the + // base is the parent constructor). + let result = match self.frames[fi].home_object { + Some(home_ref) => match self.gc.get(home_ref) { + Some(HeapObject::Object(data)) => data + .prototype + .map(Value::Object) + .unwrap_or(Value::Undefined), + Some(HeapObject::Function(fdata)) => { + match fdata.properties.get(CLASS_PARENT_KEY).map(|p| &p.value) { + Some(v @ (Value::Function(_) | Value::Object(_))) => v.clone(), + _ => Value::Undefined, + } + } + _ => Value::Undefined, + }, + None => Value::Undefined, + }; + self.registers[base + dst as usize] = result; + } // ── Exception handling ───────────────────────────── Op::PushExceptionHandler => { @@ -5585,6 +5687,29 @@ impl Vm { self.gc.alloc(HeapObject::Object(obj)) } + /// Store a non-enumerable, non-writable internal slot on a function object. + /// Used for `super`-related bookkeeping (`__home_object__`, `__class_parent__`). + fn set_function_internal_slot(&mut self, func_ref: GcRef, key: &str, value: Value) { + if let Some(HeapObject::Function(fdata)) = self.gc.get_mut(func_ref) { + fdata.properties.insert( + key.to_string(), + Property { + value, + getter: None, + setter: None, + writable: false, + enumerable: false, + configurable: false, + }, + ); + } + } + + /// Record `home` as `func_ref`'s [[HomeObject]] for `super` resolution. + fn set_function_home_object(&mut self, func_ref: GcRef, home: Value) { + self.set_function_internal_slot(func_ref, HOME_OBJECT_KEY, home); + } + fn constructor_return_value(construct_this: Option, value: Value) -> Value { match construct_this { Some(_) if matches!(value, Value::Object(_) | Value::Function(_)) => value, @@ -5708,6 +5833,20 @@ impl Vm { .property_value(prop, Value::Function(func_ref)) .unwrap_or(Value::Undefined); } + // Static-member inheritance: a subclass constructor's [[Prototype]] is + // its parent constructor (`class B extends A` → `B.__proto__ === A`). + let parent = match self.gc.get(func_ref) { + Some(HeapObject::Function(fdata)) => { + match fdata.properties.get(CLASS_PARENT_KEY).map(|p| &p.value) { + Some(Value::Function(r)) | Some(Value::Object(r)) => Some(*r), + _ => None, + } + } + _ => None, + }; + if let Some(parent_ref) = parent { + return self.get_function_property(parent_ref, key); + } if let Some(value) = self.function_builtin_method(key) { return value; } @@ -6290,9 +6429,14 @@ impl Vm { match self.gc.get(func_gc_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { FunctionKind::Native(n) => CallInfo::Native(n.callback), - FunctionKind::Bytecode(bc) => { - CallInfo::Bytecode(Box::new(bc.func.clone()), fdata.upvalues.clone()) - } + FunctionKind::Bytecode(bc) => CallInfo::Bytecode( + Box::new(bc.func.clone()), + fdata.upvalues.clone(), + match fdata.properties.get(HOME_OBJECT_KEY).map(|p| &p.value) { + Some(Value::Object(r)) | Some(Value::Function(r)) => Some(*r), + _ => None, + }, + ), }, _ => return Err(RuntimeError::type_error("not a function")), } @@ -6332,7 +6476,7 @@ impl Vm { Err(e) => Err(e), } } - CallInfo::Bytecode(callee_func, callee_upvalues) => { + CallInfo::Bytecode(callee_func, callee_upvalues, callee_home) => { let callee_func = *callee_func; if callee_func.is_generator || callee_func.is_async { // Generators and async functions need special handling. @@ -6389,6 +6533,7 @@ impl Vm { exception_handlers: Vec::new(), upvalues: callee_upvalues, construct_this: None, + home_object: callee_home, args, }); @@ -6575,7 +6720,22 @@ impl Default for Vm { /// Internal enum to avoid holding a GC borrow across the call setup. enum CallInfo { Native(fn(&[Value], &mut NativeContext) -> Result), - Bytecode(Box, Vec), + Bytecode(Box, Vec, Option), +} + +/// Read a function's recorded [[HomeObject]] (set via [`Op::SetHomeObject`] or +/// `DefineMethod`), used to propagate `super` resolution into the callee frame. +fn function_home_object(gc: &Gc, func_ref: GcRef) -> Option { + match gc.get(func_ref) { + Some(HeapObject::Function(fdata)) => match fdata.properties.get(HOME_OBJECT_KEY) { + Some(prop) => match &prop.value { + Value::Object(r) | Value::Function(r) => Some(*r), + _ => None, + }, + None => None, + }, + _ => None, + } } // ── Generator native callbacks ────────────────────────────── diff --git a/crates/js/tests/class_super.rs b/crates/js/tests/class_super.rs new file mode 100644 index 0000000..a718c07 --- /dev/null +++ b/crates/js/tests/class_super.rs @@ -0,0 +1,161 @@ +//! Regression tests for `class extends` / `super` (isu issue 353). +//! +//! Before this work `super` parsed as a bare identifier and the class compiler +//! ignored `super_class` entirely, so `super()` threw, the subclass prototype +//! chain was never linked (inherited methods were not found), and `super.m()` +//! was unsupported. + +use we_js::evaluate; + +#[test] +fn issue_repro_super_constructor_and_inherited_field() { + // The exact repro from isu issue 353. + let out = evaluate( + "class D{constructor(){this.x=1;}} \ + class E extends D{constructor(){super();this.z=2;} f(){return this.x+this.z;}} \ + new E().f();", + ) + .unwrap(); + assert_eq!(out, "3"); +} + +#[test] +fn inherited_instance_method_is_found() { + let out = evaluate( + "class A { greet() { return 'hi'; } } \ + class B extends A {} \ + new B().greet();", + ) + .unwrap(); + assert_eq!(out, "hi"); +} + +#[test] +fn super_method_call_chains_to_parent() { + let out = evaluate( + "class A { m() { return 1; } } \ + class B extends A { m() { return super.m() + 1; } } \ + new B().m();", + ) + .unwrap(); + assert_eq!(out, "2"); +} + +#[test] +fn super_method_uses_current_this() { + let out = evaluate( + "class A { who() { return this.tag; } } \ + class B extends A { constructor() { super(); this.tag = 'B'; } w() { return super.who(); } } \ + new B().w();", + ) + .unwrap(); + assert_eq!(out, "B"); +} + +#[test] +fn instanceof_walks_inherited_prototype_chain() { + let out = evaluate( + "class A {} class B extends A {} \ + var b = new B(); (b instanceof B) + ',' + (b instanceof A);", + ) + .unwrap(); + assert_eq!(out, "true,true"); +} + +#[test] +fn prototype_chain_is_linked() { + let out = evaluate( + "class A {} class B extends A {} \ + (Object.getPrototypeOf(B.prototype) === A.prototype);", + ) + .unwrap(); + assert_eq!(out, "true"); +} + +#[test] +fn prototype_constructor_points_back_to_class() { + let out = evaluate( + "class A {} class B extends A {} \ + (B.prototype.constructor === B) + ',' + (A.prototype.constructor === A);", + ) + .unwrap(); + assert_eq!(out, "true,true"); +} + +#[test] +fn super_forwards_arguments() { + let out = evaluate( + "class A { constructor(x) { this.x = x; } } \ + class B extends A { constructor() { super(7); this.y = 1; } } \ + var o = new B(); o.x + ',' + o.y;", + ) + .unwrap(); + assert_eq!(out, "7,1"); +} + +#[test] +fn default_derived_constructor_runs_parent_init() { + let out = evaluate( + "class A { constructor() { this.a = 1; } } \ + class B extends A { b = 2; } \ + var o = new B(); o.a + ',' + o.b;", + ) + .unwrap(); + assert_eq!(out, "1,2"); +} + +#[test] +fn static_members_are_inherited() { + let out = evaluate( + "class A { static s() { return 5; } } \ + class B extends A {} \ + B.s();", + ) + .unwrap(); + assert_eq!(out, "5"); +} + +#[test] +fn three_level_inheritance_chains_super() { + let out = evaluate( + "class A { constructor() { this.a = 1; } } \ + class B extends A { constructor() { super(); this.b = 2; } } \ + class C extends B { constructor() { super(); this.c = 3; } } \ + var o = new C(); o.a + o.b + o.c;", + ) + .unwrap(); + assert_eq!(out, "6"); +} + +#[test] +fn class_expression_extends_links_chain() { + let out = evaluate( + "var A = class { m() { return 'a'; } }; \ + var B = class extends A {}; \ + new B().m();", + ) + .unwrap(); + assert_eq!(out, "a"); +} + +#[test] +fn super_property_read_in_method() { + let out = evaluate( + "class A { get label() { return 'L'; } } \ + class B extends A { read() { return super.label; } } \ + new B().read();", + ) + .unwrap(); + assert_eq!(out, "L"); +} + +#[test] +fn overridden_method_dispatches_to_subclass() { + let out = evaluate( + "class A { name() { return 'A'; } describe() { return 'I am ' + this.name(); } } \ + class B extends A { name() { return 'B'; } } \ + new B().describe();", + ) + .unwrap(); + assert_eq!(out, "I am B"); +} -- 2.51.2