mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 支持基础 class 声明表达式与原型方法
This commit is contained in:
@@ -259,12 +259,35 @@ impl Compiler {
|
||||
Self
|
||||
}
|
||||
|
||||
pub(crate) fn extract_function_parts<'a>(
|
||||
&self, function: &'a oxide_parser::Function<'a>,
|
||||
) -> Result<(Vec<String>, &'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<CompiledModule, String> {
|
||||
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<CompiledModule, String> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<u8, String> {
|
||||
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<Option<u8>, 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
+117
-20
@@ -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<u8>,
|
||||
pub constructed_this: Option<JsValue>,
|
||||
}
|
||||
|
||||
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<JsValue, String> {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
} 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::<u8>());
|
||||
vm.for_of_iters.push(std::ptr::dangling_mut::<u8>());
|
||||
|
||||
@@ -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<JsValue, String> {
|
||||
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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user