Skip to main content

stryke/
serialize_normalize.rs

1//! Free-function recursive flatten of stryke `ClassInstance` /
2//! `StructInstance` values into plain hashref / arrayref trees, for use
3//! by serializers (`to_json`, `to_xml`, `to_yaml`, `to_toml`, `to_html`,
4//! `ddump`) that take `&[StrykeValue]` and don't have a `&VMHelper` to
5//! consult.
6//!
7//! Inheritance fields (parents declared via `extends`) are looked up
8//! through a thread-local `CLASS_DEFS_REGISTRY` that the VM populates
9//! on entry to `execute` and on each `ClassDecl` statement. When the
10//! registry is empty (e.g. a serializer is called outside a normal VM
11//! run), the helper falls back to the class's own field definitions
12//! only — covers the no-inheritance case correctly.
13
14use indexmap::IndexMap;
15use parking_lot::RwLock;
16use std::cell::RefCell;
17use std::collections::HashMap;
18use std::sync::Arc;
19
20use crate::ast::ClassDef;
21use crate::value::StrykeValue;
22
23thread_local! {
24    /// Per-thread registry of class definitions, keyed by class name.
25    /// VM execution sites snapshot the helper's `class_defs` into this
26    /// cell so the free serializers can reach the same MRO information
27    /// without taking a `&VMHelper`.
28    pub(crate) static CLASS_DEFS_REGISTRY: RefCell<HashMap<String, Arc<ClassDef>>> =
29        RefCell::new(HashMap::new());
30}
31
32/// Replace this thread's class registry with `defs`. Returns the
33/// previous registry so callers can restore it on exit (RAII pattern is
34/// preferred — see [`ClassDefsGuard`]).
35pub(crate) fn install_class_defs(
36    defs: HashMap<String, Arc<ClassDef>>,
37) -> HashMap<String, Arc<ClassDef>> {
38    CLASS_DEFS_REGISTRY.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), defs))
39}
40
41/// Add or update a single class definition in this thread's registry.
42/// Used when a `class C { ... }` statement runs at the top level.
43pub(crate) fn register_class_def(def: Arc<ClassDef>) {
44    CLASS_DEFS_REGISTRY.with(|cell| {
45        cell.borrow_mut().insert(def.name.clone(), def);
46    });
47}
48
49/// Walk a class's full inheritance chain and return field names in MRO
50/// order (parent fields first, then own). Mirrors
51/// `VMHelper::collect_class_fields_full` but reads from the thread-
52/// local registry. Returns an empty vec if the def has parents that
53/// aren't registered (e.g. the serializer ran in an isolated context).
54fn class_field_names(def: &ClassDef) -> Vec<String> {
55    let mut names = Vec::new();
56    for parent_name in &def.extends {
57        let parent_def_opt =
58            CLASS_DEFS_REGISTRY.with(|cell| cell.borrow().get(parent_name).cloned());
59        if let Some(parent_def) = parent_def_opt {
60            names.extend(class_field_names(&parent_def));
61        }
62    }
63    for f in &def.fields {
64        names.push(f.name.clone());
65    }
66    names
67}
68
69/// Recursively convert any `ClassInstance` / `StructInstance` /
70/// `EnumInstance` reachable inside `v` into plain hashrefs (using the
71/// field name as the key). Hashrefs and arrayrefs are walked in place;
72/// every other value (numbers, strings, undef, code refs, blessed
73/// non-hash refs, …) round-trips unchanged.
74///
75/// The intent is "make this value JSON-serializable end-to-end" — call
76/// it once at the top of every serializer that doesn't already know
77/// about stryke-native OO instances.
78pub fn deep_normalize(v: &StrykeValue) -> StrykeValue {
79    let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
80    deep_normalize_inner(v, &mut visited)
81}
82
83fn deep_normalize_inner(
84    v: &StrykeValue,
85    visited: &mut std::collections::HashSet<usize>,
86) -> StrykeValue {
87    if let Some(c) = v.as_class_inst() {
88        let names = class_field_names(&c.def);
89        let values = c.get_values();
90        let mut map = IndexMap::new();
91        // If the registry didn't resolve some parents, the names vec
92        // can be shorter than values. Iterate by min length so we still
93        // emit something useful instead of panicking.
94        let n = names.len().min(values.len());
95        for i in 0..n {
96            map.insert(names[i].clone(), deep_normalize_inner(&values[i], visited));
97        }
98        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
99    }
100    if let Some(s) = v.as_struct_inst() {
101        let values = s.get_values();
102        let mut map = IndexMap::new();
103        for (i, field) in s.def.fields.iter().enumerate() {
104            if let Some(elem) = values.get(i) {
105                map.insert(field.name.clone(), deep_normalize_inner(elem, visited));
106            }
107        }
108        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
109    }
110    if let Some(e) = v.as_enum_inst() {
111        // Enum: emit `{ variant => "Name", value => recursive(payload) }`
112        // when there's a payload; otherwise `{ variant => "Name" }`.
113        // Lets serializers preserve enum identity instead of stringifying.
114        let mut map = IndexMap::new();
115        map.insert(
116            "variant".to_string(),
117            StrykeValue::string(e.variant_name().to_string()),
118        );
119        if !e.data.is_undef() {
120            map.insert("value".to_string(), deep_normalize_inner(&e.data, visited));
121        }
122        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
123    }
124    if let Some(r) = v.as_hash_ref() {
125        // Cycle guard: pass back-edges through as UNDEF so serializers can
126        // handle them (BUG-105 — was a stack overflow on self-referential
127        // hashes/arrays).
128        let addr = Arc::as_ptr(&r) as usize;
129        if !visited.insert(addr) {
130            return StrykeValue::UNDEF;
131        }
132        let inner = r.read().clone();
133        let mut map = IndexMap::new();
134        for (k, val) in inner.into_iter() {
135            map.insert(k, deep_normalize_inner(&val, visited));
136        }
137        visited.remove(&addr);
138        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
139    }
140    if let Some(r) = v.as_array_ref() {
141        let addr = Arc::as_ptr(&r) as usize;
142        if !visited.insert(addr) {
143            return StrykeValue::UNDEF;
144        }
145        let inner = r.read().clone();
146        let out: Vec<StrykeValue> = inner
147            .iter()
148            .map(|elem| deep_normalize_inner(elem, visited))
149            .collect();
150        visited.remove(&addr);
151        return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
152    }
153    v.clone()
154}
155
156/// Convenience: normalize the first arg in place and return a Vec the
157/// caller can hand to its existing serializer logic. Use when the
158/// serializer takes `&[StrykeValue]` and only the first element is the
159/// data to serialize.
160pub fn normalize_args_head(args: &[StrykeValue]) -> Vec<StrykeValue> {
161    if args.is_empty() {
162        return Vec::new();
163    }
164    let mut out = Vec::with_capacity(args.len());
165    out.push(deep_normalize(&args[0]));
166    out.extend(args[1..].iter().cloned());
167    out
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use indexmap::IndexMap;
174
175    fn aref(v: Vec<StrykeValue>) -> StrykeValue {
176        StrykeValue::array_ref(Arc::new(RwLock::new(v)))
177    }
178
179    fn href(pairs: &[(&str, StrykeValue)]) -> StrykeValue {
180        let mut m = IndexMap::new();
181        for (k, v) in pairs {
182            m.insert((*k).to_string(), v.clone());
183        }
184        StrykeValue::hash_ref(Arc::new(RwLock::new(m)))
185    }
186
187    #[test]
188    fn deep_normalize_scalars_roundtrip_unchanged() {
189        for v in [
190            StrykeValue::integer(42),
191            StrykeValue::float(3.5),
192            StrykeValue::string("hi".into()),
193            StrykeValue::UNDEF,
194        ] {
195            let out = deep_normalize(&v);
196            assert_eq!(out.to_string(), v.to_string());
197        }
198    }
199
200    #[test]
201    fn deep_normalize_walks_nested_arrayref_hashref() {
202        let inner = href(&[("x", StrykeValue::integer(1))]);
203        let outer = aref(vec![inner, StrykeValue::integer(2)]);
204        let out = deep_normalize(&outer);
205        let arr = out.as_array_ref().expect("array_ref outer survives");
206        let arr = arr.read();
207        assert_eq!(arr.len(), 2);
208        let h = arr[0].as_hash_ref().expect("nested hash_ref survives");
209        let h = h.read();
210        assert_eq!(h.get("x").unwrap().to_int(), 1);
211        assert_eq!(arr[1].to_int(), 2);
212    }
213
214    #[test]
215    fn deep_normalize_does_not_share_storage_with_input() {
216        let arr = Arc::new(RwLock::new(vec![StrykeValue::integer(1)]));
217        let v = StrykeValue::array_ref(arr.clone());
218        let out = deep_normalize(&v);
219        let out_arr = out.as_array_ref().expect("array_ref");
220        out_arr.write().push(StrykeValue::integer(2));
221        assert_eq!(
222            arr.read().len(),
223            1,
224            "deep_normalize must clone, not alias, ref storage"
225        );
226    }
227
228    #[test]
229    fn normalize_args_head_normalizes_first_only() {
230        let h = href(&[("k", StrykeValue::integer(7))]);
231        let tail = StrykeValue::string("opt".into());
232        let out = normalize_args_head(&[h, tail.clone()]);
233        assert_eq!(out.len(), 2);
234        assert!(out[0].as_hash_ref().is_some());
235        assert_eq!(out[1].to_string(), tail.to_string());
236    }
237
238    #[test]
239    fn normalize_args_head_empty_returns_empty() {
240        assert!(normalize_args_head(&[]).is_empty());
241    }
242
243    #[test]
244    fn register_class_def_appears_in_field_names() {
245        use crate::ast::ClassDef;
246        let prev = install_class_defs(HashMap::new());
247        let def = Arc::new(ClassDef {
248            name: "T".into(),
249            is_abstract: false,
250            is_final: false,
251            extends: vec![],
252            implements: vec![],
253            fields: vec![],
254            methods: vec![],
255            static_fields: vec![],
256        });
257        register_class_def(def);
258        let names = CLASS_DEFS_REGISTRY.with(|c| c.borrow().keys().cloned().collect::<Vec<_>>());
259        assert!(names.contains(&"T".to_string()));
260        // restore to keep registry isolated across tests
261        install_class_defs(prev);
262    }
263}