Skip to main content

normalize_surface_syntax/output/
lua.rs

1//! Lua writer for surface-syntax IR.
2//!
3//! Emits surface-syntax IR as Lua source code.
4
5use crate::ir::*;
6use crate::traits::Writer;
7use std::fmt::Write;
8
9/// Static instance of the Lua writer for registry.
10pub static LUA_WRITER: LuaWriterImpl = LuaWriterImpl;
11
12/// Lua writer implementing the Writer trait.
13pub struct LuaWriterImpl;
14
15impl Writer for LuaWriterImpl {
16    fn language(&self) -> &'static str {
17        "lua"
18    }
19
20    fn extension(&self) -> &'static str {
21        "lua"
22    }
23
24    fn write(&self, program: &Program) -> String {
25        LuaWriter::emit(program)
26    }
27}
28
29/// Emits IR as Lua source code.
30pub struct LuaWriter {
31    output: String,
32    indent: usize,
33}
34
35impl LuaWriter {
36    pub fn new() -> Self {
37        Self {
38            output: String::new(),
39            indent: 0,
40        }
41    }
42
43    /// Emit a program to Lua source.
44    pub fn emit(program: &Program) -> String {
45        let mut writer = Self::new();
46        writer.write_program(program);
47        writer.output
48    }
49
50    fn write_program(&mut self, program: &Program) {
51        for stmt in &program.body {
52            self.write_stmt(stmt);
53            self.output.push('\n');
54        }
55    }
56
57    fn write_indent(&mut self) {
58        for _ in 0..self.indent {
59            self.output.push_str("  ");
60        }
61    }
62
63    fn write_stmt(&mut self, stmt: &Stmt) {
64        self.write_indent();
65        match stmt {
66            Stmt::Expr(expr) => {
67                self.write_expr(expr);
68            }
69
70            Stmt::Let { name, init, .. } => {
71                write!(self.output, "local {}", name).unwrap();
72                if let Some(init) = init {
73                    self.output.push_str(" = ");
74                    self.write_expr(init);
75                }
76            }
77
78            Stmt::Block(stmts) => {
79                self.output.push_str("do\n");
80                self.indent += 1;
81                for s in stmts {
82                    self.write_stmt(s);
83                    self.output.push('\n');
84                }
85                self.indent -= 1;
86                self.write_indent();
87                self.output.push_str("end");
88            }
89
90            Stmt::If {
91                test,
92                consequent,
93                alternate,
94            } => {
95                self.output.push_str("if ");
96                self.write_expr(test);
97                self.output.push_str(" then\n");
98                self.indent += 1;
99                self.write_stmt_body(consequent);
100                self.indent -= 1;
101                if let Some(alt) = alternate {
102                    self.write_indent();
103                    self.output.push_str("else\n");
104                    self.indent += 1;
105                    self.write_stmt_body(alt);
106                    self.indent -= 1;
107                }
108                self.write_indent();
109                self.output.push_str("end");
110            }
111
112            Stmt::While { test, body } => {
113                self.output.push_str("while ");
114                self.write_expr(test);
115                self.output.push_str(" do\n");
116                self.indent += 1;
117                self.write_stmt_body(body);
118                self.indent -= 1;
119                self.write_indent();
120                self.output.push_str("end");
121            }
122
123            Stmt::For {
124                init,
125                test,
126                update,
127                body,
128            } => {
129                // Lua doesn't have C-style for loops, emit as while
130                if let Some(init) = init {
131                    self.write_stmt(init);
132                    self.output.push('\n');
133                    self.write_indent();
134                }
135                self.output.push_str("while ");
136                if let Some(test) = test {
137                    self.write_expr(test);
138                } else {
139                    self.output.push_str("true");
140                }
141                self.output.push_str(" do\n");
142                self.indent += 1;
143                self.write_stmt_body(body);
144                if let Some(update) = update {
145                    self.write_indent();
146                    self.write_expr(update);
147                    self.output.push('\n');
148                }
149                self.indent -= 1;
150                self.write_indent();
151                self.output.push_str("end");
152            }
153
154            Stmt::ForIn {
155                variable,
156                iterable,
157                body,
158            } => {
159                write!(self.output, "for _, {} in pairs(", variable).unwrap();
160                self.write_expr(iterable);
161                self.output.push_str(") do\n");
162                self.indent += 1;
163                self.write_stmt_body(body);
164                self.indent -= 1;
165                self.write_indent();
166                self.output.push_str("end");
167            }
168
169            Stmt::Return(expr) => {
170                self.output.push_str("return");
171                if let Some(e) = expr {
172                    self.output.push(' ');
173                    self.write_expr(e);
174                }
175            }
176
177            Stmt::Break => {
178                self.output.push_str("break");
179            }
180
181            Stmt::Continue => {
182                // Lua 5.1 doesn't have continue, use goto in 5.2+
183                self.output
184                    .push_str("-- continue (not supported in Lua 5.1)");
185            }
186
187            Stmt::TryCatch {
188                body,
189                catch_param,
190                catch_body,
191                finally_body,
192            } => {
193                // Lua uses pcall/xpcall for error handling
194                let param = catch_param.as_deref().unwrap_or("_err");
195                self.output.push_str("local _ok, ");
196                self.output.push_str(param);
197                self.output.push_str(" = pcall(function()\n");
198                self.indent += 1;
199                self.write_stmt_body(body);
200                self.indent -= 1;
201                self.write_indent();
202                self.output.push_str("end)\n");
203                if let Some(cb) = catch_body {
204                    self.write_indent();
205                    self.output.push_str("if not _ok then\n");
206                    self.indent += 1;
207                    self.write_stmt_body(cb);
208                    self.indent -= 1;
209                    self.write_indent();
210                    self.output.push_str("end");
211                }
212                if let Some(fb) = finally_body {
213                    self.output.push('\n');
214                    self.write_stmt_body(fb);
215                }
216            }
217
218            Stmt::Function(f) => {
219                self.write_function(f);
220            }
221        }
222    }
223
224    fn write_stmt_body(&mut self, stmt: &Stmt) {
225        match stmt {
226            Stmt::Block(stmts) => {
227                for s in stmts {
228                    self.write_stmt(s);
229                    self.output.push('\n');
230                }
231            }
232            _ => {
233                self.write_stmt(stmt);
234                self.output.push('\n');
235            }
236        }
237    }
238
239    fn write_function(&mut self, f: &Function) {
240        if f.name.is_empty() {
241            self.output.push_str("function(");
242        } else {
243            write!(self.output, "function {}(", f.name).unwrap();
244        }
245        for (i, param) in f.params.iter().enumerate() {
246            if i > 0 {
247                self.output.push_str(", ");
248            }
249            self.output.push_str(param);
250        }
251        self.output.push_str(")\n");
252        self.indent += 1;
253        for stmt in &f.body {
254            self.write_stmt(stmt);
255            self.output.push('\n');
256        }
257        self.indent -= 1;
258        self.write_indent();
259        self.output.push_str("end");
260    }
261
262    fn write_expr(&mut self, expr: &Expr) {
263        match expr {
264            Expr::Literal(lit) => self.write_literal(lit),
265
266            Expr::Ident(name) => {
267                self.output.push_str(name);
268            }
269
270            Expr::Binary { left, op, right } => {
271                self.output.push('(');
272                self.write_expr(left);
273                self.output.push(' ');
274                self.write_binary_op(*op);
275                self.output.push(' ');
276                self.write_expr(right);
277                self.output.push(')');
278            }
279
280            Expr::Unary { op, expr } => {
281                self.write_unary_op(*op);
282                self.write_expr(expr);
283            }
284
285            Expr::Call { callee, args } => {
286                self.write_expr(callee);
287                self.output.push('(');
288                for (i, arg) in args.iter().enumerate() {
289                    if i > 0 {
290                        self.output.push_str(", ");
291                    }
292                    self.write_expr(arg);
293                }
294                self.output.push(')');
295            }
296
297            Expr::Member {
298                object,
299                property,
300                computed,
301            } => {
302                self.write_expr(object);
303                if *computed {
304                    self.output.push('[');
305                    self.write_expr(property);
306                    self.output.push(']');
307                } else if let Expr::Literal(Literal::String(s)) = property.as_ref() {
308                    self.output.push('.');
309                    self.output.push_str(s);
310                } else {
311                    self.output.push('[');
312                    self.write_expr(property);
313                    self.output.push(']');
314                }
315            }
316
317            Expr::Array(items) => {
318                self.output.push('{');
319                for (i, item) in items.iter().enumerate() {
320                    if i > 0 {
321                        self.output.push_str(", ");
322                    }
323                    self.write_expr(item);
324                }
325                self.output.push('}');
326            }
327
328            Expr::Object(pairs) => {
329                self.output.push('{');
330                for (i, (key, value)) in pairs.iter().enumerate() {
331                    if i > 0 {
332                        self.output.push_str(", ");
333                    }
334                    write!(self.output, "[\"{}\"] = ", key).unwrap();
335                    self.write_expr(value);
336                }
337                self.output.push('}');
338            }
339
340            Expr::Function(f) => {
341                self.write_function(f);
342            }
343
344            Expr::Conditional {
345                test,
346                consequent,
347                alternate,
348            } => {
349                // Lua doesn't have ternary, use `a and b or c` pattern
350                self.output.push('(');
351                self.write_expr(test);
352                self.output.push_str(" and ");
353                self.write_expr(consequent);
354                self.output.push_str(" or ");
355                self.write_expr(alternate);
356                self.output.push(')');
357            }
358
359            Expr::Assign { target, value } => {
360                self.write_expr(target);
361                self.output.push_str(" = ");
362                self.write_expr(value);
363            }
364        }
365    }
366
367    fn write_literal(&mut self, lit: &Literal) {
368        match lit {
369            Literal::Null => self.output.push_str("nil"),
370            Literal::Bool(b) => write!(self.output, "{}", b).unwrap(),
371            Literal::Number(n) => write!(self.output, "{}", n).unwrap(),
372            Literal::String(s) => write!(self.output, "\"{}\"", escape_string(s)).unwrap(),
373        }
374    }
375
376    fn write_binary_op(&mut self, op: BinaryOp) {
377        let s = match op {
378            BinaryOp::Add => "+",
379            BinaryOp::Sub => "-",
380            BinaryOp::Mul => "*",
381            BinaryOp::Div => "/",
382            BinaryOp::Mod => "%",
383            BinaryOp::Eq => "==",
384            BinaryOp::Ne => "~=",
385            BinaryOp::Lt => "<",
386            BinaryOp::Le => "<=",
387            BinaryOp::Gt => ">",
388            BinaryOp::Ge => ">=",
389            BinaryOp::And => "and",
390            BinaryOp::Or => "or",
391            BinaryOp::Concat => "..",
392        };
393        self.output.push_str(s);
394    }
395
396    fn write_unary_op(&mut self, op: UnaryOp) {
397        let s = match op {
398            UnaryOp::Neg => "-",
399            UnaryOp::Not => "not ",
400        };
401        self.output.push_str(s);
402    }
403}
404
405impl Default for LuaWriter {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411fn escape_string(s: &str) -> String {
412    s.replace('\\', "\\\\")
413        .replace('"', "\\\"")
414        .replace('\n', "\\n")
415        .replace('\r', "\\r")
416        .replace('\t', "\\t")
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_simple_let() {
425        let program = Program::new(vec![Stmt::const_decl("x", Expr::number(42))]);
426        let lua = LuaWriter::emit(&program);
427        assert_eq!(lua.trim(), "local x = 42");
428    }
429
430    #[test]
431    fn test_function_call() {
432        let program = Program::new(vec![Stmt::expr(Expr::call(
433            Expr::member(Expr::ident("console"), "log"),
434            vec![Expr::string("hello")],
435        ))]);
436        let lua = LuaWriter::emit(&program);
437        assert_eq!(lua.trim(), "console.log(\"hello\")");
438    }
439
440    #[test]
441    fn test_binary_expr() {
442        let program = Program::new(vec![Stmt::const_decl(
443            "sum",
444            Expr::binary(Expr::number(1), BinaryOp::Add, Expr::number(2)),
445        )]);
446        let lua = LuaWriter::emit(&program);
447        assert_eq!(lua.trim(), "local sum = (1 + 2)");
448    }
449}