1use crate::format::{IrError, SlotType};
2use crate::parser::{IrModule, SlotTable};
3use std::borrow::Cow;
4use std::sync::Arc;
5
6#[derive(Debug, Clone)]
13pub enum SlotValue {
14 Null,
16 Text(String),
18 Bool(bool),
22 Number(f64),
24 Array(Arc<[SlotValue]>),
27 Object(Arc<[(String, SlotValue)]>),
29}
30
31static NULL_SLOT: SlotValue = SlotValue::Null;
33
34const JS_SAFE_INTEGER_LIMIT: f64 = 9007199254740992.0;
36
37fn format_js_number(n: f64) -> String {
44 ryu_js::Buffer::new().format(n).to_owned()
45}
46
47impl SlotValue {
48 pub(crate) fn text_ref<'a>(&'a self, number: &'a mut ryu_js::Buffer) -> Cow<'a, str> {
52 match self {
53 Self::Null => Cow::Borrowed(""),
54 Self::Text(text) => Cow::Borrowed(text),
55 Self::Bool(true) => Cow::Borrowed("true"),
56 Self::Bool(false) => Cow::Borrowed("false"),
57 Self::Number(value) => Cow::Borrowed(number.format(*value)),
58 Self::Object(_) => Cow::Borrowed("[object Object]"),
59 Self::Array(_) => Cow::Owned(self.to_text()),
60 }
61 }
62
63 pub fn to_text(&self) -> String {
69 match self {
70 SlotValue::Null => String::new(),
71 SlotValue::Text(s) => s.clone(),
72 SlotValue::Bool(true) => "true".to_string(),
73 SlotValue::Bool(false) => "false".to_string(),
74 SlotValue::Number(n) => format_js_number(*n),
75 SlotValue::Array(items) => {
81 let mut out = String::new();
82 for (i, item) in items.iter().enumerate() {
83 if i > 0 {
84 out.push(',');
85 }
86 out.push_str(&item.to_text());
87 }
88 out
89 }
90 SlotValue::Object(_) => "[object Object]".to_string(),
92 }
93 }
94
95 pub fn array(items: Vec<SlotValue>) -> Self {
100 SlotValue::Array(items.into())
101 }
102
103 pub fn object(pairs: Vec<(String, SlotValue)>) -> Self {
105 SlotValue::Object(pairs.into())
106 }
107
108 pub fn as_bool(&self) -> bool {
121 match self {
122 SlotValue::Null => false,
123 SlotValue::Bool(b) => *b,
124 SlotValue::Text(s) => !s.is_empty(),
125 SlotValue::Number(n) => *n != 0.0 && !n.is_nan(),
126 SlotValue::Array(_) | SlotValue::Object(_) => true,
127 }
128 }
129
130 pub fn get_property(&self, name: &str) -> SlotValue {
132 match self {
133 SlotValue::Object(pairs) => {
134 for (k, v) in pairs.iter() {
135 if k == name {
136 return v.clone();
137 }
138 }
139 SlotValue::Null
140 }
141 _ => SlotValue::Null,
142 }
143 }
144
145 pub fn as_array(&self) -> Option<&[SlotValue]> {
147 match self {
148 SlotValue::Array(a) => Some(a),
149 _ => None,
150 }
151 }
152
153 pub fn as_text_ref(&self) -> &str {
156 match self {
157 SlotValue::Text(s) => s.as_str(),
158 _ => "",
159 }
160 }
161
162 pub fn to_json(&self) -> serde_json::Value {
173 match self {
174 SlotValue::Null => serde_json::Value::Null,
175 SlotValue::Text(s) => serde_json::Value::String(s.clone()),
176 SlotValue::Bool(b) => serde_json::Value::Bool(*b),
177 SlotValue::Number(n) => {
178 if n.fract() == 0.0 && n.abs() < JS_SAFE_INTEGER_LIMIT {
182 serde_json::Value::Number(serde_json::Number::from(*n as i64))
183 } else {
184 serde_json::Number::from_f64(*n)
185 .map(serde_json::Value::Number)
186 .unwrap_or(serde_json::Value::Null)
187 }
188 }
189 SlotValue::Array(items) => {
190 serde_json::Value::Array(items.iter().map(|v| v.to_json()).collect())
191 }
192 SlotValue::Object(pairs) => {
193 let map: serde_json::Map<String, serde_json::Value> = pairs
194 .iter()
195 .map(|(k, v)| (k.clone(), v.to_json()))
196 .collect();
197 serde_json::Value::Object(map)
198 }
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
208pub struct SlotData {
209 slots: Vec<SlotValue>,
210}
211
212impl SlotData {
213 pub fn new(capacity: usize) -> Self {
220 let mut slots = Vec::with_capacity(capacity);
221 slots.resize_with(capacity, || SlotValue::Null);
222 Self { slots }
223 }
224
225 pub fn set(&mut self, slot_id: u16, value: SlotValue) {
239 self.replace(slot_id, value);
240 }
241
242 pub fn replace(&mut self, slot_id: u16, value: SlotValue) -> SlotValue {
252 let idx = slot_id as usize;
253 if idx >= self.slots.len() {
254 #[cfg(feature = "tracing")]
255 tracing::warn!(
256 slot_id = slot_id,
257 len = self.slots.len(),
258 "Slot write past the end of SlotData — growing (SlotData sized smaller than the module's slot table)"
259 );
260 self.slots.resize_with(idx + 1, || SlotValue::Null);
261 }
262 std::mem::replace(&mut self.slots[idx], value)
263 }
264
265 pub fn get(&self, slot_id: u16) -> &SlotValue {
267 let idx = slot_id as usize;
268 self.slots.get(idx).unwrap_or(&NULL_SLOT)
269 }
270
271 pub fn get_text(&self, slot_id: u16) -> Option<&str> {
273 match self.get(slot_id) {
274 SlotValue::Text(s) => Some(s.as_str()),
275 _ => None,
276 }
277 }
278
279 pub fn from_json(json_str: &str, module: &IrModule) -> Result<Self, IrError> {
295 let parsed: serde_json::Value =
296 serde_json::from_str(json_str).map_err(|e| IrError::JsonParseError(e.to_string()))?;
297
298 let obj = match parsed {
299 serde_json::Value::Object(map) => map,
300 _ => return Err(IrError::JsonParseError("expected JSON object".to_string())),
301 };
302
303 let mut name_to_slot: std::collections::HashMap<&str, u16> =
311 std::collections::HashMap::new();
312 for entry in module.slots.entries() {
313 if let Ok(name) = module.strings.get(entry.name_str_idx) {
314 name_to_slot.entry(name).or_insert(entry.slot_id);
315 }
316 }
317
318 let mut data = Self::new_from_defaults(&module.slots);
320
321 for (key, value) in &obj {
323 if let Some(&slot_id) = name_to_slot.get(key.as_str()) {
324 data.set(slot_id, json_to_slot_value(value));
325 }
326 }
328
329 Ok(data)
330 }
331
332 pub(crate) fn apply_module_constants(&mut self, module: &IrModule) {
333 if let Some(contracts) = &module.contracts {
334 for (&id, value) in &contracts.constants {
335 self.replace(id, value.clone());
336 }
337 }
338 }
339
340 pub fn new_from_defaults(table: &SlotTable) -> Self {
344 let entries = table.entries();
345 let capacity = entries
346 .iter()
347 .map(|e| e.slot_id as usize + 1)
348 .max()
349 .unwrap_or(0);
350 let mut slots = Vec::with_capacity(capacity);
351 slots.resize_with(capacity, || SlotValue::Null);
352
353 for entry in entries {
354 let idx = entry.slot_id as usize;
355 if idx >= slots.len() {
356 continue;
357 }
358 if entry.default_bytes.is_empty() {
359 continue;
360 }
361
362 let default_str = std::str::from_utf8(&entry.default_bytes).unwrap_or("");
363 let value = match entry.type_hint {
364 SlotType::Bool => match default_str {
365 "true" => SlotValue::Bool(true),
366 "false" => SlotValue::Bool(false),
367 _ => SlotValue::Null,
368 },
369 SlotType::Text => SlotValue::Text(default_str.to_string()),
370 SlotType::Number => default_str
371 .parse::<f64>()
372 .map(SlotValue::Number)
373 .unwrap_or(SlotValue::Null),
374 SlotType::Array => {
375 if default_str == "[]" {
376 SlotValue::array(vec![])
377 } else {
378 SlotValue::Null
379 }
380 }
381 SlotType::Object => SlotValue::Null,
382 };
383 slots[idx] = value;
384 }
385
386 Self { slots }
387 }
388}
389
390pub fn json_to_slot_value(value: &serde_json::Value) -> SlotValue {
400 match value {
401 serde_json::Value::Null => SlotValue::Null,
402 serde_json::Value::String(s) => SlotValue::Text(s.clone()),
403 serde_json::Value::Bool(b) => SlotValue::Bool(*b),
404 serde_json::Value::Number(n) => SlotValue::Number(n.as_f64().unwrap_or(0.0)),
405 serde_json::Value::Array(arr) => {
406 SlotValue::Array(arr.iter().map(json_to_slot_value).collect())
407 }
408 serde_json::Value::Object(map) => SlotValue::Object(
409 map.iter()
410 .map(|(k, v)| (k.clone(), json_to_slot_value(v)))
411 .collect(),
412 ),
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::parser::{IrModule, SlotTable};
420
421 fn build_slot_table_bytes(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
424 let mut buf = Vec::new();
425 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
426 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
427 buf.extend_from_slice(&slot_id.to_le_bytes());
428 buf.extend_from_slice(&name_str_idx.to_le_bytes());
429 buf.push(type_hint);
430 buf.push(source);
431 buf.extend_from_slice(&(default_bytes.len() as u16).to_le_bytes());
432 buf.extend_from_slice(default_bytes);
433 }
434 buf
435 }
436
437 #[test]
438 fn slot_value_text_to_text() {
439 assert_eq!(SlotValue::Text("hello".to_string()).to_text(), "hello");
440 }
441
442 #[test]
443 #[allow(clippy::excessive_precision)]
445 fn borrowed_scalar_text_preserves_ecmascript_conversion() {
446 let values = vec![
447 SlotValue::Null,
448 SlotValue::Text("\0\r\n<&>\"é🦀".into()),
449 SlotValue::Bool(true),
450 SlotValue::Bool(false),
451 SlotValue::Number(-0.0),
452 SlotValue::Number(f64::NAN),
453 SlotValue::Number(f64::INFINITY),
454 SlotValue::Number(f64::NEG_INFINITY),
455 SlotValue::Number(1e-7),
456 SlotValue::Number(1e21),
457 SlotValue::Number(-270129723325078.625),
458 SlotValue::object(vec![]),
459 SlotValue::array(vec![
460 SlotValue::Text("\r".into()),
461 SlotValue::array(vec![SlotValue::Null, SlotValue::Text("\n".into())]),
462 SlotValue::Number(42.0),
463 ]),
464 ];
465 for value in values {
466 let mut number = ryu_js::Buffer::new();
467 let borrowed = value.text_ref(&mut number);
468 assert_eq!(borrowed, value.to_text(), "{value:?}");
469 assert_eq!(
470 matches!(borrowed, Cow::Owned(_)),
471 matches!(value, SlotValue::Array(_))
472 );
473 }
474 }
475
476 #[test]
477 fn slot_value_bool_to_text() {
478 assert_eq!(SlotValue::Bool(true).to_text(), "true");
479 assert_eq!(SlotValue::Bool(false).to_text(), "false");
480 }
481
482 #[test]
483 fn slot_value_number_integer() {
484 assert_eq!(SlotValue::Number(42.0).to_text(), "42");
485 }
486
487 #[test]
488 fn slot_value_number_float() {
489 assert_eq!(SlotValue::Number(3.15).to_text(), "3.15");
490 }
491
492 #[test]
493 fn slot_value_null_to_text() {
494 assert_eq!(SlotValue::Null.to_text(), "");
495 }
496
497 #[test]
498 fn as_bool_matches_javascript_tobool() {
499 assert!(SlotValue::Text("x".to_string()).as_bool());
503 assert!(SlotValue::Number(1.0).as_bool());
504 assert!(SlotValue::Number(-1.0).as_bool());
505 assert!(SlotValue::Bool(true).as_bool());
506 assert!(SlotValue::array(vec![SlotValue::Null]).as_bool());
507 assert!(SlotValue::object(vec![]).as_bool());
508 assert!(SlotValue::array(vec![]).as_bool());
511
512 assert!(!SlotValue::Null.as_bool());
513 assert!(!SlotValue::Text("".to_string()).as_bool());
514 assert!(!SlotValue::Number(0.0).as_bool());
515 assert!(!SlotValue::Number(-0.0).as_bool());
516 assert!(!SlotValue::Number(f64::NAN).as_bool());
517 assert!(!SlotValue::Bool(false).as_bool());
518 }
519
520 #[test]
521 fn to_text_matches_javascript_string_for_containers() {
522 assert_eq!(SlotValue::array(vec![]).to_text(), "");
524 assert_eq!(
525 SlotValue::array(vec![
526 SlotValue::Text("a".into()),
527 SlotValue::Text("b".into())
528 ])
529 .to_text(),
530 "a,b"
531 );
532 assert_eq!(
534 SlotValue::array(vec![
535 SlotValue::Null,
536 SlotValue::Number(1.0),
537 SlotValue::Null
538 ])
539 .to_text(),
540 ",1,"
541 );
542 assert_eq!(
544 SlotValue::array(vec![
545 SlotValue::array(vec![SlotValue::Number(1.0), SlotValue::Number(2.0)]),
546 SlotValue::array(vec![SlotValue::Number(3.0)]),
547 ])
548 .to_text(),
549 "1,2,3"
550 );
551 assert_eq!(SlotValue::object(vec![]).to_text(), "[object Object]");
552 assert_eq!(
553 SlotValue::array(vec![SlotValue::object(vec![])]).to_text(),
554 "[object Object]"
555 );
556 }
557
558 #[test]
559 fn slot_value_as_array() {
560 let arr = SlotValue::array(vec![SlotValue::Text("a".to_string())]);
561 assert!(arr.as_array().is_some());
562 assert_eq!(arr.as_array().unwrap().len(), 1);
563
564 assert!(SlotValue::Null.as_array().is_none());
565 assert!(SlotValue::Text("x".to_string()).as_array().is_none());
566 assert!(SlotValue::Number(1.0).as_array().is_none());
567 assert!(SlotValue::Bool(true).as_array().is_none());
568 assert!(SlotValue::object(vec![]).as_array().is_none());
569 }
570
571 #[test]
572 fn slot_data_set_and_get() {
573 let mut data = SlotData::new(4);
574 data.set(0, SlotValue::Text("title".to_string()));
575 data.set(2, SlotValue::Number(99.0));
576
577 assert_eq!(data.get(0).to_text(), "title");
578 assert_eq!(data.get(2).to_text(), "99");
579 assert_eq!(data.get(1).to_text(), "");
581 }
582
583 #[test]
584 fn slot_data_out_of_bounds() {
585 let data = SlotData::new(2);
586 assert_eq!(data.get(5).to_text(), "");
588 assert!(!data.get(100).as_bool());
589 }
590
591 #[test]
592 fn slot_data_get_text() {
593 let mut data = SlotData::new(3);
594 data.set(0, SlotValue::Text("hello".to_string()));
595 data.set(1, SlotValue::Number(42.0));
596
597 assert_eq!(data.get_text(0), Some("hello"));
598 assert_eq!(data.get_text(1), None); assert_eq!(data.get_text(2), None); }
601
602 #[test]
605 fn slot_data_new_from_defaults_basic() {
606 let bytes = build_slot_table_bytes(&[
611 (0, 0, 0x04, 0x00, b"[]"), (1, 1, 0x02, 0x01, b"false"), (2, 2, 0x01, 0x01, b"hello"), ]);
615 let table = SlotTable::parse(&bytes).unwrap();
616 let data = SlotData::new_from_defaults(&table);
617
618 assert!(matches!(data.get(0), SlotValue::Array(v) if v.is_empty()));
620 assert!(matches!(data.get(1), SlotValue::Bool(false)));
622 assert_eq!(data.get_text(2), Some("hello"));
624 }
625
626 #[test]
627 fn slot_data_new_from_defaults_empty_table() {
628 let bytes = build_slot_table_bytes(&[]);
630 let table = SlotTable::parse(&bytes).unwrap();
631 let data = SlotData::new_from_defaults(&table);
632
633 assert!(matches!(data.get(0), SlotValue::Null));
635 }
636
637 #[test]
638 fn slot_data_new_from_defaults_no_default_bytes() {
639 let bytes = build_slot_table_bytes(&[
641 (0, 0, 0x01, 0x00, b""), (1, 1, 0x03, 0x01, b""), ]);
644 let table = SlotTable::parse(&bytes).unwrap();
645 let data = SlotData::new_from_defaults(&table);
646
647 assert!(matches!(data.get(0), SlotValue::Null));
648 assert!(matches!(data.get(1), SlotValue::Null));
649 }
650
651 #[test]
652 fn slot_data_new_from_defaults_number() {
653 let bytes = build_slot_table_bytes(&[
655 (0, 0, 0x03, 0x00, b"42"), (1, 1, 0x03, 0x01, b"3.15"), ]);
658 let table = SlotTable::parse(&bytes).unwrap();
659 let data = SlotData::new_from_defaults(&table);
660
661 assert!(matches!(data.get(0), SlotValue::Number(n) if (*n - 42.0).abs() < f64::EPSILON));
662 assert!(matches!(data.get(1), SlotValue::Number(n) if (*n - 3.15).abs() < f64::EPSILON));
663 let bad = build_slot_table_bytes(&[(2, 2, 0x03, 0, b"nope")]);
664 assert_eq!(
665 SlotTable::parse(&bad).unwrap_err(),
666 IrError::InvalidDefault(2)
667 );
668 }
669
670 #[test]
671 fn slot_data_new_from_defaults_bool_true() {
672 let bytes = build_slot_table_bytes(&[
673 (0, 0, 0x02, 0x00, b"true"), ]);
675 let table = SlotTable::parse(&bytes).unwrap();
676 let data = SlotData::new_from_defaults(&table);
677
678 assert!(matches!(data.get(0), SlotValue::Bool(true)));
679 }
680
681 #[test]
682 fn slot_data_nonempty_object_default_is_refused_at_load() {
683 let bytes = build_slot_table_bytes(&[(0, 0, 0x05, 0, b"{}")]);
684 assert_eq!(
685 SlotTable::parse(&bytes).unwrap_err(),
686 IrError::InvalidDefault(0)
687 );
688 }
689
690 use crate::parser::test_helpers::{build_minimal_ir, encode_text};
693
694 fn build_module_for_json(
696 strings: &[&str],
697 slot_decls: &[(u16, u32, u8, u8, &[u8])],
698 ) -> IrModule {
699 let opcodes = encode_text(0);
701 let data = build_minimal_ir(strings, slot_decls, &opcodes, &[]);
702 IrModule::parse(&data).unwrap()
703 }
704
705 #[test]
706 fn from_json_basic() {
707 let module = build_module_for_json(
711 &["title", "count"],
712 &[
713 (0, 0, 0x01, 0x00, &[]), (1, 1, 0x03, 0x00, &[]), ],
716 );
717
718 let data = SlotData::from_json(r#"{"title": "Hello", "count": 42}"#, &module).unwrap();
719 assert_eq!(data.get(0).to_text(), "Hello");
720 assert_eq!(data.get(1).to_text(), "42");
721 }
722
723 #[test]
724 fn from_json_missing_key_uses_default() {
725 let module = build_module_for_json(
727 &["greeting"],
728 &[
729 (0, 0, 0x01, 0x00, b"hi"), ],
731 );
732
733 let data = SlotData::from_json(r#"{}"#, &module).unwrap();
735 assert_eq!(data.get(0).to_text(), "hi");
736 }
737
738 #[test]
739 fn from_json_null_value() {
740 let module = build_module_for_json(&["name"], &[(0, 0, 0x01, 0x00, &[])]);
741
742 let data = SlotData::from_json(r#"{"name": null}"#, &module).unwrap();
743 assert!(matches!(data.get(0), SlotValue::Null));
744 }
745
746 #[test]
747 fn from_json_bool_values() {
748 let module = build_module_for_json(
749 &["active", "hidden"],
750 &[
751 (0, 0, 0x02, 0x00, &[]), (1, 1, 0x02, 0x00, &[]), ],
754 );
755
756 let data = SlotData::from_json(r#"{"active": true, "hidden": false}"#, &module).unwrap();
757 assert!(matches!(data.get(0), SlotValue::Bool(true)));
758 assert!(matches!(data.get(1), SlotValue::Bool(false)));
759 }
760
761 #[test]
762 fn from_json_array() {
763 let module = build_module_for_json(
764 &["items"],
765 &[(0, 0, 0x04, 0x00, &[])], );
767
768 let data = SlotData::from_json(r#"{"items": ["a", "b", 3]}"#, &module).unwrap();
769 if let SlotValue::Array(arr) = data.get(0) {
770 assert_eq!(arr.len(), 3);
771 assert_eq!(arr[0].to_text(), "a");
772 assert_eq!(arr[1].to_text(), "b");
773 assert_eq!(arr[2].to_text(), "3");
774 } else {
775 panic!("expected Array, got {:?}", data.get(0));
776 }
777 }
778
779 #[test]
780 fn from_json_nested_object() {
781 let module = build_module_for_json(
782 &["config"],
783 &[(0, 0, 0x05, 0x00, &[])], );
785
786 let data = SlotData::from_json(r#"{"config": {"key": "value", "n": 7}}"#, &module).unwrap();
787 if let SlotValue::Object(pairs) = data.get(0) {
788 assert_eq!(pairs.len(), 2);
789 assert_eq!(pairs[0].0, "key");
790 assert_eq!(pairs[0].1.to_text(), "value");
791 assert_eq!(pairs[1].0, "n");
792 assert_eq!(pairs[1].1.to_text(), "7");
793 } else {
794 panic!("expected Object, got {:?}", data.get(0));
795 }
796 }
797
798 #[test]
799 fn from_json_unknown_key_ignored() {
800 let module = build_module_for_json(&["title"], &[(0, 0, 0x01, 0x00, &[])]);
801
802 let data =
804 SlotData::from_json(r#"{"title": "Hi", "extra_key": "ignored"}"#, &module).unwrap();
805 assert_eq!(data.get(0).to_text(), "Hi");
806 }
807
808 #[test]
809 fn from_json_invalid_json() {
810 let module = build_module_for_json(&["x"], &[(0, 0, 0x01, 0x00, &[])]);
811
812 let result = SlotData::from_json(r#"not valid json"#, &module);
813 assert!(result.is_err());
814 match result.unwrap_err() {
815 crate::format::IrError::JsonParseError(_) => {} other => panic!("expected JsonParseError, got {other:?}"),
817 }
818 }
819
820 #[test]
823 fn number_to_text_matches_javascript_string() {
824 let cases: &[(f64, &str)] = &[
827 (0.0, "0"),
828 (-0.0, "0"),
832 (1.0, "1"),
833 (-42.0, "-42"),
834 (1.5, "1.5"),
835 (123.456, "123.456"),
836 (0.1, "0.1"),
837 (1e16, "10000000000000000"),
841 (1e20, "100000000000000000000"),
842 (1e21, "1e+21"),
845 (1e300, "1e+300"),
846 (9007199254740992.0, "9007199254740992"), (9007199254740994.0, "9007199254740994"), (1e-6, "0.000001"),
849 (1e-7, "1e-7"),
850 (1.5e-7, "1.5e-7"),
851 (-1.5e-7, "-1.5e-7"),
852 (f64::MAX, "1.7976931348623157e+308"),
853 (f64::INFINITY, "Infinity"),
855 (f64::NEG_INFINITY, "-Infinity"),
856 (f64::NAN, "NaN"),
857 ];
858 for (value, expected) in cases {
859 assert_eq!(
860 SlotValue::Number(*value).to_text(),
861 *expected,
862 "String({value:?})"
863 );
864 }
865 }
866
867 #[test]
868 fn number_to_text_never_saturates_above_i64() {
869 let huge = 1e19_f64; assert_eq!(SlotValue::Number(huge).to_text(), "10000000000000000000");
873 assert!(!SlotValue::Number(huge)
874 .to_text()
875 .contains("9223372036854775807"));
876 }
877
878 #[test]
879 fn number_to_json_round_trips_by_value_not_by_text() {
880 for value in [0.0_f64, -0.0, 1.5, 1e16, 1e21, 1e300, -1e19] {
884 let json = SlotValue::Number(value).to_json();
885 let parsed: f64 = json.as_f64().unwrap_or_else(|| panic!("{value} -> {json}"));
886 assert_eq!(parsed, value, "value {value} did not round-trip");
887 }
888 }
889
890 #[test]
891 fn number_to_json_non_finite_is_null_matching_js() {
892 for value in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
895 assert_eq!(SlotValue::Number(value).to_json(), serde_json::Value::Null);
896 }
897 }
898
899 #[test]
902 fn set_grows_instead_of_silently_dropping() {
903 let mut data = SlotData::new(1);
906 data.set(5, SlotValue::Text("late".into()));
907 assert_eq!(data.get(5).to_text(), "late");
908 assert!(matches!(data.get(3), SlotValue::Null));
910 }
911}