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