fix: Error原型属性、规范toString、V8风格stack trace

This commit is contained in:
2026-06-27 20:13:17 +08:00
parent 9063bc4a8c
commit 365b0e58f6
5 changed files with 379 additions and 99 deletions
+113 -92
View File
@@ -6,31 +6,24 @@ use oxide_types::mem::P;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
fn create_error_object<H: VmHost>(host: &mut H, error_proto_ptr: *mut JsObject, name: &str, message: &str) -> JsValue {
let obj = host
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(error_proto_ptr)));
fn set_own_message<H: VmHost>(host: &mut H, this: *mut JsObject, args: &[u8]) {
if args.len() <= 1 {
return;
}
let msg_val = host.reg(args[1]);
if msg_val.is_undefined() {
return;
}
let msg_str = to_string(msg_val);
let sf = Arc::clone(host.kernel_core().perm_interner());
let sh = Arc::clone(host.kernel_core().shape_forge());
let si_message = sf.intern("message").0;
let sh_message = sh.make_shape(EMPTY_SHAPE_ID, si_message);
let msg_val = host.new_string(message);
let si = sf.intern("message").0;
let new_shape = sh.make_shape(EMPTY_SHAPE_ID, si);
let perm_val = host.new_string(&msg_str);
unsafe {
(*obj).set_shape_id(sh_message);
(*obj).push_prop(msg_val);
(*this).set_shape_id(new_shape);
(*this).push_prop(perm_val);
}
let si_name = sf.intern("name").0;
let sh_name = sh.make_shape(sh_message, si_name);
let name_val = host.new_string(name);
unsafe {
(*obj).set_shape_id(sh_name);
(*obj).push_prop(name_val);
}
JsValue::from_js_object(obj)
}
pub fn create_kind_error<H: VmHost>(host: &mut H, kind: &str, msg: &str) -> JsValue {
@@ -43,7 +36,21 @@ pub fn create_kind_error<H: VmHost>(host: &mut H, kind: &str, msg: &str) -> JsVa
"EvalError" => P::as_ptr(&host.session().builtin_world().eval_error_proto) as *mut JsObject,
_ => P::as_ptr(&host.session().builtin_world().error_proto) as *mut JsObject,
};
create_error_object(host, proto_ptr, kind, msg)
let obj = host
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)));
if !msg.is_empty() {
let sf = Arc::clone(host.kernel_core().perm_interner());
let sh = Arc::clone(host.kernel_core().shape_forge());
let si = sf.intern("message").0;
let new_shape = sh.make_shape(EMPTY_SHAPE_ID, si);
let msg_val = host.new_string(msg);
unsafe {
(*obj).set_shape_id(new_shape);
(*obj).push_prop(msg_val);
}
}
JsValue::from_js_object(obj)
}
pub fn create_type_error<H: VmHost>(host: &mut H, msg: &str) -> JsValue {
@@ -70,85 +77,99 @@ pub fn create_uri_error<H: VmHost>(host: &mut H, msg: &str) -> JsValue {
create_kind_error(host, "URIError", msg)
}
fn get_msg<H: VmHost>(host: &mut H, args: &[u8]) -> String {
if args.len() > 1 {
to_string(host.reg(args[1]))
} else {
String::new()
}
macro_rules! error_ctor {
($name:ident, $proto_field:ident) => {
pub fn $name<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(255);
let this = if this_val.is_object() {
this_val.as_js_object_ptr()
} else {
let proto_ptr = P::as_ptr(&host.session().builtin_world().$proto_field) as *mut JsObject;
host.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)))
};
set_own_message(host, this, args);
NativeResult::Ok(JsValue::from_js_object(this))
}
};
}
pub fn error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "Error", &msg))
}
pub fn type_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().type_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "TypeError", &msg))
}
pub fn reference_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().reference_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "ReferenceError", &msg))
}
pub fn range_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().range_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "RangeError", &msg))
}
pub fn syntax_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().syntax_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "SyntaxError", &msg))
}
pub fn uri_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().uri_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "URIError", &msg))
}
pub fn eval_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().eval_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "EvalError", &msg))
}
error_ctor!(error_constructor, error_proto);
error_ctor!(type_error_constructor, type_error_proto);
error_ctor!(reference_error_constructor, reference_error_proto);
error_ctor!(range_error_constructor, range_error_proto);
error_ctor!(syntax_error_constructor, syntax_error_proto);
error_ctor!(uri_error_constructor, uri_error_proto);
error_ctor!(eval_error_constructor, eval_error_proto);
pub fn error_to_string<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(args[0]);
let (name, msg) = if this_val.is_object() {
let obj = unsafe { &*this_val.as_js_object_ptr() };
let name_val = obj
.hash_props_vec()
.and_then(|v| v.first())
.copied()
.unwrap_or(JsValue::undefined());
let msg_val = obj
.hash_props_vec()
.and_then(|v| v.get(1))
.copied()
.unwrap_or(JsValue::undefined());
(
host.lookup_str(name_val).unwrap_or_else(|| "Error".to_string()),
host.lookup_str(msg_val).unwrap_or_default(),
)
} else {
("Error".to_string(), String::new())
if !this_val.is_object() {
let err = create_type_error(host, "Error.prototype.toString called on non-object");
return NativeResult::Err(err);
}
let obj = unsafe { &*this_val.as_js_object_ptr() };
let sf = Arc::clone(host.kernel_core().perm_interner());
let si_name = sf.intern("name").0;
let si_msg = sf.intern("message").0;
let name_str = match host.resolve_property(obj, si_name) {
Some(v) if !v.is_undefined() => match host.coerce_primitive_bounded(v, true) {
Ok(prim) => to_string(prim),
Err(_) => {
let err = create_type_error(host, "Cannot convert name to primitive value");
return NativeResult::Err(err);
}
},
_ => "Error".to_string(),
};
let msg_str = match host.resolve_property(obj, si_msg) {
Some(v) if !v.is_undefined() => match host.coerce_primitive_bounded(v, true) {
Ok(prim) => to_string(prim),
Err(_) => {
let err = create_type_error(host, "Cannot convert message to primitive value");
return NativeResult::Err(err);
}
},
_ => String::new(),
};
let result = if name_str.is_empty() {
msg_str
} else if msg_str.is_empty() {
name_str
} else {
format!("{}: {}", name_str, msg_str)
};
let result = oxide_runtime_api::format_error_message(&name, &msg);
NativeResult::Ok(host.new_string(&result))
}
pub fn error_stack_getter<H: VmHost>(host: &mut H, _args: &[u8]) -> NativeResult {
pub fn error_stack_getter<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(args[0]);
let (name_str, msg_str) = if this_val.is_object() {
let obj = unsafe { &*this_val.as_js_object_ptr() };
let sf = Arc::clone(host.kernel_core().perm_interner());
let si_name = sf.intern("name").0;
let si_msg = sf.intern("message").0;
let n = host
.resolve_property(obj, si_name)
.and_then(|v| host.lookup_str(v))
.unwrap_or_else(|| "Error".to_string());
let m = host
.resolve_property(obj, si_msg)
.and_then(|v| host.lookup_str(v))
.unwrap_or_default();
(n, m)
} else {
("Error".to_string(), String::new())
};
let header = oxide_runtime_api::format_error_message(&name_str, &msg_str);
let mut result = header;
let names = host.call_stack_function_names();
let mut lines: Vec<String> = names.iter().rev().map(|n| format!(" at {} (native)", n)).collect();
lines.push(" at <anonymous> (native)".to_string());
let result = lines.join("\n");
for n in &names {
result.push_str(&format!("\n at {} (<unknown>:0:0)", n));
}
NativeResult::Ok(host.new_string(&result))
}
+33
View File
@@ -1011,6 +1011,39 @@ impl BuiltinWorld {
("toString", methods.to_string, 0),
("stack", methods.stack, 0),
);
let si_name = string_forge.intern("name").0;
let error_si = string_forge.intern("Error").0;
let error_name_val = JsValue::perm_string(string_forge.string_ptr(error_si));
let name_shape = shape_forge.make_shape(proto.shape_id(), si_name);
proto.set_shape_id(name_shape);
proto.ensure_hash_props().push(error_name_val);
let si_message = string_forge.intern("message").0;
let empty_si = string_forge.intern("").0;
let empty_val = JsValue::perm_string(string_forge.string_ptr(empty_si));
let msg_shape = shape_forge.make_shape(proto.shape_id(), si_message);
proto.set_shape_id(msg_shape);
proto.ensure_hash_props().push(empty_val);
self.set_subtype_proto_name(string_forge, shape_forge, &self.type_error_proto, "TypeError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.reference_error_proto, "ReferenceError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.range_error_proto, "RangeError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.syntax_error_proto, "SyntaxError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.uri_error_proto, "URIError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.eval_error_proto, "EvalError", si_name);
}
fn set_subtype_proto_name(
&self, string_forge: &PermInterner, shape_forge: &ShapeForge, proto_p: &P<JsObject>, name: &str, si_name: u32,
) {
let proto_ptr = P::as_ptr(proto_p) as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
let name_si = string_forge.intern(name).0;
let name_val = JsValue::perm_string(string_forge.string_ptr(name_si));
let name_shape = shape_forge.make_shape(proto.shape_id(), si_name);
proto.set_shape_id(name_shape);
proto.ensure_hash_props().push(name_val);
}
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
+5 -3
View File
@@ -121,11 +121,13 @@ pub trait VmHost {
fn symbol_key_for_id(&self, idx: u32) -> Option<String>;
}
pub fn format_error_message(kind: &str, msg: &str) -> String {
if msg.is_empty() || msg == kind || msg.starts_with(&format!("{kind}:")) {
pub fn format_error_message(name: &str, msg: &str) -> String {
if name.is_empty() {
msg.to_string()
} else if msg.is_empty() {
name.to_string()
} else {
format!("{kind}: {msg}")
format!("{name}: {msg}")
}
}
+5 -3
View File
@@ -55,11 +55,13 @@ fn js_error_kind_name(kind: JsErrorKind) -> &'static str {
}
}
pub(crate) fn format_error_message(kind: &str, msg: &str) -> String {
if msg.is_empty() || msg == kind || msg.starts_with(&format!("{kind}:")) {
pub(crate) fn format_error_message(name: &str, msg: &str) -> String {
if name.is_empty() {
msg.to_string()
} else if msg.is_empty() {
name.to_string()
} else {
format!("{kind}: {msg}")
format!("{name}: {msg}")
}
}
+223 -1
View File
@@ -1,5 +1,6 @@
use oxide_builtins::error;
use oxide_compiler::compiler::Compiler;
use oxide_types::mem::P;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
@@ -115,7 +116,7 @@ fn error_name_is_string_property() {
let err = error::error_constructor(&mut vm, &[0u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
let n = obj.prop_count();
assert!(n >= 2);
assert_eq!(n, 0);
}
#[test]
@@ -142,3 +143,224 @@ fn error_subtype_constructors_produce_named_objects() {
assert_eq!(format!("{}", eval("new TypeError('boom').name == 'TypeError'").unwrap()), "true");
assert_eq!(format!("{}", eval("new SyntaxError('boom').name == 'SyntaxError'").unwrap()), "true");
}
// New tests for prototype properties + constructor fixes
#[test]
fn error_prototype_has_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some(), "Error.prototype should have 'name' property");
}
#[test]
fn error_prototype_has_message() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let msg_si = vm.kernel_core().perm_interner().intern("message").0;
let proto_ptr = P::as_ptr(&bw.error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), msg_si);
assert!(si.is_some(), "Error.prototype should have 'message' property");
}
#[test]
fn type_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.type_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some(), "TypeError.prototype should have 'name'");
}
#[test]
fn reference_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.reference_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn range_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.range_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn syntax_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.syntax_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn uri_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.uri_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn eval_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.eval_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn error_constructor_no_own_props() {
let mut vm = make_vm();
let err = error::error_constructor(&mut vm, &[0u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 0);
}
#[test]
fn error_constructor_with_message() {
let mut vm = make_vm();
let msg = vm.new_string("boom");
vm.set_reg(1, msg);
let err = error::error_constructor(&mut vm, &[0u8, 1u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 1);
}
#[test]
fn create_type_error_no_own_name() {
let mut vm = make_vm();
let err = error::create_type_error(&mut vm, "something broken");
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 1);
}
// Plan 02: toString tests
#[test]
fn error_to_string_basic() {
let result = eval("new Error('test').toString()").unwrap();
assert!(result.is_string());
}
#[test]
fn error_to_string_empty_message() {
let result = eval("new Error().toString()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("Error".to_string()));
}
#[test]
fn error_to_string_type_error() {
let result = eval("new TypeError('bad').toString()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("TypeError: bad".to_string()));
}
#[test]
fn error_to_string_name_only() {
let result = eval("Error.prototype.toString.call({name: 'E'})").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("E".to_string()));
}
#[test]
fn error_to_string_non_object_throws() {
let mut vm = make_vm();
vm.set_reg(0, JsValue::int(42));
let result = error::error_to_string(&mut vm, &[0u8]);
match result {
oxide_runtime_api::NativeResult::Err(_) => {}
_ => panic!("expected Err, got Ok or TailCall"),
}
}
// format_error_message tests
#[test]
fn format_error_message_both() {
let result = oxide_runtime_api::format_error_message("TypeError", "bad arg");
assert_eq!(result, "TypeError: bad arg");
}
#[test]
fn format_error_message_empty_msg() {
let result = oxide_runtime_api::format_error_message("Error", "");
assert_eq!(result, "Error");
}
#[test]
fn format_error_message_empty_name() {
let result = oxide_runtime_api::format_error_message("", "msg");
assert_eq!(result, "msg");
}
// stack tests
#[test]
fn error_stack_is_string() {
let result = eval("typeof new Error().stack()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("string".to_string()));
}
#[test]
fn error_stack_starts_with_header() {
let result = eval("new Error().stack()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result).unwrap();
assert!(s.starts_with("Error"), "stack should start with 'Error', got: {}", s);
}
#[test]
fn error_stack_frame_format() {
let result = eval("(function foo() { return new Error('boom').stack(); })()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result).unwrap();
assert!(s.contains(" at "), "stack should have 4-space indent, got: {}", s);
}