From 5333be462291f03900d4c2fb08246994ed99b0be Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 9 Jun 2026 08:38:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=9F=BA=E7=A1=80=20?= =?UTF-8?q?class=20=E5=A3=B0=E6=98=8E=E8=A1=A8=E8=BE=BE=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 36 +++++ crates/oxide_compiler/src/counter.rs | 42 +++++- crates/oxide_compiler/src/emitter.rs | 113 ++++++++++++++- crates/oxide_compiler/src/hash.rs | 69 ++++++++- crates/oxide_compiler/src/module.rs | 5 + .../tests/compile_expr_tests.rs | 24 +++ .../tests/compile_stmt_tests.rs | 34 +++++ crates/oxide_types/src/object.rs | 15 ++ crates/oxide_vm/src/builtins/error.rs | 2 + crates/oxide_vm/src/vm.rs | 137 +++++++++++++++--- crates/oxide_vm/tests/class_tests.rs | 70 +++++++++ crates/oxide_vm/tests/function_tests.rs | 15 +- crates/oxide_vm/tests/regression_tests.rs | 4 +- 13 files changed, 535 insertions(+), 31 deletions(-) create mode 100644 crates/oxide_vm/tests/class_tests.rs diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 3340e283..b741ffb6 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -259,12 +259,35 @@ impl Compiler { Self } + pub(crate) fn extract_function_parts<'a>( + &self, function: &'a oxide_parser::Function<'a>, + ) -> Result<(Vec, &'a [Statement<'a>]), String> { + let mut param_names = Vec::new(); + for param in &function.params.items { + match ¶m.pattern { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + param_names.push(bi.name.to_string()); + } + _ => return Err("destructuring parameters not supported".into()), + } + } + let body_stmts: &[Statement] = if let Some(body) = &function.body { &body.statements } else { &[] }; + Ok((param_names, body_stmts)) + } + /// Compile a function body (used for FD, FE, and arrow functions). /// This performs both counting and emitting in one pass. /// When `is_expression_body` is true (arrow function with expression body), /// the last expression's value is returned instead of undefined. pub(crate) fn compile_function_body<'a>( &self, param_names: &[String], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, is_expression_body: bool, + ) -> Result { + self.compile_function_body_with_bindings(param_names, body_stmts, parent_ctx, is_expression_body, &[]) + } + + pub(crate) fn compile_function_body_with_bindings<'a>( + &self, param_names: &[String], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, is_expression_body: bool, + extra_bindings: &[(&str, u8)], ) -> Result { let mut ctx = CompileCtx::new(); @@ -289,6 +312,17 @@ impl Compiler { ); inherited_reg_start = inherited_reg_start.max(binding.reg.saturating_add(1)); } + for (name, reg) in extra_bindings { + ctx.symbols.scopes[0].bindings.insert( + (*name).to_string(), + Binding { + reg: *reg, + initialized: true, + is_const: true, + }, + ); + inherited_reg_start = inherited_reg_start.max(reg.saturating_add(1)); + } ctx.reserved_reg_start = inherited_reg_start; // Align next_reg with builtin count so both count and emit passes start at the @@ -367,6 +401,7 @@ impl Compiler { is_arrow: false, captured_this_const_idx: 0, function_name: None, + is_class_constructor: false, }) } @@ -424,6 +459,7 @@ impl Compiler { is_arrow: false, captured_this_const_idx: 0, function_name: None, + is_class_constructor: false, }) } } diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index ac386bb5..7c21c0d4 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -1,11 +1,41 @@ use oxide_parser::{ - AssignmentOperator, Expression, ForStatementInit, SimpleAssignmentTarget, Statement, UnaryOperator, - VariableDeclarationKind, + AssignmentOperator, ClassElement, Expression, ForStatementInit, MethodDefinitionKind, SimpleAssignmentTarget, + Statement, UnaryOperator, VariableDeclarationKind, }; use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label}; impl Compiler { + fn count_class(&self, class: &oxide_parser::Class, ctx: &mut CompileCtx) { + ctx.alloc_reg(); // ctor_reg + ctx.alloc_reg(); // proto_reg + ctx.projected_pc += 2; // LOAD_CONST ctor + NEW_OBJECT proto + + for element in &class.body.body { + let ClassElement::MethodDefinition(method) = element else { + continue; + }; + let method = method.as_ref(); + match method.kind { + MethodDefinitionKind::Constructor => {} + MethodDefinitionKind::Method => { + ctx.alloc_reg(); // method_reg + ctx.alloc_reg(); // key_reg + ctx.projected_pc += 3; // LOAD_CONST method + LOAD_CONST key + SET_PROP + } + MethodDefinitionKind::Get | MethodDefinitionKind::Set => {} + } + } + + ctx.alloc_reg(); // constructor key + ctx.projected_pc += 1; // LOAD_CONST "constructor" + ctx.projected_pc += 1; // SET_PROP proto.constructor = ctor + + ctx.alloc_reg(); // prototype key + ctx.projected_pc += 1; // LOAD_CONST "prototype" + ctx.projected_pc += 1; // SET_PROP ctor.prototype = proto + } + pub(crate) fn count_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) { match stmt { Statement::ExpressionStatement(es) => { @@ -198,6 +228,11 @@ impl Compiler { // FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR ctx.projected_pc += 2; } + Statement::ClassDeclaration(class) => { + ctx.alloc_reg(); // class binding reg + self.count_class(class, ctx); + ctx.projected_pc += 1; // STORE_VAR binding <- ctor + } Statement::ThrowStatement(ts) => { self.count_expression(&ts.argument, ctx); ctx.projected_pc += 1; // THROW @@ -484,6 +519,9 @@ impl Compiler { ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_CONST } + Expression::ClassExpression(class) => { + self.count_class(class, ctx); + } Expression::NewExpression(ne) => { // Count callee expression self.count_expression(&ne.callee, ctx); diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index f9fd2ed1..2950aeae 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -1,14 +1,109 @@ use crate::compiler::Label; use crate::opcode::{self, OpCode}; use oxide_parser::{ - AssignmentOperator, Expression, ForStatementInit, ForStatementLeft, SimpleAssignmentTarget, Statement, - UnaryOperator, UpdateOperator, VariableDeclarationKind, + AssignmentOperator, Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, MethodDefinitionKind, + PropertyKey, SimpleAssignmentTarget, Statement, UnaryOperator, UpdateOperator, VariableDeclarationKind, }; use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler}; use crate::module::Constant; impl Compiler { + fn class_property_name(&self, key: &PropertyKey) -> Result { + match key { + PropertyKey::StaticIdentifier(ident) => Ok(ident.name.as_str().to_string()), + PropertyKey::StringLiteral(s) => Ok(s.value.to_string()), + PropertyKey::PrivateIdentifier(_) => Err("private class elements not yet supported".into()), + _ => Err("unsupported class property key type".into()), + } + } + + fn emit_class(&self, class: &Class, ctx: &mut CompileCtx) -> Result { + if class.super_class.is_some() { + return Err("class extends not yet supported".into()); + } + + let mut constructor_method = None; + let mut instance_methods = Vec::new(); + + for element in &class.body.body { + let ClassElement::MethodDefinition(method) = element else { + return Err("class fields/accessors/static blocks not yet supported".into()); + }; + let method = method.as_ref(); + if method.r#static { + return Err("static methods not yet supported".into()); + } + if method.computed { + return Err("computed class methods not yet supported".into()); + } + + match method.kind { + MethodDefinitionKind::Constructor => { + if constructor_method.is_some() { + return Err("duplicate class constructor".into()); + } + constructor_method = Some(method); + } + MethodDefinitionKind::Method => { + let name = self.class_property_name(&method.key)?; + instance_methods.push((method, name)); + } + MethodDefinitionKind::Get | MethodDefinitionKind::Set => { + return Err("getters/setters not yet supported".into()); + } + } + } + + let ctor_name = class.id.as_ref().map(|id| id.name.to_string()); + let ctor_reg = ctx.alloc_reg(); + let proto_reg = ctx.alloc_reg(); + let self_binding = ctor_name.as_deref().map(|name| vec![(name, ctor_reg)]).unwrap_or_default(); + + let mut ctor_module = if let Some(method) = constructor_method { + let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; + self.compile_function_body_with_bindings(¶m_names, body_stmts, ctx, false, &self_binding)? + } else { + self.compile_function_body_with_bindings(&[], &[], ctx, false, &self_binding)? + }; + ctor_module.is_class_constructor = true; + ctor_module.function_name = ctor_name.clone(); + ctx.sub_modules.push(ctor_module); + + let ctor_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); + ctx.emit_load_const(ctor_reg, ctor_idx); + ctx.emit(opcode::encode(OpCode::NEW_OBJECT, proto_reg, 0, 0)); + + for (method, method_name) in instance_methods { + let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; + let mut method_module = + self.compile_function_body_with_bindings(¶m_names, body_stmts, ctx, false, &self_binding)?; + method_module.function_name = Some(method_name.clone()); + ctx.sub_modules.push(method_module); + + let method_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); + let method_reg = ctx.alloc_reg(); + ctx.emit_load_const(method_reg, method_idx); + + let key_idx = ctx.add_constant(Constant::String(method_name)); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, key_idx); + ctx.emit(opcode::encode(OpCode::SET_PROP, proto_reg, method_reg, key_reg)); + } + + let ctor_key_idx = ctx.add_constant(Constant::String("constructor".to_string())); + let ctor_key_reg = ctx.alloc_reg(); + ctx.emit_load_const(ctor_key_reg, ctor_key_idx); + ctx.emit(opcode::encode(OpCode::SET_PROP, proto_reg, ctor_reg, ctor_key_reg)); + + let proto_key_idx = ctx.add_constant(Constant::String("prototype".to_string())); + let proto_key_reg = ctx.alloc_reg(); + ctx.emit_load_const(proto_key_reg, proto_key_idx); + ctx.emit(opcode::encode(OpCode::SET_PROP, ctor_reg, proto_reg, proto_key_reg)); + + Ok(ctor_reg) + } + pub(crate) fn emit_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) -> Result, String> { match stmt { Statement::ExpressionStatement(es) => Ok(Some(self.emit_expression(&es.expression, ctx)?)), @@ -348,6 +443,19 @@ impl Compiler { Ok(None) } + Statement::ClassDeclaration(class) => { + let name = class + .id + .as_ref() + .map(|id| id.name.to_string()) + .ok_or_else(|| "ClassDeclaration without name".to_string())?; + let var_reg = ctx.alloc_reg(); + ctx.declare(&name, var_reg, VariableDeclarationKind::Let, false)?; + ctx.init_var(&name); + let ctor_reg = self.emit_class(class, ctx)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, ctor_reg, 0)); + Ok(None) + } Statement::ThrowStatement(ts) => { let exc_reg = self.emit_expression(&ts.argument, ctx)?; ctx.emit(opcode::encode(OpCode::THROW, exc_reg, 0, 0)); @@ -1014,6 +1122,7 @@ impl Compiler { ctx.emit_load_const(r, const_idx); Ok(r) } + Expression::ClassExpression(class) => self.emit_class(class, ctx), Expression::NewExpression(ne) => { let constructor_reg = self.emit_expression(&ne.callee, ctx)?; let mut arg_regs = Vec::new(); diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index 0dd666e2..ace4ab6d 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -1,4 +1,4 @@ -use oxide_parser::{Expression, ForStatementInit, SimpleAssignmentTarget, Statement}; +use oxide_parser::{ClassElement, Expression, ForStatementInit, PropertyKey, SimpleAssignmentTarget, Statement}; pub fn structural_hash(program: &oxide_parser::Program) -> u64 { use std::hash::Hasher; @@ -117,6 +117,16 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { } } } + Statement::ClassDeclaration(class) => { + 13u8.hash(h); + if let Some(id) = &class.id { + id.name.as_str().hash(h); + } + class.super_class.is_some().hash(h); + for element in &class.body.body { + hash_class_element(element, h); + } + } Statement::ThrowStatement(ts) => { 14u8.hash(h); hash_expression(&ts.argument, h); @@ -148,6 +158,53 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { } } +fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher) { + use std::hash::Hash; + + match key { + PropertyKey::StaticIdentifier(ident) => { + 0u8.hash(h); + ident.name.as_str().hash(h); + } + PropertyKey::StringLiteral(s) => { + 1u8.hash(h); + s.value.hash(h); + } + PropertyKey::PrivateIdentifier(pi) => { + 2u8.hash(h); + pi.name.as_str().hash(h); + } + _ => { + 3u8.hash(h); + std::mem::discriminant(key).hash(h); + } + } +} + +fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxHasher) { + use std::hash::Hash; + + match element { + ClassElement::MethodDefinition(method) => { + 0u8.hash(h); + method.r#static.hash(h); + method.computed.hash(h); + std::mem::discriminant(&method.kind).hash(h); + hash_property_key(&method.key, h); + (method.value.params.items.len() as u32).hash(h); + if let Some(body) = &method.value.body { + for stmt in &body.statements { + hash_statement(stmt, h); + } + } + } + _ => { + 1u8.hash(h); + std::mem::discriminant(element).hash(h); + } + } +} + fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { use std::hash::Hash; @@ -256,6 +313,16 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } } } + Expression::ClassExpression(class) => { + 18u8.hash(h); + if let Some(id) = &class.id { + id.name.as_str().hash(h); + } + class.super_class.is_some().hash(h); + for element in &class.body.body { + hash_class_element(element, h); + } + } Expression::NewExpression(ne) => { 14u8.hash(h); hash_expression(&ne.callee, h); diff --git a/crates/oxide_compiler/src/module.rs b/crates/oxide_compiler/src/module.rs index 3a8d9cc5..225a5234 100644 --- a/crates/oxide_compiler/src/module.rs +++ b/crates/oxide_compiler/src/module.rs @@ -31,6 +31,9 @@ pub struct CompiledModule { /// Function name inferred from assignment context (D-04). /// Set at the VariableDeclaration / ObjectProperty assignment site. pub function_name: Option, + /// True when this bytecode function is a class constructor. + /// Ordinary CALL must reject it, while NEW_EXPRESSION may construct through it. + pub is_class_constructor: bool, } impl CompiledModule { @@ -46,6 +49,7 @@ impl CompiledModule { is_arrow: false, captured_this_const_idx: 0, function_name: None, + is_class_constructor: false, } } } @@ -69,6 +73,7 @@ impl Clone for CompiledModule { is_arrow: self.is_arrow, captured_this_const_idx: self.captured_this_const_idx, function_name: self.function_name.clone(), + is_class_constructor: self.is_class_constructor, } } } diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index 5d1dafa0..838141f8 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -325,3 +325,27 @@ fn compile_neg_infinity_ok() { let module = compile_source("1 / -Infinity"); assert!(!module.bytecode.is_empty()); } + +#[test] +fn compile_class_expression_emits_constructor_value() { + let module = compile_source("const C = class Foo { method() { return 2; } }; C"); + assert_eq!(module.sub_modules.len(), 2, "expected constructor + method submodules"); + assert!( + module + .bytecode + .iter() + .filter(|&&i| opcode::opcode(i) == OpCode::SET_PROP) + .count() + >= 3, + "class expression should wire method/constructor/prototype properties" + ); +} + +#[test] +fn compile_class_expression_default_constructor() { + let module = compile_source("class A {}; (class B {})"); + assert!( + module.sub_modules.iter().any(|m| m.is_class_constructor), + "expected at least one class constructor submodule" + ); +} diff --git a/crates/oxide_compiler/tests/compile_stmt_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs index 64b8f189..efe62831 100644 --- a/crates/oxide_compiler/tests/compile_stmt_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -244,3 +244,37 @@ fn compile_large_adjacent_switches_do_not_collide_labels() { "expected both large switches to compile with distinct case labels" ); } + +#[test] +fn compile_class_declaration_emits_constructor_and_prototype_setup() { + let module = compile_source("class A { m() { return 1; } }"); + assert_eq!(module.sub_modules.len(), 2, "expected constructor + method submodules"); + assert!( + module + .bytecode + .iter() + .filter(|&&i| opcode::opcode(i) == OpCode::NEW_OBJECT) + .count() + >= 1, + "class should allocate a prototype object" + ); + assert!( + module + .bytecode + .iter() + .filter(|&&i| opcode::opcode(i) == OpCode::SET_PROP) + .count() + >= 3, + "class should assign method, constructor, and prototype properties" + ); +} + +#[test] +fn compile_class_declaration_default_constructor_creates_submodule() { + let module = compile_source("class A {}"); + assert_eq!(module.sub_modules.len(), 1, "expected synthesized default constructor"); + assert!( + module.sub_modules[0].is_class_constructor, + "default constructor submodule should be marked as class constructor" + ); +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 9cd1a493..709dd2eb 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -40,6 +40,7 @@ impl PropIndex for i32 { /// Layout (56B): /// header: u32 bits /// [0:23] shape_id +/// [27] is_class_constructor /// [28] is_arrow /// [29] is_array /// [30] is_extensible @@ -318,6 +319,20 @@ impl JsObject { pub fn set_captured_this(&mut self, v: JsValue) { self.captured_this = v; } + + /// Class constructor flag (header bit 27). + /// Ordinary CALL rejects objects with this flag; NEW_EXPRESSION is allowed. + pub fn is_class_constructor(&self) -> bool { + (self.header >> 27) & 1 != 0 + } + + pub fn set_class_constructor(&mut self, v: bool) { + if v { + self.header |= 1 << 27; + } else { + self.header &= !(1 << 27); + } + } } #[cfg(test)] diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index 16b13c09..5cff659b 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -170,6 +170,8 @@ mod tests { saved_regs: vec![JsValue::undefined()].into_boxed_slice(), saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), + construct_result_reg: None, + constructed_this: None, }); let stack = error_stack_getter(&mut vm, &[0]).unwrap(); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 7c71ca09..3163bda7 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -50,6 +50,8 @@ pub struct CallFrame { pub saved_regs: Box<[JsValue]>, pub saved_this: JsValue, pub saved_new_target: JsValue, + pub construct_result_reg: Option, + pub constructed_this: Option, } pub struct ForInIter<'bump> { @@ -244,12 +246,13 @@ impl Vm { /// Create a function JsObject for a BytecodeFunc constant. /// When `is_arrow` is true, captures the current `this` (regs[254]) /// for lexical this binding at call time (D-01). - fn create_function_object(&mut self, sub_idx: u32, is_arrow: bool) -> JsValue { + fn create_function_object(&mut self, sub_idx: u32, is_arrow: bool, is_class_constructor: bool) -> JsValue { let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject; let proto_val = JsValue::from_js_object(func_proto_ptr); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val); obj.set_function(true); obj.set_sub_module_index(sub_idx); + obj.set_class_constructor(is_class_constructor); if is_arrow { obj.set_arrow(true); // Capture lexical `this` from the enclosing scope (regs[254]). @@ -290,12 +293,13 @@ impl Vm { Constant::Undefined => Ok(JsValue::undefined()), Constant::BytecodeFunc(idx) => { let sub_idx = *idx as usize; - let is_arrow = if sub_idx > 0 && sub_idx <= self.sub_modules.len() { - self.sub_modules[sub_idx - 1].is_arrow + let (is_arrow, is_class_constructor) = if sub_idx > 0 && sub_idx <= self.sub_modules.len() { + let sub_module = &self.sub_modules[sub_idx - 1]; + (sub_module.is_arrow, sub_module.is_class_constructor) } else { - false + (false, false) }; - Ok(self.create_function_object(*idx, is_arrow)) + Ok(self.create_function_object(*idx, is_arrow, is_class_constructor)) } Constant::RegExp(pattern, flags) => { let pat_si = self.kernel.string_forge().intern(pattern).0; @@ -483,6 +487,21 @@ impl Vm { self.bytecode[self.pc - 1] = (ptr as u64 >> 32) as u32; } + fn restore_frame(&mut self, frame: CallFrame) { + if let Some(saved_bc) = self.saved_bytecode_stack.pop() { + self.bytecode = saved_bc; + } + if let Some(saved_consts) = self.saved_constants_stack.pop() { + self.constants = saved_consts; + } + let restore_len = frame.saved_regs.len(); + self.regs[..restore_len].copy_from_slice(&frame.saved_regs); + self.regs[254] = frame.saved_this; + self.regs[255] = frame.saved_new_target; + self.active_reg_limit = frame.caller_reg_limit; + self.pc = frame.return_addr; + } + pub fn rerun(&mut self) -> Result { self.clear_execution_state(); self.active_reg_limit = self.root_reg_limit; @@ -536,6 +555,11 @@ impl Vm { pub(crate) fn unwind(&mut self) -> Result<(), String> { while let Some(handler) = self.try_stack.pop() { + while self.frames.len() > handler.frame_depth { + if let Some(frame) = self.frames.pop() { + self.restore_frame(frame); + } + } if let Some(finally_pc) = handler.finally_pc { if self.pending_exception.is_none() { self.pending_exception = self.exception_value.take(); @@ -551,6 +575,9 @@ impl Vm { return Ok(()); } } + while let Some(frame) = self.frames.pop() { + self.restore_frame(frame); + } let exc = self.exception_value.take().unwrap_or(JsValue::undefined()); let kind_str = self.pending_error_kind.take().unwrap_or("Error"); let msg = format!("uncaught {kind_str}: {exc}"); @@ -695,6 +722,9 @@ impl Vm { if !obj_ptr.is_null() { let obj = unsafe { &*obj_ptr }; if obj.is_function() { + if obj.is_class_constructor() { + throw_err!(self, TypeError, "class constructor cannot be invoked without 'new'"); + } let ext = self.bytecode[self.pc]; self.pc += 1; let arg_count = (ext & 0xFF) as usize; @@ -738,7 +768,8 @@ impl Vm { } // Set this (reg 254 convention). // Arrow functions use captured lexical `this` (D-01). - self.regs[254] = if sub_is_arrow { obj.captured_this() } else { callee }; + self.regs[254] = + if sub_is_arrow { obj.captured_this() } else { self.regs[this_reg as usize] }; self.regs[255] = JsValue::undefined(); let converted_sub_constants = self.convert_constants(&sub_constants)?; @@ -759,6 +790,8 @@ impl Vm { saved_regs, saved_this, saved_new_target, + construct_result_reg: None, + constructed_this: None, }); self.bytecode = sub_bytecode; @@ -886,6 +919,72 @@ impl Vm { } } } + } else if ctor_obj.sub_module_index() > 0 { + let sub_idx = ctor_obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "NEW_EXPRESSION: sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + + if self.frames.len() >= self.kernel.config.max_call_depth { + return Err("RangeError: Maximum call stack size exceeded".into()); + } + + let new_obj_val = JsValue::object(new_obj as *mut u8); + let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); + let sub_n_args = self.sub_modules[sub_idx].n_args as usize; + let sub_n_registers = self.sub_modules[sub_idx].n_registers; + let sub_constants = self.sub_modules[sub_idx].constants.clone(); + let sub_param_base = self.sub_modules[sub_idx].param_base as usize; + let caller_reg_limit = self.active_reg_limit.max(1); + let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_this = self.regs[254]; + let saved_new_target = self.regs[255]; + + for i in 0..sub_n_args { + let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; + self.regs[sub_param_base + i] = self.regs[src_reg]; + } + self.regs[254] = new_obj_val; + self.regs[255] = constructor; + + let converted_sub_constants = self.convert_constants(&sub_constants)?; + + self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); + self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + + self.frames.push(CallFrame { + return_addr: self.pc, + function_name: self.sub_modules[sub_idx] + .function_name + .as_deref() + .map(|name| self.kernel.string_forge().intern(name).0) + .unwrap_or(0), + caller_reg_limit, + saved_regs, + saved_this, + saved_new_target, + construct_result_reg: Some(rd as u8), + constructed_this: Some(new_obj_val), + }); + + self.bytecode = sub_bytecode; + self.constants = converted_sub_constants; + + for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { + let si = self.kernel.string_forge().intern(name.as_str()).0; + let global = self.kernel.global_object(); + if let Some(pos) = self.kernel.shape_forge().lookup_position(global.shape_id(), si) { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + + self.active_reg_limit = sub_n_registers.max(1); + self.pc = 0; + continue; } else { let error = crate::builtins::error::create_error( self, @@ -903,21 +1002,17 @@ impl Vm { OpCode::RETURN => { let result = self.regs[rd]; if let Some(frame) = self.frames.pop() { - // Restore caller's saved bytecode and constants - if let Some(saved_bc) = self.saved_bytecode_stack.pop() { - self.bytecode = saved_bc; + let construct_result_reg = frame.construct_result_reg; + let constructed_this = frame.constructed_this; + self.restore_frame(frame); + if let (Some(target_reg), Some(constructed_this)) = + (construct_result_reg, constructed_this) + { + self.regs[target_reg as usize] = if result.is_object() { result } else { constructed_this }; + self.regs[0] = self.regs[target_reg as usize]; + } else { + self.regs[0] = result; } - if let Some(saved_consts) = self.saved_constants_stack.pop() { - self.constants = saved_consts; - } - let restore_len = frame.saved_regs.len(); - self.regs[..restore_len].copy_from_slice(&frame.saved_regs); - self.regs[254] = frame.saved_this; - self.regs[255] = frame.saved_new_target; - self.active_reg_limit = frame.caller_reg_limit; - // Restore caller's pc and merge result into caller's regs[0]. - self.pc = frame.return_addr; - self.regs[0] = result; } else { return Ok(result); } @@ -1324,6 +1419,8 @@ mod tests { saved_regs: vec![JsValue::undefined()].into_boxed_slice(), saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), + construct_result_reg: None, + constructed_this: None, }); vm.for_in_iters.push(std::ptr::dangling_mut::()); vm.for_of_iters.push(std::ptr::dangling_mut::()); diff --git a/crates/oxide_vm/tests/class_tests.rs b/crates/oxide_vm/tests/class_tests.rs new file mode 100644 index 00000000..7d311125 --- /dev/null +++ b/crates/oxide_vm/tests/class_tests.rs @@ -0,0 +1,70 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +#[test] +fn class_declaration_is_function_value() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A {} ; typeof A").unwrap(); + let ty = vm.lookup_str(result).expect("typeof result should be string"); + assert_eq!(ty, "function"); +} + +#[test] +fn class_method_is_on_prototype() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { m() { return 1; } } new A().m()").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn class_constructor_initializes_instance_state() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { constructor(x) { this.x = x; } } new A(3).x").unwrap(); + assert_eq!(result.as_int(), 3); +} + +#[test] +fn class_expression_constructs_instances() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const C = class Foo { method() { return 2; } }; new C().method()").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn class_expression_inner_name_is_visible_in_method_body() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const C = class Foo { self() { return Foo; } }; new C().self() === C").unwrap(); + assert!( + result.is_bool() && result.as_bool(), + "expected inner class name to resolve to constructor" + ); +} + +#[test] +fn class_constructor_cannot_be_called_without_new() { + let mut vm = Vm::new(); + let err = eval(&mut vm, "class A {} ; A()").unwrap_err(); + assert!(err.contains("TypeError"), "expected TypeError, got: {err}"); +} + +#[test] +fn class_constructor_object_return_overrides_instance() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { constructor() { return { x: 1 }; } } new A().x").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn class_constructor_primitive_return_preserves_instance() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { constructor() { this.x = 1; return 5; } } new A().x").unwrap(); + assert_eq!(result.as_int(), 1); +} diff --git a/crates/oxide_vm/tests/function_tests.rs b/crates/oxide_vm/tests/function_tests.rs index 9e166503..94f69255 100644 --- a/crates/oxide_vm/tests/function_tests.rs +++ b/crates/oxide_vm/tests/function_tests.rs @@ -158,20 +158,27 @@ fn fd_returns_new_array() { fn this_in_function_reads_value() { let mut vm = Vm::new(); let result = eval(&mut vm, "function f() { return this; } f()").unwrap(); - assert!(result.is_object(), "expected object (function as this), got: {:?}", result); + assert!(result.is_undefined(), "expected undefined in strict-mode call, got: {:?}", result); } #[test] fn this_in_function_assign_member() { let mut vm = Vm::new(); - let result = eval(&mut vm, "function f(m) { this.message = m; } f('hello'); 1").unwrap(); - assert_eq!(result.as_int(), 1); + let err = eval(&mut vm, "function f(m) { this.message = m; } f('hello'); 1").unwrap_err(); + assert!(err.contains("TypeError"), "expected TypeError, got: {err}"); } #[test] fn this_member_access() { let mut vm = Vm::new(); - let result = eval(&mut vm, "function f() { this.x = 42; return this.x; } f()").unwrap(); + let err = eval(&mut vm, "function f() { this.x = 42; return this.x; } f()").unwrap_err(); + assert!(err.contains("TypeError"), "expected TypeError, got: {err}"); +} + +#[test] +fn member_call_uses_receiver_as_this() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var o = { x: 42, f: function() { return this.x; } }; o.f()").unwrap(); assert_eq!(result.as_int(), 42); } diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index 017f2829..b8a75809 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -128,9 +128,9 @@ fn regression_new_expression_native_constructor_error_is_catchable() { #[test] fn regression_new_expression_bytecode_constructor_error_is_catchable() { assert_eq!( - eval("try { function C() {} new C(); 0 } catch (e) { 1 }"), + eval("try { class C { constructor() { throw new Error('boom') } } new C(); 0 } catch (e) { 1 }"), "1", - "expected bytecode constructor capability error to enter catch" + "expected bytecode class constructor error to enter catch" ); }