diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index 730c90e..ada11c1 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -160,15 +160,53 @@ pub fn function_bind(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn function_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "Function.prototype.toString called on non-function", + )); + } + + let func = unsafe { &*this_val.as_js_object_ptr() }; + if !func.is_function() { + return Err(crate::builtins::error::create_type_error( + vm, + "Function.prototype.toString called on non-function", + )); + } + + let name_si = vm.kernel().string_forge().intern("name").0; let name = vm - .kernel() - .string_forge() - .lookup(vm.reg(args[0]).as_string_index()) - .unwrap_or_default(); - let result = if name.is_empty() { - "function () { [native code] }".to_string() + .resolve_property(func, name_si) + .and_then(|v| { + if v.is_string() { + vm.kernel().string_forge().lookup(v.as_string_index()) + } else { + None + } + }) + .unwrap_or_else(|| { + let sub_idx = func.sub_module_index(); + if sub_idx > 0 && (sub_idx as usize) <= vm.sub_modules.len() { + vm.sub_modules[sub_idx as usize - 1] + .function_name + .clone() + .unwrap_or_default() + } else { + String::new() + } + }); + + let body = if func.native_fn().is_some() { + "[native code]" } else { - format!("function {}() {{ [native code] }}", name) + "[bytecode]" + }; + let result = if name.is_empty() { + format!("function () {{ {body} }}") + } else { + format!("function {name}() {{ {body} }}") }; let si = vm.kernel().string_forge().intern(&result).0; Ok(JsValue::string(si, 0)) diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index e6a0783..4ffb8c1 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -1,8 +1,9 @@ use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; use oxide_vm::builtins::array::{array_constructor, array_push}; use oxide_vm::vm::Vm; -fn eval(source: &str) -> Result { +fn eval(source: &str) -> Result<(Vm, JsValue), String> { let allocator = oxide_parser::Allocator::default(); let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; @@ -10,20 +11,21 @@ fn eval(source: &str) -> Result { .compile(&program) .map_err(|e| format!("Compile error: {}", e))?; let mut vm = Vm::new(); - vm.run(&module) + let result = vm.run(&module)?; + Ok((vm, result)) } #[test] fn array_push_adds_element() { - let result = eval("var a = [1,2]; a.push(3); a").unwrap(); + let (_vm, result) = eval("var a = [1,2]; a.push(3); a").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 3); - assert_eq!(obj.get_prop_at(2), oxide_types::value::JsValue::int(3)); + assert_eq!(obj.get_prop_at(2), JsValue::int(3)); } #[test] fn array_push_returns_length() { - let result = eval("[1,2,3].push(4)").unwrap(); + let (_vm, result) = eval("[1,2,3].push(4)").unwrap(); assert_eq!(result.as_int(), 4); } @@ -40,13 +42,13 @@ fn array_push_returns_true_length_beyond_31() { #[test] fn array_length_tracks_more_than_31_elements() { - let result = eval("var a=[]; for (var i=0;i<40;i=i+1) a.push(i); a.length").unwrap(); + let (_vm, result) = eval("var a=[]; for (var i=0;i<40;i=i+1) a.push(i); a.length").unwrap(); assert_eq!(result.as_int(), 40); } #[test] fn array_push_preserves_slots_beyond_255() { - let result = eval("var a=[]; for (var i=0;i<257;i=i+1) a.push(i); a").unwrap(); + let (_vm, result) = eval("var a=[]; for (var i=0;i<257;i=i+1) a.push(i); a").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 257); let tail = obj.get_prop_at(256); @@ -55,126 +57,126 @@ fn array_push_preserves_slots_beyond_255() { #[test] fn array_pop_returns_last() { - let result = eval("[1,2,3].pop()").unwrap(); + let (_vm, result) = eval("[1,2,3].pop()").unwrap(); assert_eq!(result.as_int(), 3); } #[test] fn array_pop_empty_returns_undefined() { - let result = eval("[].pop()").unwrap(); + let (_vm, result) = eval("[].pop()").unwrap(); assert!(result.is_undefined()); } #[test] fn array_pop_reduces_length() { - let result = eval("var a = [1,2,3]; a.pop(); a").unwrap(); + let (_vm, result) = eval("var a = [1,2,3]; a.pop(); a").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); } #[test] fn array_slice_returns_subarray() { - let result = eval("[1,2,3,4].slice(1,3)").unwrap(); + let (_vm, result) = eval("[1,2,3,4].slice(1,3)").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); - assert_eq!(obj.get_prop_at(0), oxide_types::value::JsValue::int(2)); - assert_eq!(obj.get_prop_at(1), oxide_types::value::JsValue::int(3)); + assert_eq!(obj.get_prop_at(0), JsValue::int(2)); + assert_eq!(obj.get_prop_at(1), JsValue::int(3)); } #[test] fn array_slice_no_args_copies() { - let result = eval("[1,2,3].slice()").unwrap(); + let (_vm, result) = eval("[1,2,3].slice()").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 3); } #[test] fn array_concat_combines() { - let result = eval("[1,2].concat([3,4])").unwrap(); + let (_vm, result) = eval("[1,2].concat([3,4])").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 4); - assert_eq!(obj.get_prop_at(3), oxide_types::value::JsValue::int(4)); + assert_eq!(obj.get_prop_at(3), JsValue::int(4)); } #[test] fn array_concat_non_array() { - let result = eval("[1,2].concat(3)").unwrap(); + let (_vm, result) = eval("[1,2].concat(3)").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 3); } #[test] fn array_join_with_dash() { - let result = eval("['a','b','c'].join('-')").unwrap(); + let (_vm, result) = eval("['a','b','c'].join('-')").unwrap(); assert!(result.is_string()); } #[test] fn array_index_of_found() { - let result = eval("[10,20,30].indexOf(20)").unwrap(); + let (_vm, result) = eval("[10,20,30].indexOf(20)").unwrap(); assert_eq!(result.as_int(), 1); } #[test] fn array_index_of_not_found() { - let result = eval("[1,2,3].indexOf(99)").unwrap(); + let (_vm, result) = eval("[1,2,3].indexOf(99)").unwrap(); assert_eq!(result.as_int(), -1); } #[test] fn array_includes_true() { - let result = eval("[1,2,3].includes(2)").unwrap(); + let (_vm, result) = eval("[1,2,3].includes(2)").unwrap(); assert_eq!(result.as_bool(), true); } #[test] fn array_includes_false() { - let result = eval("[1,2,3].includes(99)").unwrap(); + let (_vm, result) = eval("[1,2,3].includes(99)").unwrap(); assert_eq!(result.as_bool(), false); } #[test] fn array_reverse_mutates() { - let result = eval("var a = [1,2,3]; a.reverse(); a").unwrap(); + let (_vm, result) = eval("var a = [1,2,3]; a.reverse(); a").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; - assert_eq!(obj.get_prop_at(0), oxide_types::value::JsValue::int(3)); - assert_eq!(obj.get_prop_at(1), oxide_types::value::JsValue::int(2)); - assert_eq!(obj.get_prop_at(2), oxide_types::value::JsValue::int(1)); + assert_eq!(obj.get_prop_at(0), JsValue::int(3)); + assert_eq!(obj.get_prop_at(1), JsValue::int(2)); + assert_eq!(obj.get_prop_at(2), JsValue::int(1)); } #[test] fn array_splice_remove() { - let result = eval("[1,2,3,4].splice(1,2)").unwrap(); + let (_vm, result) = eval("[1,2,3,4].splice(1,2)").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); - assert_eq!(obj.get_prop_at(0), oxide_types::value::JsValue::int(2)); - assert_eq!(obj.get_prop_at(1), oxide_types::value::JsValue::int(3)); + assert_eq!(obj.get_prop_at(0), JsValue::int(2)); + assert_eq!(obj.get_prop_at(1), JsValue::int(3)); } #[test] fn array_splice_insert() { - let result = eval("var a = [1,4]; a.splice(1,0,2,3); a").unwrap(); + let (_vm, result) = eval("var a = [1,4]; a.splice(1,0,2,3); a").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 4); } #[test] fn array_flat_noop() { - let result = eval("[1,2,3].flat()").unwrap(); + let (_vm, result) = eval("[1,2,3].flat()").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 3); } #[test] fn array_flat_one_level() { - let result = eval("[1,[2,3],4].flat()").unwrap(); + let (_vm, result) = eval("[1,[2,3],4].flat()").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 4); } #[test] fn array_literal_creates_array() { - let result = eval("[1,2,3]").unwrap(); + let (_vm, result) = eval("[1,2,3]").unwrap(); assert!(result.is_object()); let obj = unsafe { &*result.as_js_object_ptr() }; assert!(obj.is_array()); @@ -182,7 +184,7 @@ fn array_literal_creates_array() { #[test] fn array_empty_literal() { - let result = eval("[]").unwrap(); + let (_vm, result) = eval("[]").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert!(obj.is_array()); assert_eq!(obj.prop_count(), 0); @@ -190,63 +192,63 @@ fn array_empty_literal() { #[test] fn array_shift_removes_first() { - let result = eval("[1,2,3].shift()").unwrap(); + let (_vm, result) = eval("[1,2,3].shift()").unwrap(); assert_eq!(result.as_int(), 1); } #[test] fn array_unshift_adds_front() { - let result = eval("[3].unshift(1,2)").unwrap(); + let (_vm, result) = eval("[3].unshift(1,2)").unwrap(); assert_eq!(result.as_int(), 3); } #[test] fn array_fill_replaces() { - let result = eval("[1,2,3,4].fill(0,1,3)").unwrap(); + let (_vm, result) = eval("[1,2,3,4].fill(0,1,3)").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 4); } #[test] fn array_at_positive() { - let result = eval("[10,20,30].at(1)").unwrap(); + let (_vm, result) = eval("[10,20,30].at(1)").unwrap(); assert_eq!(result.as_int(), 20); } #[test] fn array_at_negative() { - let result = eval("[10,20,30].at(-1)").unwrap(); + let (_vm, result) = eval("[10,20,30].at(-1)").unwrap(); assert_eq!(result.as_int(), 30); } #[test] fn array_last_index_of_found() { - let result = eval("[1,2,3,2].lastIndexOf(2)").unwrap(); + let (_vm, result) = eval("[1,2,3,2].lastIndexOf(2)").unwrap(); assert_eq!(result.as_int(), 3); } #[test] fn array_last_index_of_not_found() { - let result = eval("[1,2,3].lastIndexOf(99)").unwrap(); + let (_vm, result) = eval("[1,2,3].lastIndexOf(99)").unwrap(); assert_eq!(result.as_int(), -1); } #[test] fn array_sort_default() { - let result = eval("[3,1,2].sort()").unwrap(); + let (_vm, result) = eval("[3,1,2].sort()").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 3); } #[test] fn array_copy_within_copies() { - let result = eval("[1,2,3,4,5].copyWithin(0,3)").unwrap(); + let (_vm, result) = eval("[1,2,3,4,5].copyWithin(0,3)").unwrap(); assert!(result.is_object()); } #[test] fn array_fill_single() { - let result = eval("[1,2,3].fill(0)").unwrap(); + let (_vm, result) = eval("[1,2,3].fill(0)").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.get_prop_at(0).as_int(), 0); } diff --git a/crates/oxide_vm/tests/builtins_function_tests.rs b/crates/oxide_vm/tests/builtins_function_tests.rs index e565b29..746a93d 100644 --- a/crates/oxide_vm/tests/builtins_function_tests.rs +++ b/crates/oxide_vm/tests/builtins_function_tests.rs @@ -38,4 +38,17 @@ fn function_to_string_includes_function() { let mut vm = Vm::new(); let result = eval(&mut vm, "Math.max.toString()").unwrap(); assert!(result.is_string()); + let rendered = vm + .kernel() + .string_forge() + .lookup(result.as_string_index()) + .unwrap_or_default(); + assert!(rendered.contains("function"), "got: {}", rendered); +} + +#[test] +fn function_to_string_non_function_throws() { + let mut vm = Vm::new(); + let err = eval(&mut vm, "Function.prototype.toString.call(1)").unwrap_err(); + assert!(err.contains("TypeError"), "got: {}", err); } diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 074fc22..a8e278c 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -1,7 +1,8 @@ use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; use oxide_vm::vm::Vm; -fn eval(source: &str) -> Result { +fn eval(source: &str) -> Result<(Vm, JsValue), String> { let allocator = oxide_parser::Allocator::default(); let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; @@ -9,12 +10,13 @@ fn eval(source: &str) -> Result { .compile(&program) .map_err(|e| format!("Compile error: {}", e))?; let mut vm = Vm::new(); - vm.run(&module) + let result = vm.run(&module)?; + Ok((vm, result)) } #[test] fn object_keys_empty() { - let result = eval("Object.keys({})").unwrap(); + let (_vm, result) = eval("Object.keys({})").unwrap(); assert!(result.is_object()); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 0); @@ -22,55 +24,55 @@ fn object_keys_empty() { #[test] fn object_keys_has_own_property() { - let result = eval("Object.keys({a:1,b:2})").unwrap(); + let (_vm, result) = eval("Object.keys({a:1,b:2})").unwrap(); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); } #[test] fn object_create_null_proto() { - eval("Object.create(null)").unwrap(); + let (_vm, _result) = eval("Object.create(null)").unwrap(); } #[test] fn object_assign_copies() { - let result = eval("Object.assign({a:1},{b:2})").unwrap(); + let (_vm, result) = eval("Object.assign({a:1},{b:2})").unwrap(); assert!(result.is_object()); } #[test] fn object_get_own_property_descriptor_value() { - let result = eval("Object.getOwnPropertyDescriptor({a:1},'a')").unwrap(); + let (_vm, result) = eval("Object.getOwnPropertyDescriptor({a:1},'a')").unwrap(); assert!(result.is_object()); } #[test] fn object_get_own_property_descriptor_missing() { - let result = eval("Object.getOwnPropertyDescriptor({},'x')").unwrap(); + let (_vm, result) = eval("Object.getOwnPropertyDescriptor({},'x')").unwrap(); assert!(result.is_undefined()); } #[test] fn object_define_property_sets_value() { - let result = eval("Object.defineProperty({},'x',{value:42})").unwrap(); + let (_vm, result) = eval("Object.defineProperty({},'x',{value:42})").unwrap(); assert!(result.is_object()); } #[test] fn object_get_prototype_of() { - let result = eval("Object.getPrototypeOf({})").unwrap(); + let (_vm, result) = eval("Object.getPrototypeOf({})").unwrap(); assert!(result.is_object() || result.is_null()); } #[test] fn object_has_own_true() { - let result = eval("Object.hasOwn({a:1}, 'a')").unwrap(); + let (_vm, result) = eval("Object.hasOwn({a:1}, 'a')").unwrap(); assert!(result.is_bool()); } #[test] fn object_entries_returns_array() { - let result = eval("Object.entries({a:1,b:2})").unwrap(); + let (_vm, result) = eval("Object.entries({a:1,b:2})").unwrap(); assert!(result.is_object()); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); @@ -78,7 +80,7 @@ fn object_entries_returns_array() { #[test] fn object_values_returns_array() { - let result = eval("Object.values({a:1,b:2})").unwrap(); + let (_vm, result) = eval("Object.values({a:1,b:2})").unwrap(); assert!(result.is_object()); let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2);