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 visited: std::collections::HashSet<String> = std::collections::HashSet::new();
56    class_field_names_dedup(def, &mut visited)
57}
58
59fn class_field_names_dedup(
60    def: &ClassDef,
61    visited_classes: &mut std::collections::HashSet<String>,
62) -> Vec<String> {
63    let mut names = Vec::new();
64    // Diamond-inheritance dedup: in a class graph like D→B,C; B→A; C→A, walking
65    // `extends` recursively without a visited set emits A's fields twice. The
66    // duplicated name slot then silently mis-pairs with the values vector in
67    // MRO order (which DOES deduplicate parents). Track visited parent class
68    // NAMES so the same parent's fields appear at most once in the result.
69    for parent_name in &def.extends {
70        if !visited_classes.insert(parent_name.clone()) {
71            continue;
72        }
73        let parent_def_opt =
74            CLASS_DEFS_REGISTRY.with(|cell| cell.borrow().get(parent_name).cloned());
75        if let Some(parent_def) = parent_def_opt {
76            names.extend(class_field_names_dedup(&parent_def, visited_classes));
77        }
78    }
79    for f in &def.fields {
80        names.push(f.name.clone());
81    }
82    names
83}
84
85/// Recursively convert any `ClassInstance` / `StructInstance` /
86/// `EnumInstance` reachable inside `v` into plain hashrefs (using the
87/// field name as the key). Hashrefs and arrayrefs are walked in place;
88/// every other value (numbers, strings, undef, code refs, blessed
89/// non-hash refs, …) round-trips unchanged.
90///
91/// The intent is "make this value JSON-serializable end-to-end" — call
92/// it once at the top of every serializer that doesn't already know
93/// about stryke-native OO instances.
94pub fn deep_normalize(v: &StrykeValue) -> StrykeValue {
95    let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
96    deep_normalize_inner(v, &mut visited)
97}
98
99fn deep_normalize_inner(
100    v: &StrykeValue,
101    visited: &mut std::collections::HashSet<usize>,
102) -> StrykeValue {
103    if let Some(c) = v.as_class_inst() {
104        let names = class_field_names(&c.def);
105        let values = c.get_values();
106        let mut map = IndexMap::new();
107        // If the registry didn't resolve some parents, the names vec
108        // can be shorter than values. Iterate by min length so we still
109        // emit something useful instead of panicking.
110        let n = names.len().min(values.len());
111        for i in 0..n {
112            map.insert(names[i].clone(), deep_normalize_inner(&values[i], visited));
113        }
114        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
115    }
116    if let Some(s) = v.as_struct_inst() {
117        let values = s.get_values();
118        let mut map = IndexMap::new();
119        for (i, field) in s.def.fields.iter().enumerate() {
120            if let Some(elem) = values.get(i) {
121                map.insert(field.name.clone(), deep_normalize_inner(elem, visited));
122            }
123        }
124        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
125    }
126    if let Some(e) = v.as_enum_inst() {
127        // Enum: emit `{ variant => "Name", value => recursive(payload) }`
128        // when there's a payload; otherwise `{ variant => "Name" }`.
129        // Lets serializers preserve enum identity instead of stringifying.
130        let mut map = IndexMap::new();
131        map.insert(
132            "variant".to_string(),
133            StrykeValue::string(e.variant_name().to_string()),
134        );
135        if !e.data.is_undef() {
136            map.insert("value".to_string(), deep_normalize_inner(&e.data, visited));
137        }
138        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
139    }
140    if let Some(r) = v.as_hash_ref() {
141        // Cycle guard: pass back-edges through as UNDEF so serializers can
142        // handle them (BUG-105 — was a stack overflow on self-referential
143        // hashes/arrays).
144        let addr = Arc::as_ptr(&r) as usize;
145        if !visited.insert(addr) {
146            return StrykeValue::UNDEF;
147        }
148        let inner = r.read().clone();
149        let mut map = IndexMap::new();
150        for (k, val) in inner.into_iter() {
151            map.insert(k, deep_normalize_inner(&val, visited));
152        }
153        visited.remove(&addr);
154        return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
155    }
156    if let Some(r) = v.as_array_ref() {
157        let addr = Arc::as_ptr(&r) as usize;
158        if !visited.insert(addr) {
159            return StrykeValue::UNDEF;
160        }
161        let inner = r.read().clone();
162        let out: Vec<StrykeValue> = inner
163            .iter()
164            .map(|elem| deep_normalize_inner(elem, visited))
165            .collect();
166        visited.remove(&addr);
167        return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
168    }
169    v.clone()
170}
171
172/// Convenience: normalize the first arg in place and return a Vec the
173/// caller can hand to its existing serializer logic. Use when the
174/// serializer takes `&[StrykeValue]` and only the first element is the
175/// data to serialize.
176pub fn normalize_args_head(args: &[StrykeValue]) -> Vec<StrykeValue> {
177    if args.is_empty() {
178        return Vec::new();
179    }
180    let mut out = Vec::with_capacity(args.len());
181    out.push(deep_normalize(&args[0]));
182    out.extend(args[1..].iter().cloned());
183    out
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use indexmap::IndexMap;
190
191    fn aref(v: Vec<StrykeValue>) -> StrykeValue {
192        StrykeValue::array_ref(Arc::new(RwLock::new(v)))
193    }
194
195    fn href(pairs: &[(&str, StrykeValue)]) -> StrykeValue {
196        let mut m = IndexMap::new();
197        for (k, v) in pairs {
198            m.insert((*k).to_string(), v.clone());
199        }
200        StrykeValue::hash_ref(Arc::new(RwLock::new(m)))
201    }
202
203    #[test]
204    fn deep_normalize_scalars_roundtrip_unchanged() {
205        for v in [
206            StrykeValue::integer(42),
207            StrykeValue::float(3.5),
208            StrykeValue::string("hi".into()),
209            StrykeValue::UNDEF,
210        ] {
211            let out = deep_normalize(&v);
212            assert_eq!(out.to_string(), v.to_string());
213        }
214    }
215
216    #[test]
217    fn deep_normalize_walks_nested_arrayref_hashref() {
218        let inner = href(&[("x", StrykeValue::integer(1))]);
219        let outer = aref(vec![inner, StrykeValue::integer(2)]);
220        let out = deep_normalize(&outer);
221        let arr = out.as_array_ref().expect("array_ref outer survives");
222        let arr = arr.read();
223        assert_eq!(arr.len(), 2);
224        let h = arr[0].as_hash_ref().expect("nested hash_ref survives");
225        let h = h.read();
226        assert_eq!(h.get("x").unwrap().to_int(), 1);
227        assert_eq!(arr[1].to_int(), 2);
228    }
229
230    #[test]
231    fn deep_normalize_does_not_share_storage_with_input() {
232        let arr = Arc::new(RwLock::new(vec![StrykeValue::integer(1)]));
233        let v = StrykeValue::array_ref(arr.clone());
234        let out = deep_normalize(&v);
235        let out_arr = out.as_array_ref().expect("array_ref");
236        out_arr.write().push(StrykeValue::integer(2));
237        assert_eq!(
238            arr.read().len(),
239            1,
240            "deep_normalize must clone, not alias, ref storage"
241        );
242    }
243
244    #[test]
245    fn normalize_args_head_normalizes_first_only() {
246        let h = href(&[("k", StrykeValue::integer(7))]);
247        let tail = StrykeValue::string("opt".into());
248        let out = normalize_args_head(&[h, tail.clone()]);
249        assert_eq!(out.len(), 2);
250        assert!(out[0].as_hash_ref().is_some());
251        assert_eq!(out[1].to_string(), tail.to_string());
252    }
253
254    #[test]
255    fn normalize_args_head_empty_returns_empty() {
256        assert!(normalize_args_head(&[]).is_empty());
257    }
258
259    #[test]
260    fn register_class_def_appears_in_field_names() {
261        use crate::ast::ClassDef;
262        let prev = install_class_defs(HashMap::new());
263        let def = Arc::new(ClassDef {
264            name: "T".into(),
265            is_abstract: false,
266            is_final: false,
267            extends: vec![],
268            implements: vec![],
269            fields: vec![],
270            methods: vec![],
271            static_fields: vec![],
272        });
273        register_class_def(def);
274        let names = CLASS_DEFS_REGISTRY.with(|c| c.borrow().keys().cloned().collect::<Vec<_>>());
275        assert!(names.contains(&"T".to_string()));
276        // restore to keep registry isolated across tests
277        install_class_defs(prev);
278    }
279}