diff --git a/Cargo.lock b/Cargo.lock index d8ce851..fcb730f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,6 +850,7 @@ dependencies = [ "proptest", "regex", "ryu", + "serde_json", ] [[package]] diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 9117e74..651ae81 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -112,6 +112,7 @@ pub struct BuiltinWorld { pub uri_error_proto: P, pub eval_error_proto: P, pub math_object: P, + pub json_object: P, } fn intern_label(string_forge: &StringForge, label: &str) -> u32 { @@ -242,6 +243,8 @@ impl BuiltinWorld { let math_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let json_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + /// Overwrite placeholder slots (set up by make_pair) with real values. /// ctor.vec[0] = constructor.prototype -> proto /// proto.vec[0] = proto.constructor -> ctor @@ -293,6 +296,7 @@ impl BuiltinWorld { uri_error_proto, eval_error_proto, math_object, + json_object, } } diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 7d2f70b..30960d0 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -15,6 +15,7 @@ fast-float.workspace = true ryu.workspace = true memchr.workspace = true regex.workspace = true +serde_json.workspace = true [dev-dependencies] proptest.workspace = true diff --git a/crates/oxide_vm/src/builtins/json.rs b/crates/oxide_vm/src/builtins/json.rs new file mode 100644 index 0000000..9ef4f60 --- /dev/null +++ b/crates/oxide_vm/src/builtins/json.rs @@ -0,0 +1,236 @@ +use std::collections::HashSet; +use std::fmt::Write; + +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::builtins::object::walk_own_keys; +use crate::native::NativeResult; +use crate::vm::Vm; + +pub fn json_parse(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Err(crate::builtins::error::create_syntax_error( + vm, + "JSON.parse requires 1 argument", + )); + } + let val = vm.reg(args[1]); + if !val.is_string() { + return Err(crate::builtins::error::create_syntax_error( + vm, + "JSON.parse: argument is not a string", + )); + } + let text = { + let si = val.as_string_index(); + vm.kernel().string_forge().lookup(si).unwrap_or_default() + }; + + let parsed: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| crate::builtins::error::create_syntax_error(vm, &format!("{}", e)))?; + + Ok(value_to_jsvalue(vm, &parsed)) +} + +fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { + match val { + serde_json::Value::Null => JsValue::null(), + serde_json::Value::Bool(b) => JsValue::bool(*b), + serde_json::Value::Number(n) => { + if let Some(f) = n.as_f64() { + JsValue::float(f) + } else { + JsValue::float(0.0) + } + } + serde_json::Value::String(s) => vm.intern(s), + serde_json::Value::Array(arr) => { + let array_proto = vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject; + let n = arr.len(); + let array_obj = vm.epoch().alloc(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(array_proto), + n, + vm.epoch().bump(), + )); + for (i, v) in arr.iter().enumerate() { + let jsv = value_to_jsvalue(vm, v); + unsafe { + (*array_obj).set_prop_at(i as u8, jsv); + } + } + JsValue::from_js_object(array_obj) + } + serde_json::Value::Object(map) => { + let object_proto = vm.kernel().builtin_world().object_proto.as_ptr() as *mut JsObject; + let mut obj = + JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); + for (key, val) in map { + let si = vm.kernel().string_forge().intern(key).0; + let jsv = value_to_jsvalue(vm, val); + let new_shape = vm.kernel().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(new_shape); + obj.ensure_hash_props().push(Box::new(jsv)); + } + let obj_ptr = vm.epoch().alloc(obj); + JsValue::from_js_object(obj_ptr) + } + } +} + +pub fn json_stringify(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::undefined()); + } + let value = vm.reg(args[1]); + if value.is_undefined() { + return Ok(JsValue::undefined()); + } + let mut visited = HashSet::new(); + let mut output = String::new(); + jsvalue_to_json(vm, value, &mut visited, &mut output) + .map_err(|_| crate::builtins::error::create_type_error(vm, "cyclic object value"))?; + Ok(vm.intern(&output)) +} + +fn jsvalue_to_json( + vm: &Vm, + val: JsValue, + visited: &mut HashSet<*const u8>, + out: &mut String, +) -> Result<(), ()> { + if val.is_null() { + out.push_str("null"); + } else if val.is_undefined() { + } else if val.is_bool() { + out.push_str(if val.as_bool() { "true" } else { "false" }); + } else if val.is_int() { + write!(out, "{}", val.as_int()).unwrap(); + } else if val.is_double() { + let n = val.as_double(); + if !n.is_finite() { + out.push_str("null"); + } else if n.fract() == 0.0 && n.abs() < 1e21 { + write!(out, "{}", n as i64).unwrap(); + } else { + let mut buf = ryu::Buffer::new(); + out.push_str(buf.format(n)); + } + } else if val.is_string() { + let si = val.as_string_index(); + let s = vm.kernel().string_forge().lookup(si).unwrap_or_default(); + stringify_string(&s, out); + } else if val.is_object() { + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + out.push_str("null"); + return Ok(()); + } + + if visited.contains(&(obj_ptr as *const u8)) { + return Err(()); + } + visited.insert(obj_ptr as *const u8); + + let obj = unsafe { &*obj_ptr }; + + if obj.is_array() { + stringify_array(vm, obj, visited, out)?; + } else { + stringify_object(vm, obj, visited, out)?; + } + + visited.remove(&(obj_ptr as *const u8)); + } + Ok(()) +} + +fn stringify_string(s: &str, out: &mut String) { + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c.is_control() => { + let mut buf = [0u16; 2]; + let encoded = c.encode_utf16(&mut buf); + for unit in encoded { + out.push_str(&format!("\\u{:04x}", unit)); + } + } + _ => out.push(c), + } + } + out.push('"'); +} + +fn stringify_object( + vm: &Vm, + obj: &JsObject, + visited: &mut HashSet<*const u8>, + out: &mut String, +) -> Result<(), ()> { + out.push('{'); + let keys = walk_own_keys(vm, obj); + let mut first = true; + for (si, pos) in keys { + let val = obj.get_prop_at(pos); + let is_function = val.is_object() && { + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + unsafe { (*ptr).is_function() } + } + }; + if val.is_undefined() || is_function { + continue; + } + if !first { + out.push(','); + } + first = false; + let name = vm.kernel().string_forge().lookup(si).unwrap_or_default(); + stringify_string(&name, out); + out.push(':'); + jsvalue_to_json(vm, val, visited, out)?; + } + out.push('}'); + Ok(()) +} + +fn stringify_array( + vm: &Vm, + obj: &JsObject, + visited: &mut HashSet<*const u8>, + out: &mut String, +) -> Result<(), ()> { + out.push('['); + let len = obj.prop_vec_len(); + for i in 0..len { + if i > 0 { + out.push(','); + } + let val = obj.get_prop_at(i as u8); + let is_function = val.is_object() && { + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + unsafe { (*ptr).is_function() } + } + }; + if is_function || val.is_undefined() { + out.push_str("null"); + } else { + jsvalue_to_json(vm, val, visited, out)?; + } + } + out.push(']'); + Ok(()) +} diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index b487f28..7b883ac 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -2,6 +2,7 @@ pub mod array; pub mod error; pub mod function; pub mod global; +pub mod json; pub mod math; pub mod number; pub mod object; diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index f600ae8..a297fa5 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -7,7 +7,7 @@ use crate::coercion; use crate::native::NativeResult; use crate::vm::Vm; -fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u8)> { +pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u8)> { let mut keys: Vec<(u32, u8)> = Vec::new(); let shape_id = obj.shape_id(); let mut pos: u8 = 0; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 6d62e70..14bf521 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -670,6 +670,38 @@ fn bind_math(kernel: &Arc, global: &mut JsObject) { global.bump_generation(); } +fn bind_json(kernel: &Arc, global: &mut JsObject) { + let json_ptr = kernel.builtin_world().json_object.as_ptr() as *mut JsObject; + let json = unsafe { &mut *json_ptr }; + + bind_method!( + kernel.builtin_world(), + json, + kernel.string_forge().as_ref(), + kernel.shape_forge().as_ref(), + "parse", + crate::builtins::json::json_parse, + 1 + ); + bind_method!( + kernel.builtin_world(), + json, + kernel.string_forge().as_ref(), + kernel.shape_forge().as_ref(), + "stringify", + crate::builtins::json::json_stringify, + 1 + ); + + let si_j = kernel.string_forge().intern("JSON").0; + let j_shape = kernel.shape_forge().make_shape(global.shape_id(), si_j); + let j_val = + JsValue::from_js_object(kernel.builtin_world().json_object.as_ptr() as *mut JsObject); + global.set_shape_id(j_shape); + global.ensure_hash_props().push(Box::new(j_val)); + global.bump_generation(); +} + fn bind_function(kernel: &Arc) { let function_methods = FunctionMethods { call: crate::builtins::function::function_call as *const (), @@ -693,6 +725,7 @@ pub fn init_kernel_builtins(kernel: &Arc) { bind_string(kernel, global); bind_number(kernel, global); bind_math(kernel, global); + bind_json(kernel, global); bind_function(kernel); } diff --git a/crates/oxide_vm/tests/json_tests.rs b/crates/oxide_vm/tests/json_tests.rs new file mode 100644 index 0000000..d1069ce --- /dev/null +++ b/crates/oxide_vm/tests/json_tests.rs @@ -0,0 +1,295 @@ +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) +} + +fn string_value(vm: &Vm, val: JsValue) -> String { + vm.kernel() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +// -- JSON.parse -- + +#[test] +fn json_parse_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('{\"a\":1,\"b\":2}')").unwrap(); + assert!(result.is_object()); + let obj = unsafe { &*result.as_js_object_ptr() }; + let mut count = 0; + for i in 0..obj.prop_vec_len() { + if !obj.get_prop_at(i as u8).is_undefined() { + count += 1; + } + } + assert_eq!(count, 2); +} + +#[test] +fn json_parse_array() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('[1,2,3]')").unwrap(); + assert!(result.is_object()); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert!(obj.is_array()); + assert_eq!(obj.prop_vec_len(), 3); +} + +#[test] +fn json_parse_string() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('\"hello\"')").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "hello"); +} + +#[test] +fn json_parse_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('42')").unwrap(); + assert!(result.is_double()); +} + +#[test] +fn json_parse_float() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('3.14')").unwrap(); + assert!(result.is_double()); +} + +#[test] +fn json_parse_true() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('true')").unwrap(); + assert!(result.is_bool()); + assert!(result.as_bool()); +} + +#[test] +fn json_parse_false() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('false')").unwrap(); + assert!(result.is_bool()); + assert!(!result.as_bool()); +} + +#[test] +fn json_parse_null() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('null')").unwrap(); + assert!(result.is_null()); +} + +#[test] +fn json_parse_nested() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "JSON.parse('{\"a\":1,\"b\":[1,2],\"c\":{\"d\":\"hello\"}}')", + ) + .unwrap(); + assert!(result.is_object()); +} + +#[test] +fn json_parse_invalid_throws() { + let mut vm = Vm::new(); + let err = eval(&mut vm, "JSON.parse('not json')"); + assert!(err.is_err()); +} + +// -- JSON.stringify -- + +#[test] +fn json_stringify_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify({a:1})").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "{\"a\":1}"); +} + +#[test] +fn json_stringify_array() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify([1,2,3])").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "[1,2,3]"); +} + +#[test] +fn json_stringify_string() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify('hello')").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "\"hello\""); +} + +#[test] +fn json_stringify_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(42)").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "42"); +} + +#[test] +fn json_stringify_float() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(3.14)").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert!(s.starts_with("3.14")); +} + +#[test] +fn json_stringify_true() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(true)").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "true"); +} + +#[test] +fn json_stringify_false() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(false)").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "false"); +} + +#[test] +fn json_stringify_null() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(null)").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "null"); +} + +#[test] +fn json_stringify_undefined() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(undefined)").unwrap(); + assert!(result.is_undefined()); +} + +// -- Roundtrip -- + +#[test] +fn json_parse_stringify_roundtrip() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse(JSON.stringify({a:1,b:2}))").unwrap(); + assert!(result.is_object()); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(obj.prop_vec_len(), 2); +} + +#[test] +fn json_roundtrip_array() { + let mut vm = Vm::new(); + let _ = eval( + &mut vm, + "var arr = JSON.parse(JSON.stringify([1,2,3])); arr", + ) + .unwrap(); +} + +// -- Edge cases -- + +#[test] +fn json_stringify_empty_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify({})").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "{}"); +} + +#[test] +fn json_stringify_empty_array() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify([])").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "[]"); +} + +#[test] +fn json_stringify_escape_quotes() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify('he\"llo')").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "\"he\\\"llo\""); +} + +#[test] +fn json_stringify_escape_backslash() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify('a\\\\b')").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "\"a\\\\b\""); +} + +#[test] +fn json_stringify_nested_objects() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify({a: {b: 1}})").unwrap(); + assert!(result.is_string()); + let s = string_value(&vm, result); + assert_eq!(s, "{\"a\":{\"b\":1}}"); +} + +#[test] +fn json_parse_empty_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('{}')").unwrap(); + assert!(result.is_object()); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(obj.prop_vec_len(), 0); +} + +#[test] +fn json_parse_empty_array() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('[]')").unwrap(); + assert!(result.is_object()); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert!(obj.is_array()); + assert_eq!(obj.prop_vec_len(), 0); +} + +#[test] +fn json_parse_int_is_double() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('123')").unwrap(); + assert!((result.as_double() - 123.0).abs() < 0.0001); +} + +// -- Cycle detection -- + +#[test] +fn json_stringify_cycle_throws_type_error() { + let mut vm = Vm::new(); + let err = eval(&mut vm, "var a = {}; a.self = a; JSON.stringify(a)"); + assert!(err.is_err()); +}