nanvm_lib/serializer/
to_djs.rs

1use crate::{
2    js::{any::Any, js_array::JsArrayRef, js_object::JsObjectRef, type_::Type},
3    mem::manager::Dealloc,
4};
5
6use core::{
7    fmt::{self},
8    result,
9};
10
11use std::collections::HashMap;
12
13use super::to_json::WriteJson;
14
15/// `Seen` is a bool-like enumeration to represent a "seen" status of a js compound (an object or
16/// an array) visited by `ConstTracker`. In case of `Seen::Once`, the compound was visited just once
17/// and if it remains with that status, it will be written out as a const. In case of
18/// `Seen::Repeatedly`, the compound was visited more than once and will be written out as a const.
19#[derive(PartialEq)]
20enum Seen {
21    Once,
22    Repeatedly,
23}
24
25/// ConstTracker holds references to js compounds (objects or arrays) in two sets:
26/// `visited_once` refers to compounds that we've seen just once so far;
27/// `visited_repeatedly` refers to compounds that we've seen more than once.
28/// When djs tracking pass is done, `visited_repeatedly` refers to compounds that will be written
29/// out via const definitions.
30/// Note that we use one ConstTracker for js objects and another for js arrays, keeping them
31/// separate - to reduce set sizes and save on operations.
32struct ConstTracker<D: Dealloc> {
33    visited: HashMap<Any<D>, Seen>,
34}
35
36impl<D: Dealloc> ConstTracker<D> {
37    /// Returns true if `any` was visited before; updates the `const_tracker` set, tracking whether
38    /// `any` was visited just once (it's in `const_tracker.visited_once`) or more than once (it's
39    /// in `visited_repeatedly` in this case since we are up to writing it out as a const).
40    fn is_visited(&mut self, any: &Any<D>) -> bool {
41        let optional_seen = self.visited.get_mut(any);
42        if let Some(seen) = optional_seen {
43            *seen = Seen::Repeatedly;
44            true
45        } else {
46            self.visited.insert(any.clone(), Seen::Once);
47            false
48        }
49    }
50
51    /// Traverse a DAG referred by `any` (of any js type), tracking objects and arrays, including
52    /// `any` itself.
53    fn track_consts_for_any(&mut self, any: &Any<D>) -> fmt::Result {
54        match any.get_type() {
55            Type::Array | Type::Object => {
56                if !self.is_visited(any) {
57                    any.for_each::<fmt::Error>(|_k, v| self.track_consts_for_any(v))?;
58                }
59                Ok(())
60            }
61            _ => Ok(()),
62        }
63    }
64}
65
66/// Writes a const definition for a compound (an array or an object).
67fn write_compound_const<D: Dealloc>(
68    write_json: &mut (impl WriteJson + ?Sized),
69    any: &Any<D>,
70    to_be_consts: &mut HashMap<Any<D>, Seen>,
71    const_refs: &mut HashMap<Any<D>, usize>,
72) -> fmt::Result {
73    any.for_each(|_k, v| write_consts_and_any(write_json, v, to_be_consts, const_refs))?;
74    if to_be_consts.remove(any).is_some() {
75        let id = const_refs.len();
76        write_json.write_str("const _")?;
77        write_json.write_str(id.to_string().as_str())?;
78        write_json.write_char('=')?;
79        write_with_const_refs(write_json, any.clone(), const_refs)?;
80        const_refs.insert(any.clone(), id);
81        write_json.write_char(';')
82    } else {
83        fmt::Result::Ok(())
84    }
85}
86
87/// Writes a const js entity of any type (skipping over types other than object, array),
88/// ensuring that its const dependencies are written out as well in the right order (with no
89/// forward references).
90fn write_consts_and_any<D: Dealloc>(
91    write_json: &mut (impl WriteJson + ?Sized),
92    any: &Any<D>,
93    to_be_consts: &mut HashMap<Any<D>, Seen>,
94    const_refs: &mut HashMap<Any<D>, usize>,
95) -> fmt::Result {
96    match any.get_type() {
97        Type::Array | Type::Object => {
98            write_compound_const(write_json, any, to_be_consts, const_refs)?;
99        }
100        _ => {}
101    }
102    fmt::Result::Ok(())
103}
104
105/// Peeks one value from a hash map. This helper resolves a borrowing issue in write_consts - that
106/// function needs to iterate through a hash map while mutating that hash map.
107fn peek<D: Dealloc>(hash_map: &HashMap<Any<D>, Seen>) -> Option<Any<D>> {
108    Some(hash_map.iter().next()?.0.clone())
109}
110
111/// Writes const definitions for objects, arrays in the right order (with no forward references).
112fn write_consts<D: Dealloc>(
113    write_json: &mut (impl WriteJson + ?Sized),
114    to_be_consts: &mut HashMap<Any<D>, Seen>,
115    const_refs: &mut HashMap<Any<D>, usize>,
116) -> fmt::Result {
117    while let Some(any) = peek(to_be_consts) {
118        write_consts_and_any(write_json, &any, to_be_consts, const_refs)?;
119    }
120    fmt::Result::Ok(())
121}
122
123/// Writes `any` using const references.
124fn write_with_const_refs<D: Dealloc>(
125    write_json: &mut (impl WriteJson + ?Sized),
126    any: Any<D>,
127    const_refs: &HashMap<Any<D>, usize>,
128) -> fmt::Result {
129    match any.get_type() {
130        Type::Object => {
131            if let Some(n) = const_refs.get(&any) {
132                write_json.write_str("_")?;
133                write_json.write_str(n.to_string().as_str())
134            } else {
135                write_json.write_list(
136                    '{',
137                    '}',
138                    any.try_move::<JsObjectRef<D>>().unwrap(),
139                    |w, (k, v)| {
140                        w.write_js_string(k)?;
141                        w.write_char(':')?;
142                        write_with_const_refs(w, v.clone(), const_refs)
143                    },
144                )
145            }
146        }
147        Type::Array => {
148            if let Some(n) = const_refs.get(&any) {
149                write_json.write_str("_")?;
150                write_json.write_str(n.to_string().as_str())
151            } else {
152                write_json.write_list(
153                    '[',
154                    ']',
155                    any.try_move::<JsArrayRef<D>>().unwrap(),
156                    |w, i| write_with_const_refs(w, i.clone(), const_refs),
157                )
158            }
159        }
160        _ => write_json.write_json(any),
161    }
162}
163
164pub trait WriteDjs: WriteJson {
165    /// Writes a DAG referred by `any` with const definitions for objects, arrays that are referred
166    /// multiple times.
167    fn write_djs<D: Dealloc>(&mut self, any: Any<D>, common_js: bool) -> fmt::Result {
168        let mut const_refs = HashMap::<Any<D>, usize>::new();
169        let mut const_tracker = ConstTracker {
170            visited: HashMap::new(),
171        };
172        const_tracker.track_consts_for_any(&any)?;
173        const_tracker
174            .visited
175            .retain(|_, seen| *seen == Seen::Repeatedly);
176        write_consts(self, &mut const_tracker.visited, &mut const_refs)?;
177        if common_js {
178            self.write_str("module.exports=")?;
179        } else {
180            self.write_str("export default ")?;
181        }
182        write_with_const_refs(self, any, &const_refs)
183    }
184}
185
186impl<T: WriteJson> WriteDjs for T {}
187
188pub fn to_djs(any: Any<impl Dealloc>, common_js: bool) -> result::Result<String, fmt::Error> {
189    let mut s = String::default();
190    s.write_djs(any, common_js)?;
191    Ok(s)
192}
193
194#[cfg(test)]
195mod test {
196    use wasm_bindgen_test::wasm_bindgen_test;
197
198    use crate::{
199        js::{any::Any, any_cast::AnyCast, js_string::new_string, new::New, null::Null},
200        mem::global::{Global, GLOBAL},
201        serializer::to_djs::WriteDjs,
202    };
203
204    #[test]
205    #[wasm_bindgen_test]
206    fn test() {
207        type A = Any<Global>;
208        let s = new_string(
209            GLOBAL,
210            ['a' as u16, '\\' as u16, 'b' as u16, '"' as u16, 31],
211        )
212        .to_ref();
213        let o = GLOBAL.new_js_object([(s, 2.0.move_to_any())]);
214        let a0 = GLOBAL.new_js_array([
215            1.0.move_to_any(),
216            true.move_to_any(),
217            Null().move_to_any(),
218            GLOBAL.new_js_array([]),
219            GLOBAL.new_js_string([]),
220            o.clone(),
221        ]);
222        let a0_as_any: Any<Global> = a0;
223        let a1: A = GLOBAL.new_js_array([a0_as_any.clone(), a0_as_any, o]);
224        let mut s = String::new();
225        s.write_djs(a1, false).unwrap();
226        assert_eq!(
227            s,
228            r#"const _0={"a\\b\"\u001F":2};const _1=[1,true,null,[],"",_0];export default [_1,_1,_0]"#
229        );
230    }
231}