1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
use crate::{
    js::{any::Any, js_array::JsArrayRef, js_object::JsObjectRef, type_::Type},
    mem::manager::Dealloc,
};

use core::{
    fmt::{self},
    result,
};

use std::collections::HashMap;

use super::to_json::WriteJson;

/// `Seen` is a bool-like enumeration to represent a "seen" status of a js compound (an object or
/// an array) visited by `ConstTracker`. In case of `Seen::Once`, the compound was visited just once
/// and if it remains with that status, it will be written out as a const. In case of
/// `Seen::Repeatedly`, the compound was visited more than once and will be written out as a const.
#[derive(PartialEq)]
enum Seen {
    Once,
    Repeatedly,
}

/// ConstTracker holds references to js compounds (objects or arrays) in two sets:
/// `visited_once` refers to compounds that we've seen just once so far;
/// `visited_repeatedly` refers to compounds that we've seen more than once.
/// When djs tracking pass is done, `visited_repeatedly` refers to compounds that will be written
/// out via const definitions.
/// Note that we use one ConstTracker for js objects and another for js arrays, keeping them
/// separate - to reduce set sizes and save on operations.
struct ConstTracker<D: Dealloc> {
    visited: HashMap<Any<D>, Seen>,
}

impl<D: Dealloc> ConstTracker<D> {
    /// Returns true if `any` was visited before; updates the `const_tracker` set, tracking whether
    /// `any` was visited just once (it's in `const_tracker.visited_once`) or more than once (it's
    /// in `visited_repeatedly` in this case since we are up to writing it out as a const).
    fn is_visited(&mut self, any: &Any<D>) -> bool {
        let optional_seen = self.visited.get_mut(any);
        if let Some(seen) = optional_seen {
            *seen = Seen::Repeatedly;
            true
        } else {
            self.visited.insert(any.clone(), Seen::Once);
            false
        }
    }

    /// Traverse a DAG referred by `any` (of any js type), tracking objects and arrays, including
    /// `any` itself.
    fn track_consts_for_any(&mut self, any: &Any<D>) -> fmt::Result {
        match any.get_type() {
            Type::Array | Type::Object => {
                if !self.is_visited(any) {
                    any.for_each::<fmt::Error>(|_k, v| self.track_consts_for_any(v))?;
                }
                Ok(())
            }
            _ => Ok(()),
        }
    }
}

/// Writes a const definition for a compound (an array or an object).
fn write_compound_const<D: Dealloc>(
    write_json: &mut (impl WriteJson + ?Sized),
    any: &Any<D>,
    to_be_consts: &mut HashMap<Any<D>, Seen>,
    const_refs: &mut HashMap<Any<D>, usize>,
) -> fmt::Result {
    any.for_each(|_k, v| write_consts_and_any(write_json, v, to_be_consts, const_refs))?;
    if to_be_consts.remove(any).is_some() {
        let id = const_refs.len();
        write_json.write_str("const _")?;
        write_json.write_str(id.to_string().as_str())?;
        write_json.write_char('=')?;
        write_with_const_refs(write_json, any.clone(), const_refs)?;
        const_refs.insert(any.clone(), id);
        write_json.write_char(';')
    } else {
        fmt::Result::Ok(())
    }
}

/// Writes a const js entity of any type (skipping over types other than object, array),
/// ensuring that its const dependencies are written out as well in the right order (with no
/// forward references).
fn write_consts_and_any<D: Dealloc>(
    write_json: &mut (impl WriteJson + ?Sized),
    any: &Any<D>,
    to_be_consts: &mut HashMap<Any<D>, Seen>,
    const_refs: &mut HashMap<Any<D>, usize>,
) -> fmt::Result {
    match any.get_type() {
        Type::Array | Type::Object => {
            write_compound_const(write_json, any, to_be_consts, const_refs)?;
        }
        _ => {}
    }
    fmt::Result::Ok(())
}

/// Peeks one value from a hash map. This helper resolves a borrowing issue in write_consts - that
/// function needs to iterate through a hash map while mutating that hash map.
fn peek<D: Dealloc>(hash_map: &HashMap<Any<D>, Seen>) -> Option<Any<D>> {
    Some(hash_map.iter().next()?.0.clone())
}

/// Writes const definitions for objects, arrays in the right order (with no forward references).
fn write_consts<D: Dealloc>(
    write_json: &mut (impl WriteJson + ?Sized),
    to_be_consts: &mut HashMap<Any<D>, Seen>,
    const_refs: &mut HashMap<Any<D>, usize>,
) -> fmt::Result {
    while let Some(any) = peek(to_be_consts) {
        write_consts_and_any(write_json, &any, to_be_consts, const_refs)?;
    }
    fmt::Result::Ok(())
}

/// Writes `any` using const references.
fn write_with_const_refs<D: Dealloc>(
    write_json: &mut (impl WriteJson + ?Sized),
    any: Any<D>,
    const_refs: &HashMap<Any<D>, usize>,
) -> fmt::Result {
    match any.get_type() {
        Type::Object => {
            if let Some(n) = const_refs.get(&any) {
                write_json.write_str("_")?;
                write_json.write_str(n.to_string().as_str())
            } else {
                write_json.write_list(
                    '{',
                    '}',
                    any.try_move::<JsObjectRef<D>>().unwrap(),
                    |w, (k, v)| {
                        w.write_js_string(k)?;
                        w.write_char(':')?;
                        write_with_const_refs(w, v.clone(), const_refs)
                    },
                )
            }
        }
        Type::Array => {
            if let Some(n) = const_refs.get(&any) {
                write_json.write_str("_")?;
                write_json.write_str(n.to_string().as_str())
            } else {
                write_json.write_list(
                    '[',
                    ']',
                    any.try_move::<JsArrayRef<D>>().unwrap(),
                    |w, i| write_with_const_refs(w, i.clone(), const_refs),
                )
            }
        }
        _ => write_json.write_json(any),
    }
}

pub trait WriteDjs: WriteJson {
    /// Writes a DAG referred by `any` with const definitions for objects, arrays that are referred
    /// multiple times.
    fn write_djs<D: Dealloc>(&mut self, any: Any<D>, common_js: bool) -> fmt::Result {
        let mut const_refs = HashMap::<Any<D>, usize>::new();
        let mut const_tracker = ConstTracker {
            visited: HashMap::new(),
        };
        const_tracker.track_consts_for_any(&any)?;
        const_tracker
            .visited
            .retain(|_, seen| *seen == Seen::Repeatedly);
        write_consts(self, &mut const_tracker.visited, &mut const_refs)?;
        if common_js {
            self.write_str("module.exports=")?;
        } else {
            self.write_str("export default ")?;
        }
        write_with_const_refs(self, any, &const_refs)
    }
}

impl<T: WriteJson> WriteDjs for T {}

pub fn to_djs(any: Any<impl Dealloc>, common_js: bool) -> result::Result<String, fmt::Error> {
    let mut s = String::default();
    s.write_djs(any, common_js)?;
    Ok(s)
}

#[cfg(test)]
mod test {
    use wasm_bindgen_test::wasm_bindgen_test;

    use crate::{
        js::{any::Any, any_cast::AnyCast, js_string::new_string, new::New, null::Null},
        mem::global::{Global, GLOBAL},
        serializer::to_djs::WriteDjs,
    };

    #[test]
    #[wasm_bindgen_test]
    fn test() {
        type A = Any<Global>;
        let s = new_string(
            GLOBAL,
            ['a' as u16, '\\' as u16, 'b' as u16, '"' as u16, 31],
        )
        .to_ref();
        let o = GLOBAL.new_js_object([(s, 2.0.move_to_any())]);
        let a0 = GLOBAL.new_js_array([
            1.0.move_to_any(),
            true.move_to_any(),
            Null().move_to_any(),
            GLOBAL.new_js_array([]),
            GLOBAL.new_js_string([]),
            o.clone(),
        ]);
        let a0_as_any: Any<Global> = a0;
        let a1: A = GLOBAL.new_js_array([a0_as_any.clone(), a0_as_any, o]);
        let mut s = String::new();
        s.write_djs(a1, false).unwrap();
        assert_eq!(
            s,
            r#"const _0={"a\\b\"\u001F":2};const _1=[1,true,null,[],"",_0];export default [_1,_1,_0]"#
        );
    }
}