1use crate::format::{IrError, SlotType};
2use crate::parser::{IrModule, SlotTable};
3use std::sync::Arc;
4
5#[derive(Debug, Clone)]
12pub enum SlotValue {
13 Null,
15 Text(String),
17 Bool(bool),
20 Number(f64),
22 Array(Arc<[SlotValue]>),
25 Object(Arc<[(String, SlotValue)]>),
27}
28
29static NULL_SLOT: SlotValue = SlotValue::Null;
31
32const JS_SAFE_INTEGER_LIMIT: f64 = 9007199254740992.0;
35
36fn format_js_number(n: f64) -> String {
45 if n.is_nan() {
46 return "NaN".to_string();
48 }
49 if n.is_infinite() {
50 return if n > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
51 }
52
53 if n.fract() == 0.0 && n.abs() < JS_SAFE_INTEGER_LIMIT {
57 return format!("{}", n as i64);
58 }
59
60 let exp_form = format!("{:e}", n);
62 let (mantissa, exp_str) = match exp_form.split_once('e') {
63 Some(parts) => parts,
64 None => return exp_form, };
66 let exp: i32 = match exp_str.parse() {
67 Ok(e) => e,
68 Err(_) => return exp_form, };
70
71 let negative = mantissa.starts_with('-');
72 let digits: String = mantissa
73 .trim_start_matches('-')
74 .chars()
75 .filter(|c| *c != '.')
76 .collect();
77 let k = digits.len() as i32; let pos = exp + 1; let mut out = String::with_capacity(digits.len() + 8);
81 if negative {
82 out.push('-');
83 }
84
85 if k <= pos && pos <= 21 {
86 out.push_str(&digits);
88 for _ in 0..(pos - k) {
89 out.push('0');
90 }
91 } else if 0 < pos && pos <= 21 {
92 out.push_str(&digits[..pos as usize]);
94 out.push('.');
95 out.push_str(&digits[pos as usize..]);
96 } else if -6 < pos && pos <= 0 {
97 out.push_str("0.");
99 for _ in 0..(-pos) {
100 out.push('0');
101 }
102 out.push_str(&digits);
103 } else {
104 if k == 1 {
107 out.push_str(&digits);
108 } else {
109 out.push_str(&digits[..1]);
110 out.push('.');
111 out.push_str(&digits[1..]);
112 }
113 out.push('e');
114 let e = pos - 1;
115 if e >= 0 {
116 out.push('+');
117 }
118 out.push_str(&e.to_string());
119 }
120
121 out
122}
123
124impl SlotValue {
125 pub fn to_text(&self) -> String {
131 match self {
132 SlotValue::Null => String::new(),
133 SlotValue::Text(s) => s.clone(),
134 SlotValue::Bool(true) => "true".to_string(),
135 SlotValue::Bool(false) => "false".to_string(),
136 SlotValue::Number(n) => format_js_number(*n),
137 SlotValue::Array(items) => {
143 let mut out = String::new();
144 for (i, item) in items.iter().enumerate() {
145 if i > 0 {
146 out.push(',');
147 }
148 out.push_str(&item.to_text());
149 }
150 out
151 }
152 SlotValue::Object(_) => "[object Object]".to_string(),
154 }
155 }
156
157 pub fn array(items: Vec<SlotValue>) -> Self {
162 SlotValue::Array(items.into())
163 }
164
165 pub fn object(pairs: Vec<(String, SlotValue)>) -> Self {
167 SlotValue::Object(pairs.into())
168 }
169
170 pub fn as_bool(&self) -> bool {
183 match self {
184 SlotValue::Null => false,
185 SlotValue::Bool(b) => *b,
186 SlotValue::Text(s) => !s.is_empty(),
187 SlotValue::Number(n) => *n != 0.0 && !n.is_nan(),
188 SlotValue::Array(_) | SlotValue::Object(_) => true,
189 }
190 }
191
192 pub fn get_property(&self, name: &str) -> SlotValue {
194 match self {
195 SlotValue::Object(pairs) => {
196 for (k, v) in pairs.iter() {
197 if k == name {
198 return v.clone();
199 }
200 }
201 SlotValue::Null
202 }
203 _ => SlotValue::Null,
204 }
205 }
206
207 pub fn as_array(&self) -> Option<&[SlotValue]> {
209 match self {
210 SlotValue::Array(a) => Some(a),
211 _ => None,
212 }
213 }
214
215 pub fn as_text_ref(&self) -> &str {
218 match self {
219 SlotValue::Text(s) => s.as_str(),
220 _ => "",
221 }
222 }
223
224 pub fn to_json(&self) -> serde_json::Value {
235 match self {
236 SlotValue::Null => serde_json::Value::Null,
237 SlotValue::Text(s) => serde_json::Value::String(s.clone()),
238 SlotValue::Bool(b) => serde_json::Value::Bool(*b),
239 SlotValue::Number(n) => {
240 if n.fract() == 0.0 && n.abs() < JS_SAFE_INTEGER_LIMIT {
244 serde_json::Value::Number(serde_json::Number::from(*n as i64))
245 } else {
246 serde_json::Number::from_f64(*n)
247 .map(serde_json::Value::Number)
248 .unwrap_or(serde_json::Value::Null)
249 }
250 }
251 SlotValue::Array(items) => {
252 serde_json::Value::Array(items.iter().map(|v| v.to_json()).collect())
253 }
254 SlotValue::Object(pairs) => {
255 let map: serde_json::Map<String, serde_json::Value> = pairs
256 .iter()
257 .map(|(k, v)| (k.clone(), v.to_json()))
258 .collect();
259 serde_json::Value::Object(map)
260 }
261 }
262 }
263}
264
265#[derive(Debug, Clone)]
270pub struct SlotData {
271 slots: Vec<SlotValue>,
272}
273
274impl SlotData {
275 pub fn new(capacity: usize) -> Self {
282 let mut slots = Vec::with_capacity(capacity);
283 slots.resize_with(capacity, || SlotValue::Null);
284 Self { slots }
285 }
286
287 pub fn set(&mut self, slot_id: u16, value: SlotValue) {
301 self.replace(slot_id, value);
302 }
303
304 pub fn replace(&mut self, slot_id: u16, value: SlotValue) -> SlotValue {
314 let idx = slot_id as usize;
315 if idx >= self.slots.len() {
316 #[cfg(feature = "tracing")]
317 tracing::warn!(
318 slot_id = slot_id,
319 len = self.slots.len(),
320 "Slot write past the end of SlotData — growing (SlotData sized smaller than the module's slot table)"
321 );
322 self.slots.resize_with(idx + 1, || SlotValue::Null);
323 }
324 std::mem::replace(&mut self.slots[idx], value)
325 }
326
327 pub fn get(&self, slot_id: u16) -> &SlotValue {
329 let idx = slot_id as usize;
330 self.slots.get(idx).unwrap_or(&NULL_SLOT)
331 }
332
333 pub fn get_text(&self, slot_id: u16) -> Option<&str> {
335 match self.get(slot_id) {
336 SlotValue::Text(s) => Some(s.as_str()),
337 _ => None,
338 }
339 }
340
341 pub fn from_json(json_str: &str, module: &IrModule) -> Result<Self, IrError> {
352 let parsed: serde_json::Value =
353 serde_json::from_str(json_str).map_err(|e| IrError::JsonParseError(e.to_string()))?;
354
355 let obj = match parsed {
356 serde_json::Value::Object(map) => map,
357 _ => return Err(IrError::JsonParseError("expected JSON object".to_string())),
358 };
359
360 let mut name_to_slot: std::collections::HashMap<&str, u16> =
368 std::collections::HashMap::new();
369 for entry in module.slots.entries() {
370 if let Ok(name) = module.strings.get(entry.name_str_idx) {
371 name_to_slot.entry(name).or_insert(entry.slot_id);
372 }
373 }
374
375 let mut data = Self::new_from_defaults(&module.slots);
377
378 for (key, value) in &obj {
380 if let Some(&slot_id) = name_to_slot.get(key.as_str()) {
381 data.set(slot_id, json_to_slot_value(value));
382 }
383 }
385
386 Ok(data)
387 }
388
389 pub fn new_from_defaults(table: &SlotTable) -> Self {
393 let entries = table.entries();
394 let capacity = entries
395 .iter()
396 .map(|e| e.slot_id as usize + 1)
397 .max()
398 .unwrap_or(0);
399 let mut slots = Vec::with_capacity(capacity);
400 slots.resize_with(capacity, || SlotValue::Null);
401
402 for entry in entries {
403 let idx = entry.slot_id as usize;
404 if idx >= slots.len() {
405 continue;
406 }
407 if entry.default_bytes.is_empty() {
408 continue;
409 }
410
411 let default_str = std::str::from_utf8(&entry.default_bytes).unwrap_or("");
412 let value = match entry.type_hint {
413 SlotType::Bool => match default_str {
414 "true" => SlotValue::Bool(true),
415 "false" => SlotValue::Bool(false),
416 _ => SlotValue::Null,
417 },
418 SlotType::Text => SlotValue::Text(default_str.to_string()),
419 SlotType::Number => default_str
420 .parse::<f64>()
421 .map(SlotValue::Number)
422 .unwrap_or(SlotValue::Null),
423 SlotType::Array => {
424 if default_str == "[]" {
425 SlotValue::array(vec![])
426 } else {
427 SlotValue::Null
428 }
429 }
430 SlotType::Object => SlotValue::Null,
431 };
432 slots[idx] = value;
433 }
434
435 Self { slots }
436 }
437}
438
439pub fn json_to_slot_value(value: &serde_json::Value) -> SlotValue {
449 match value {
450 serde_json::Value::Null => SlotValue::Null,
451 serde_json::Value::String(s) => SlotValue::Text(s.clone()),
452 serde_json::Value::Bool(b) => SlotValue::Bool(*b),
453 serde_json::Value::Number(n) => SlotValue::Number(n.as_f64().unwrap_or(0.0)),
454 serde_json::Value::Array(arr) => {
455 SlotValue::Array(arr.iter().map(json_to_slot_value).collect())
456 }
457 serde_json::Value::Object(map) => SlotValue::Object(
458 map.iter()
459 .map(|(k, v)| (k.clone(), json_to_slot_value(v)))
460 .collect(),
461 ),
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::parser::{IrModule, SlotTable};
469
470 fn build_slot_table_bytes(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
473 let mut buf = Vec::new();
474 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
475 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
476 buf.extend_from_slice(&slot_id.to_le_bytes());
477 buf.extend_from_slice(&name_str_idx.to_le_bytes());
478 buf.push(type_hint);
479 buf.push(source);
480 buf.extend_from_slice(&(default_bytes.len() as u16).to_le_bytes());
481 buf.extend_from_slice(default_bytes);
482 }
483 buf
484 }
485
486 #[test]
487 fn slot_value_text_to_text() {
488 assert_eq!(SlotValue::Text("hello".to_string()).to_text(), "hello");
489 }
490
491 #[test]
492 fn slot_value_bool_to_text() {
493 assert_eq!(SlotValue::Bool(true).to_text(), "true");
494 assert_eq!(SlotValue::Bool(false).to_text(), "false");
495 }
496
497 #[test]
498 fn slot_value_number_integer() {
499 assert_eq!(SlotValue::Number(42.0).to_text(), "42");
500 }
501
502 #[test]
503 fn slot_value_number_float() {
504 assert_eq!(SlotValue::Number(3.15).to_text(), "3.15");
505 }
506
507 #[test]
508 fn slot_value_null_to_text() {
509 assert_eq!(SlotValue::Null.to_text(), "");
510 }
511
512 #[test]
513 fn as_bool_matches_javascript_tobool() {
514 assert!(SlotValue::Text("x".to_string()).as_bool());
518 assert!(SlotValue::Number(1.0).as_bool());
519 assert!(SlotValue::Number(-1.0).as_bool());
520 assert!(SlotValue::Bool(true).as_bool());
521 assert!(SlotValue::array(vec![SlotValue::Null]).as_bool());
522 assert!(SlotValue::object(vec![]).as_bool());
523 assert!(SlotValue::array(vec![]).as_bool());
526
527 assert!(!SlotValue::Null.as_bool());
528 assert!(!SlotValue::Text("".to_string()).as_bool());
529 assert!(!SlotValue::Number(0.0).as_bool());
530 assert!(!SlotValue::Number(-0.0).as_bool());
531 assert!(!SlotValue::Number(f64::NAN).as_bool());
532 assert!(!SlotValue::Bool(false).as_bool());
533 }
534
535 #[test]
536 fn to_text_matches_javascript_string_for_containers() {
537 assert_eq!(SlotValue::array(vec![]).to_text(), "");
539 assert_eq!(
540 SlotValue::array(vec![
541 SlotValue::Text("a".into()),
542 SlotValue::Text("b".into())
543 ])
544 .to_text(),
545 "a,b"
546 );
547 assert_eq!(
549 SlotValue::array(vec![
550 SlotValue::Null,
551 SlotValue::Number(1.0),
552 SlotValue::Null
553 ])
554 .to_text(),
555 ",1,"
556 );
557 assert_eq!(
559 SlotValue::array(vec![
560 SlotValue::array(vec![SlotValue::Number(1.0), SlotValue::Number(2.0)]),
561 SlotValue::array(vec![SlotValue::Number(3.0)]),
562 ])
563 .to_text(),
564 "1,2,3"
565 );
566 assert_eq!(SlotValue::object(vec![]).to_text(), "[object Object]");
567 assert_eq!(
568 SlotValue::array(vec![SlotValue::object(vec![])]).to_text(),
569 "[object Object]"
570 );
571 }
572
573 #[test]
574 fn slot_value_as_array() {
575 let arr = SlotValue::array(vec![SlotValue::Text("a".to_string())]);
576 assert!(arr.as_array().is_some());
577 assert_eq!(arr.as_array().unwrap().len(), 1);
578
579 assert!(SlotValue::Null.as_array().is_none());
580 assert!(SlotValue::Text("x".to_string()).as_array().is_none());
581 assert!(SlotValue::Number(1.0).as_array().is_none());
582 assert!(SlotValue::Bool(true).as_array().is_none());
583 assert!(SlotValue::object(vec![]).as_array().is_none());
584 }
585
586 #[test]
587 fn slot_data_set_and_get() {
588 let mut data = SlotData::new(4);
589 data.set(0, SlotValue::Text("title".to_string()));
590 data.set(2, SlotValue::Number(99.0));
591
592 assert_eq!(data.get(0).to_text(), "title");
593 assert_eq!(data.get(2).to_text(), "99");
594 assert_eq!(data.get(1).to_text(), "");
596 }
597
598 #[test]
599 fn slot_data_out_of_bounds() {
600 let data = SlotData::new(2);
601 assert_eq!(data.get(5).to_text(), "");
603 assert!(!data.get(100).as_bool());
604 }
605
606 #[test]
607 fn slot_data_get_text() {
608 let mut data = SlotData::new(3);
609 data.set(0, SlotValue::Text("hello".to_string()));
610 data.set(1, SlotValue::Number(42.0));
611
612 assert_eq!(data.get_text(0), Some("hello"));
613 assert_eq!(data.get_text(1), None); assert_eq!(data.get_text(2), None); }
616
617 #[test]
620 fn slot_data_new_from_defaults_basic() {
621 let bytes = build_slot_table_bytes(&[
626 (0, 0, 0x04, 0x00, b"[]"), (1, 1, 0x02, 0x01, b"false"), (2, 2, 0x01, 0x01, b"hello"), ]);
630 let table = SlotTable::parse(&bytes).unwrap();
631 let data = SlotData::new_from_defaults(&table);
632
633 assert!(matches!(data.get(0), SlotValue::Array(v) if v.is_empty()));
635 assert!(matches!(data.get(1), SlotValue::Bool(false)));
637 assert_eq!(data.get_text(2), Some("hello"));
639 }
640
641 #[test]
642 fn slot_data_new_from_defaults_empty_table() {
643 let bytes = build_slot_table_bytes(&[]);
645 let table = SlotTable::parse(&bytes).unwrap();
646 let data = SlotData::new_from_defaults(&table);
647
648 assert!(matches!(data.get(0), SlotValue::Null));
650 }
651
652 #[test]
653 fn slot_data_new_from_defaults_no_default_bytes() {
654 let bytes = build_slot_table_bytes(&[
656 (0, 0, 0x01, 0x00, b""), (1, 1, 0x03, 0x01, b""), ]);
659 let table = SlotTable::parse(&bytes).unwrap();
660 let data = SlotData::new_from_defaults(&table);
661
662 assert!(matches!(data.get(0), SlotValue::Null));
663 assert!(matches!(data.get(1), SlotValue::Null));
664 }
665
666 #[test]
667 fn slot_data_new_from_defaults_number() {
668 let bytes = build_slot_table_bytes(&[
670 (0, 0, 0x03, 0x00, b"42"), (1, 1, 0x03, 0x01, b"3.15"), (2, 2, 0x03, 0x00, b"nope"), ]);
674 let table = SlotTable::parse(&bytes).unwrap();
675 let data = SlotData::new_from_defaults(&table);
676
677 assert!(matches!(data.get(0), SlotValue::Number(n) if (*n - 42.0).abs() < f64::EPSILON));
678 assert!(matches!(data.get(1), SlotValue::Number(n) if (*n - 3.15).abs() < f64::EPSILON));
679 assert!(matches!(data.get(2), SlotValue::Null));
681 }
682
683 #[test]
684 fn slot_data_new_from_defaults_bool_true() {
685 let bytes = build_slot_table_bytes(&[
686 (0, 0, 0x02, 0x00, b"true"), ]);
688 let table = SlotTable::parse(&bytes).unwrap();
689 let data = SlotData::new_from_defaults(&table);
690
691 assert!(matches!(data.get(0), SlotValue::Bool(true)));
692 }
693
694 #[test]
695 fn slot_data_new_from_defaults_object_ignored() {
696 let bytes = build_slot_table_bytes(&[
698 (0, 0, 0x05, 0x00, b"{}"), ]);
700 let table = SlotTable::parse(&bytes).unwrap();
701 let data = SlotData::new_from_defaults(&table);
702
703 assert!(matches!(data.get(0), SlotValue::Null));
704 }
705
706 use crate::parser::test_helpers::{build_minimal_ir, encode_text};
709
710 fn build_module_for_json(
712 strings: &[&str],
713 slot_decls: &[(u16, u32, u8, u8, &[u8])],
714 ) -> IrModule {
715 let opcodes = encode_text(0);
717 let data = build_minimal_ir(strings, slot_decls, &opcodes, &[]);
718 IrModule::parse(&data).unwrap()
719 }
720
721 #[test]
722 fn from_json_basic() {
723 let module = build_module_for_json(
727 &["title", "count"],
728 &[
729 (0, 0, 0x01, 0x00, &[]), (1, 1, 0x03, 0x00, &[]), ],
732 );
733
734 let data = SlotData::from_json(r#"{"title": "Hello", "count": 42}"#, &module).unwrap();
735 assert_eq!(data.get(0).to_text(), "Hello");
736 assert_eq!(data.get(1).to_text(), "42");
737 }
738
739 #[test]
740 fn from_json_missing_key_uses_default() {
741 let module = build_module_for_json(
743 &["greeting"],
744 &[
745 (0, 0, 0x01, 0x00, b"hi"), ],
747 );
748
749 let data = SlotData::from_json(r#"{}"#, &module).unwrap();
751 assert_eq!(data.get(0).to_text(), "hi");
752 }
753
754 #[test]
755 fn from_json_null_value() {
756 let module = build_module_for_json(&["name"], &[(0, 0, 0x01, 0x00, &[])]);
757
758 let data = SlotData::from_json(r#"{"name": null}"#, &module).unwrap();
759 assert!(matches!(data.get(0), SlotValue::Null));
760 }
761
762 #[test]
763 fn from_json_bool_values() {
764 let module = build_module_for_json(
765 &["active", "hidden"],
766 &[
767 (0, 0, 0x02, 0x00, &[]), (1, 1, 0x02, 0x00, &[]), ],
770 );
771
772 let data = SlotData::from_json(r#"{"active": true, "hidden": false}"#, &module).unwrap();
773 assert!(matches!(data.get(0), SlotValue::Bool(true)));
774 assert!(matches!(data.get(1), SlotValue::Bool(false)));
775 }
776
777 #[test]
778 fn from_json_array() {
779 let module = build_module_for_json(
780 &["items"],
781 &[(0, 0, 0x04, 0x00, &[])], );
783
784 let data = SlotData::from_json(r#"{"items": ["a", "b", 3]}"#, &module).unwrap();
785 if let SlotValue::Array(arr) = data.get(0) {
786 assert_eq!(arr.len(), 3);
787 assert_eq!(arr[0].to_text(), "a");
788 assert_eq!(arr[1].to_text(), "b");
789 assert_eq!(arr[2].to_text(), "3");
790 } else {
791 panic!("expected Array, got {:?}", data.get(0));
792 }
793 }
794
795 #[test]
796 fn from_json_nested_object() {
797 let module = build_module_for_json(
798 &["config"],
799 &[(0, 0, 0x05, 0x00, &[])], );
801
802 let data = SlotData::from_json(r#"{"config": {"key": "value", "n": 7}}"#, &module).unwrap();
803 if let SlotValue::Object(pairs) = data.get(0) {
804 assert_eq!(pairs.len(), 2);
805 assert_eq!(pairs[0].0, "key");
806 assert_eq!(pairs[0].1.to_text(), "value");
807 assert_eq!(pairs[1].0, "n");
808 assert_eq!(pairs[1].1.to_text(), "7");
809 } else {
810 panic!("expected Object, got {:?}", data.get(0));
811 }
812 }
813
814 #[test]
815 fn from_json_unknown_key_ignored() {
816 let module = build_module_for_json(&["title"], &[(0, 0, 0x01, 0x00, &[])]);
817
818 let data =
820 SlotData::from_json(r#"{"title": "Hi", "extra_key": "ignored"}"#, &module).unwrap();
821 assert_eq!(data.get(0).to_text(), "Hi");
822 }
823
824 #[test]
825 fn from_json_invalid_json() {
826 let module = build_module_for_json(&["x"], &[(0, 0, 0x01, 0x00, &[])]);
827
828 let result = SlotData::from_json(r#"not valid json"#, &module);
829 assert!(result.is_err());
830 match result.unwrap_err() {
831 crate::format::IrError::JsonParseError(_) => {} other => panic!("expected JsonParseError, got {other:?}"),
833 }
834 }
835
836 #[test]
839 fn number_to_text_matches_javascript_string() {
840 let cases: &[(f64, &str)] = &[
843 (0.0, "0"),
844 (-0.0, "0"),
848 (1.0, "1"),
849 (-42.0, "-42"),
850 (1.5, "1.5"),
851 (123.456, "123.456"),
852 (0.1, "0.1"),
853 (1e16, "10000000000000000"),
857 (1e20, "100000000000000000000"),
858 (1e21, "1e+21"),
861 (1e300, "1e+300"),
862 (9007199254740992.0, "9007199254740992"), (9007199254740994.0, "9007199254740994"), (1e-6, "0.000001"),
865 (1e-7, "1e-7"),
866 (1.5e-7, "1.5e-7"),
867 (-1.5e-7, "-1.5e-7"),
868 (f64::MAX, "1.7976931348623157e+308"),
869 (f64::INFINITY, "Infinity"),
871 (f64::NEG_INFINITY, "-Infinity"),
872 (f64::NAN, "NaN"),
873 ];
874 for (value, expected) in cases {
875 assert_eq!(
876 SlotValue::Number(*value).to_text(),
877 *expected,
878 "String({value:?})"
879 );
880 }
881 }
882
883 #[test]
884 fn number_to_text_never_saturates_above_i64() {
885 let huge = 1e19_f64; assert_eq!(SlotValue::Number(huge).to_text(), "10000000000000000000");
889 assert!(!SlotValue::Number(huge)
890 .to_text()
891 .contains("9223372036854775807"));
892 }
893
894 #[test]
895 fn number_to_json_round_trips_by_value_not_by_text() {
896 for value in [0.0_f64, -0.0, 1.5, 1e16, 1e21, 1e300, -1e19] {
900 let json = SlotValue::Number(value).to_json();
901 let parsed: f64 = json.as_f64().unwrap_or_else(|| panic!("{value} -> {json}"));
902 assert_eq!(parsed, value, "value {value} did not round-trip");
903 }
904 }
905
906 #[test]
907 fn number_to_json_non_finite_is_null_matching_js() {
908 for value in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
911 assert_eq!(SlotValue::Number(value).to_json(), serde_json::Value::Null);
912 }
913 }
914
915 #[test]
918 fn set_grows_instead_of_silently_dropping() {
919 let mut data = SlotData::new(1);
922 data.set(5, SlotValue::Text("late".into()));
923 assert_eq!(data.get(5).to_text(), "late");
924 assert!(matches!(data.get(3), SlotValue::Null));
926 }
927}