From 365b0e58f68dc5c76d83cc1216d1dae921a7efab Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 27 Jun 2026 20:13:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Error=E5=8E=9F=E5=9E=8B=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E3=80=81=E8=A7=84=E8=8C=83toString=E3=80=81V8?= =?UTF-8?q?=E9=A3=8E=E6=A0=BCstack=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/error.rs | 205 +++++++++------- crates/oxide_kernel/src/builtin.rs | 33 +++ crates/oxide_runtime_api/src/lib.rs | 8 +- crates/oxide_vm/src/vm.rs | 8 +- crates/oxide_vm/tests/builtins_error_tests.rs | 224 +++++++++++++++++- 5 files changed, 379 insertions(+), 99 deletions(-) diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index 79a95558..598a4576 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -6,31 +6,24 @@ use oxide_types::mem::P; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -fn create_error_object(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(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(host: &mut H, kind: &str, msg: &str) -> JsValue { @@ -43,7 +36,21 @@ pub fn create_kind_error(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(host: &mut H, msg: &str) -> JsValue { @@ -70,85 +77,99 @@ pub fn create_uri_error(host: &mut H, msg: &str) -> JsValue { create_kind_error(host, "URIError", msg) } -fn get_msg(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(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(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(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(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(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(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(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(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(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(host: &mut H, _args: &[u8]) -> NativeResult { +pub fn error_stack_getter(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 = names.iter().rev().map(|n| format!(" at {} (native)", n)).collect(); - lines.push(" at (native)".to_string()); - let result = lines.join("\n"); + for n in &names { + result.push_str(&format!("\n at {} (:0:0)", n)); + } NativeResult::Ok(host.new_string(&result)) } diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 5dc9b050..a75761f5 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -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, 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) { diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index f8043e37..e7a40fe3 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -121,11 +121,13 @@ pub trait VmHost { fn symbol_key_for_id(&self, idx: u32) -> Option; } -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}") } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 1d14254c..a1abb8ce 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -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}") } } diff --git a/crates/oxide_vm/tests/builtins_error_tests.rs b/crates/oxide_vm/tests/builtins_error_tests.rs index 58eca22c..8be47cb6 100644 --- a/crates/oxide_vm/tests/builtins_error_tests.rs +++ b/crates/oxide_vm/tests/builtins_error_tests.rs @@ -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); +}