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 if let Some(c) = v.as_class_inst() {
80 let names = class_field_names(&c.def);
81 let values = c.get_values();
82 let mut map = IndexMap::new();
83 // If the registry didn't resolve some parents, the names vec
84 // can be shorter than values. Iterate by min length so we still
85 // emit something useful instead of panicking.
86 let n = names.len().min(values.len());
87 for i in 0..n {
88 map.insert(names[i].clone(), deep_normalize(&values[i]));
89 }
90 return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
91 }
92 if let Some(s) = v.as_struct_inst() {
93 let values = s.get_values();
94 let mut map = IndexMap::new();
95 for (i, field) in s.def.fields.iter().enumerate() {
96 if let Some(elem) = values.get(i) {
97 map.insert(field.name.clone(), deep_normalize(elem));
98 }
99 }
100 return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
101 }
102 if let Some(e) = v.as_enum_inst() {
103 // Enum: emit `{ variant => "Name", value => recursive(payload) }`
104 // when there's a payload; otherwise `{ variant => "Name" }`.
105 // Lets serializers preserve enum identity instead of stringifying.
106 let mut map = IndexMap::new();
107 map.insert(
108 "variant".to_string(),
109 StrykeValue::string(e.variant_name().to_string()),
110 );
111 if !e.data.is_undef() {
112 map.insert("value".to_string(), deep_normalize(&e.data));
113 }
114 return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
115 }
116 if let Some(r) = v.as_hash_ref() {
117 let inner = r.read().clone();
118 let mut map = IndexMap::new();
119 for (k, val) in inner.into_iter() {
120 map.insert(k, deep_normalize(&val));
121 }
122 return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
123 }
124 if let Some(r) = v.as_array_ref() {
125 let inner = r.read().clone();
126 let out: Vec<StrykeValue> = inner.iter().map(deep_normalize).collect();
127 return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
128 }
129 v.clone()
130}
131
132/// Convenience: normalize the first arg in place and return a Vec the
133/// caller can hand to its existing serializer logic. Use when the
134/// serializer takes `&[StrykeValue]` and only the first element is the
135/// data to serialize.
136pub fn normalize_args_head(args: &[StrykeValue]) -> Vec<StrykeValue> {
137 if args.is_empty() {
138 return Vec::new();
139 }
140 let mut out = Vec::with_capacity(args.len());
141 out.push(deep_normalize(&args[0]));
142 out.extend(args[1..].iter().cloned());
143 out
144}