Skip to main content

normalize_surface_syntax/
registry.rs

1//! Registry for readers and writers.
2
3use crate::traits::{Reader, Writer};
4use std::sync::{OnceLock, RwLock};
5
6/// Global reader registry.
7static READERS: RwLock<Vec<&'static dyn Reader>> = RwLock::new(Vec::new());
8static READERS_INITIALIZED: OnceLock<()> = OnceLock::new();
9
10/// Global writer registry.
11static WRITERS: RwLock<Vec<&'static dyn Writer>> = RwLock::new(Vec::new());
12static WRITERS_INITIALIZED: OnceLock<()> = OnceLock::new();
13
14/// Register a custom reader.
15pub fn register_reader(reader: &'static dyn Reader) {
16    READERS.write().unwrap().push(reader);
17}
18
19/// Register a custom writer.
20pub fn register_writer(writer: &'static dyn Writer) {
21    WRITERS.write().unwrap().push(writer);
22}
23
24fn init_readers() {
25    READERS_INITIALIZED.get_or_init(|| {
26        #[cfg(feature = "read-typescript")]
27        {
28            register_reader(&crate::input::typescript::TYPESCRIPT_READER);
29        }
30        #[cfg(feature = "read-lua")]
31        {
32            register_reader(&crate::input::lua::LUA_READER);
33        }
34        #[cfg(feature = "read-python")]
35        {
36            register_reader(&crate::input::python::PYTHON_READER);
37        }
38    });
39}
40
41fn init_writers() {
42    WRITERS_INITIALIZED.get_or_init(|| {
43        #[cfg(feature = "write-lua")]
44        {
45            register_writer(&crate::output::lua::LUA_WRITER);
46        }
47        #[cfg(feature = "write-typescript")]
48        {
49            register_writer(&crate::output::typescript::TYPESCRIPT_WRITER);
50        }
51        #[cfg(feature = "write-python")]
52        {
53            register_writer(&crate::output::python::PYTHON_WRITER);
54        }
55    });
56}
57
58/// Get a reader by language name.
59pub fn reader_for_language(lang: &str) -> Option<&'static dyn Reader> {
60    init_readers();
61    READERS
62        .read()
63        .unwrap()
64        .iter()
65        .find(|r| r.language() == lang)
66        .copied()
67}
68
69/// Get a reader by file extension.
70pub fn reader_for_extension(ext: &str) -> Option<&'static dyn Reader> {
71    init_readers();
72    READERS
73        .read()
74        .unwrap()
75        .iter()
76        .find(|r| r.extensions().contains(&ext))
77        .copied()
78}
79
80/// Get a writer by language name.
81pub fn writer_for_language(lang: &str) -> Option<&'static dyn Writer> {
82    init_writers();
83    WRITERS
84        .read()
85        .unwrap()
86        .iter()
87        .find(|w| w.language() == lang)
88        .copied()
89}
90
91/// Get all registered readers.
92pub fn readers() -> Vec<&'static dyn Reader> {
93    init_readers();
94    READERS.read().unwrap().clone()
95}
96
97/// Get all registered writers.
98pub fn writers() -> Vec<&'static dyn Writer> {
99    init_writers();
100    WRITERS.read().unwrap().clone()
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::ir::StructureEq;
107
108    #[test]
109    #[cfg(feature = "read-typescript")]
110    fn test_reader_lookup() {
111        let reader = reader_for_language("typescript").expect("typescript reader");
112        assert_eq!(reader.language(), "typescript");
113        assert!(reader.extensions().contains(&"ts"));
114
115        let reader = reader_for_extension("tsx").expect("tsx extension");
116        assert_eq!(reader.language(), "typescript");
117    }
118
119    #[test]
120    #[cfg(feature = "write-lua")]
121    fn test_writer_lookup() {
122        let writer = writer_for_language("lua").expect("lua writer");
123        assert_eq!(writer.language(), "lua");
124        assert_eq!(writer.extension(), "lua");
125    }
126
127    #[test]
128    #[cfg(all(feature = "read-typescript", feature = "write-lua"))]
129    fn test_roundtrip_via_registry() {
130        let reader = reader_for_language("typescript").unwrap();
131        let writer = writer_for_language("lua").unwrap();
132
133        let ir = reader.read("const x = 1 + 2;").unwrap();
134        let lua = writer.write(&ir);
135
136        assert!(lua.contains("local x"));
137    }
138
139    #[test]
140    #[cfg(feature = "read-lua")]
141    fn test_lua_reader_lookup() {
142        let reader = reader_for_language("lua").expect("lua reader");
143        assert_eq!(reader.language(), "lua");
144        assert!(reader.extensions().contains(&"lua"));
145    }
146
147    #[test]
148    #[cfg(feature = "write-typescript")]
149    fn test_typescript_writer_lookup() {
150        let writer = writer_for_language("typescript").expect("typescript writer");
151        assert_eq!(writer.language(), "typescript");
152        assert_eq!(writer.extension(), "ts");
153    }
154
155    #[test]
156    #[cfg(all(feature = "read-lua", feature = "write-typescript"))]
157    fn test_lua_to_typescript_roundtrip() {
158        let reader = reader_for_language("lua").unwrap();
159        let writer = writer_for_language("typescript").unwrap();
160
161        let ir = reader.read("local x = 1 + 2").unwrap();
162        let ts = writer.write(&ir);
163
164        assert!(ts.contains("let x") || ts.contains("const x"));
165        assert!(ts.contains("1 + 2") || ts.contains("(1 + 2)"));
166    }
167
168    #[test]
169    #[cfg(all(feature = "read-typescript", feature = "write-typescript"))]
170    fn test_typescript_roundtrip() {
171        let reader = reader_for_language("typescript").unwrap();
172        let writer = writer_for_language("typescript").unwrap();
173
174        let ir = reader.read("const x = 1 + 2;").unwrap();
175        let ts = writer.write(&ir);
176
177        assert!(ts.contains("const x"));
178    }
179
180    // ========================================================================
181    // Roundtrip tests with structure_eq
182    // ========================================================================
183    //
184    // These tests verify that IR is preserved through:
185    //   Source₁ → IR₁ → Source₂ → IR₂
186    // Using structure_eq to ignore surface hints (mutable, computed, etc.)
187
188    #[test]
189    #[cfg(all(
190        feature = "read-typescript",
191        feature = "write-lua",
192        feature = "read-lua"
193    ))]
194    fn test_structure_eq_ts_lua_variable() {
195        let ts_reader = reader_for_language("typescript").unwrap();
196        let lua_writer = writer_for_language("lua").unwrap();
197        let lua_reader = reader_for_language("lua").unwrap();
198
199        // TS → IR₁
200        let ir1 = ts_reader.read("const x = 42;").unwrap();
201        // IR₁ → Lua
202        let lua = lua_writer.write(&ir1);
203        // Lua → IR₂
204        let ir2 = lua_reader.read(&lua).unwrap();
205
206        assert!(
207            ir1.structure_eq(&ir2),
208            "IR mismatch:\nIR₁: {:?}\nLua: {}\nIR₂: {:?}",
209            ir1,
210            lua,
211            ir2
212        );
213    }
214
215    #[test]
216    #[cfg(all(
217        feature = "read-typescript",
218        feature = "write-lua",
219        feature = "read-lua"
220    ))]
221    fn test_structure_eq_ts_lua_binary_expr() {
222        let ts_reader = reader_for_language("typescript").unwrap();
223        let lua_writer = writer_for_language("lua").unwrap();
224        let lua_reader = reader_for_language("lua").unwrap();
225
226        let ir1 = ts_reader.read("let result = 1 + 2 * 3;").unwrap();
227        let lua = lua_writer.write(&ir1);
228        let ir2 = lua_reader.read(&lua).unwrap();
229
230        assert!(
231            ir1.structure_eq(&ir2),
232            "IR mismatch:\nIR₁: {:?}\nLua: {}\nIR₂: {:?}",
233            ir1,
234            lua,
235            ir2
236        );
237    }
238
239    #[test]
240    #[cfg(all(
241        feature = "read-typescript",
242        feature = "write-lua",
243        feature = "read-lua"
244    ))]
245    fn test_structure_eq_ts_lua_function_call() {
246        let ts_reader = reader_for_language("typescript").unwrap();
247        let lua_writer = writer_for_language("lua").unwrap();
248        let lua_reader = reader_for_language("lua").unwrap();
249
250        let ir1 = ts_reader.read("console.log(\"hello\", 42);").unwrap();
251        let lua = lua_writer.write(&ir1);
252        let ir2 = lua_reader.read(&lua).unwrap();
253
254        assert!(
255            ir1.structure_eq(&ir2),
256            "IR mismatch:\nIR₁: {:?}\nLua: {}\nIR₂: {:?}",
257            ir1,
258            lua,
259            ir2
260        );
261    }
262
263    #[test]
264    #[cfg(all(
265        feature = "read-typescript",
266        feature = "write-lua",
267        feature = "read-lua"
268    ))]
269    fn test_structure_eq_ts_lua_if_statement() {
270        let ts_reader = reader_for_language("typescript").unwrap();
271        let lua_writer = writer_for_language("lua").unwrap();
272        let lua_reader = reader_for_language("lua").unwrap();
273
274        let ir1 = ts_reader.read("if (x > 0) { console.log(x); }").unwrap();
275        let lua = lua_writer.write(&ir1);
276        let ir2 = lua_reader.read(&lua).unwrap();
277
278        assert!(
279            ir1.structure_eq(&ir2),
280            "IR mismatch:\nIR₁: {:?}\nLua: {}\nIR₂: {:?}",
281            ir1,
282            lua,
283            ir2
284        );
285    }
286
287    #[test]
288    #[cfg(all(
289        feature = "read-lua",
290        feature = "write-typescript",
291        feature = "read-typescript"
292    ))]
293    fn test_structure_eq_lua_ts_variable() {
294        let lua_reader = reader_for_language("lua").unwrap();
295        let ts_writer = writer_for_language("typescript").unwrap();
296        let ts_reader = reader_for_language("typescript").unwrap();
297
298        // Lua → IR₁
299        let ir1 = lua_reader.read("local x = 42").unwrap();
300        // IR₁ → TS
301        let ts = ts_writer.write(&ir1);
302        // TS → IR₂
303        let ir2 = ts_reader.read(&ts).unwrap();
304
305        assert!(
306            ir1.structure_eq(&ir2),
307            "IR mismatch:\nIR₁: {:?}\nTS: {}\nIR₂: {:?}",
308            ir1,
309            ts,
310            ir2
311        );
312    }
313
314    #[test]
315    #[cfg(all(
316        feature = "read-lua",
317        feature = "write-typescript",
318        feature = "read-typescript"
319    ))]
320    fn test_structure_eq_lua_ts_function() {
321        let lua_reader = reader_for_language("lua").unwrap();
322        let ts_writer = writer_for_language("typescript").unwrap();
323        let ts_reader = reader_for_language("typescript").unwrap();
324
325        let ir1 = lua_reader
326            .read("function add(a, b) return a + b end")
327            .unwrap();
328        let ts = ts_writer.write(&ir1);
329        let ir2 = ts_reader.read(&ts).unwrap();
330
331        assert!(
332            ir1.structure_eq(&ir2),
333            "IR mismatch:\nIR₁: {:?}\nTS: {}\nIR₂: {:?}",
334            ir1,
335            ts,
336            ir2
337        );
338    }
339}