1use crate::ir::*;
6use crate::traits::Writer;
7
8pub static LUA_WRITER: LuaWriterImpl = LuaWriterImpl;
10
11pub struct LuaWriterImpl;
13
14impl Writer for LuaWriterImpl {
15 fn language(&self) -> &'static str {
16 "lua"
17 }
18
19 fn extension(&self) -> &'static str {
20 "lua"
21 }
22
23 fn write(&self, program: &Program) -> String {
24 LuaWriter::emit(program)
25 }
26}
27
28pub struct LuaWriter {
30 output: String,
31 indent: usize,
32}
33
34impl LuaWriter {
35 pub fn new() -> Self {
36 Self {
37 output: String::new(),
38 indent: 0,
39 }
40 }
41
42 pub fn emit(program: &Program) -> String {
44 let mut writer = Self::new();
45 writer.write_program(program);
46 writer.output
47 }
48
49 fn write_program(&mut self, program: &Program) {
50 for stmt in &program.body {
51 self.write_stmt(stmt);
52 self.output.push('\n');
53 }
54 }
55
56 fn write_indent(&mut self) {
57 for _ in 0..self.indent {
58 self.output.push_str(" ");
59 }
60 }
61
62 fn write_stmt(&mut self, stmt: &Stmt) {
63 self.write_indent();
64 match stmt {
65 Stmt::Expr(expr) => {
66 self.write_expr(expr);
67 }
68
69 Stmt::Let { name, init, .. } => {
70 self.output.push_str("local ");
71 self.output.push_str(name);
72 if let Some(init) = init {
73 self.output.push_str(" = ");
74 self.write_expr(init);
75 }
76 }
77
78 Stmt::Destructure { pat, value, .. } => {
79 self.output.push_str("local ");
81 self.write_lua_pat(pat);
82 self.output.push_str(" = table.unpack(");
83 self.write_expr(value);
84 self.output.push(')');
85 }
86
87 Stmt::Block(stmts) => {
88 self.output.push_str("do\n");
89 self.indent += 1;
90 for s in stmts {
91 self.write_stmt(s);
92 self.output.push('\n');
93 }
94 self.indent -= 1;
95 self.write_indent();
96 self.output.push_str("end");
97 }
98
99 Stmt::If {
100 test,
101 consequent,
102 alternate,
103 ..
104 } => {
105 self.output.push_str("if ");
106 self.write_expr(test);
107 self.output.push_str(" then\n");
108 self.indent += 1;
109 self.write_stmt_body(consequent);
110 self.indent -= 1;
111 if let Some(alt) = alternate {
112 self.write_indent();
113 self.output.push_str("else\n");
114 self.indent += 1;
115 self.write_stmt_body(alt);
116 self.indent -= 1;
117 }
118 self.write_indent();
119 self.output.push_str("end");
120 }
121
122 Stmt::While { test, body, .. } => {
123 self.output.push_str("while ");
124 self.write_expr(test);
125 self.output.push_str(" do\n");
126 self.indent += 1;
127 self.write_stmt_body(body);
128 self.indent -= 1;
129 self.write_indent();
130 self.output.push_str("end");
131 }
132
133 Stmt::For {
134 init,
135 test,
136 update,
137 body,
138 ..
139 } => {
140 if let Some(init) = init {
142 self.write_stmt(init);
143 self.output.push('\n');
144 self.write_indent();
145 }
146 self.output.push_str("while ");
147 if let Some(test) = test {
148 self.write_expr(test);
149 } else {
150 self.output.push_str("true");
151 }
152 self.output.push_str(" do\n");
153 self.indent += 1;
154 self.write_stmt_body(body);
155 if let Some(update) = update {
156 self.write_indent();
157 self.write_expr(update);
158 self.output.push('\n');
159 }
160 self.indent -= 1;
161 self.write_indent();
162 self.output.push_str("end");
163 }
164
165 Stmt::ForIn {
166 variable,
167 iterable,
168 body,
169 ..
170 } => {
171 self.output.push_str("for ");
172 self.output.push_str(variable);
173 self.output.push_str(" in pairs(");
174 self.write_expr(iterable);
175 self.output.push_str(") do\n");
176 self.indent += 1;
177 self.write_stmt_body(body);
178 self.indent -= 1;
179 self.write_indent();
180 self.output.push_str("end");
181 }
182
183 Stmt::Return(expr) => {
184 self.output.push_str("return");
185 if let Some(e) = expr {
186 self.output.push(' ');
187 self.write_expr(e);
188 }
189 }
190
191 Stmt::Break => {
192 self.output.push_str("break");
193 }
194
195 Stmt::Continue => {
196 self.output
198 .push_str("-- continue (not supported in Lua 5.1)");
199 }
200
201 Stmt::TryCatch {
202 body,
203 catch_param,
204 catch_body,
205 finally_body,
206 ..
207 } => {
208 let param = catch_param.as_deref().unwrap_or("_err");
210 self.output.push_str("local _ok, ");
211 self.output.push_str(param);
212 self.output.push_str(" = pcall(function()\n");
213 self.indent += 1;
214 self.write_stmt_body(body);
215 self.indent -= 1;
216 self.write_indent();
217 self.output.push_str("end)\n");
218 if let Some(cb) = catch_body {
219 self.write_indent();
220 self.output.push_str("if not _ok then\n");
221 self.indent += 1;
222 self.write_stmt_body(cb);
223 self.indent -= 1;
224 self.write_indent();
225 self.output.push_str("end");
226 }
227 if let Some(fb) = finally_body {
228 self.output.push('\n');
229 self.write_stmt_body(fb);
230 }
231 }
232
233 Stmt::Function(f) => {
234 self.write_function(f);
235 }
236
237 Stmt::Import { source, names, .. } => {
238 if names.is_empty() {
241 self.output.push_str("require('");
242 self.output.push_str(source);
243 self.output.push_str("')");
244 } else if names.len() == 1 && !names[0].is_namespace {
245 let local_name = names[0].alias.as_deref().unwrap_or(&names[0].name);
246 self.output.push_str("local ");
247 self.output.push_str(local_name);
248 self.output.push_str(" = require('");
249 self.output.push_str(source);
250 self.output.push_str("')");
251 } else {
252 self.output.push_str("local _mod_");
254 let mod_var: String = source
256 .chars()
257 .map(|c| if c.is_alphanumeric() { c } else { '_' })
258 .collect();
259 self.output.push_str(&mod_var);
260 self.output.push_str(" = require('");
261 self.output.push_str(source);
262 self.output.push_str("')");
263 for n in names {
264 if n.is_namespace {
265 continue;
266 }
267 self.output.push('\n');
268 self.write_indent();
269 let local_name = n.alias.as_deref().unwrap_or(&n.name);
270 self.output.push_str("local ");
271 self.output.push_str(local_name);
272 self.output.push_str(" = _mod_");
273 self.output.push_str(&mod_var);
274 self.output.push('.');
275 self.output.push_str(&n.name);
276 }
277 }
278 }
279
280 Stmt::Export { .. } => {
281 self.output.push_str("-- export (not applicable in Lua)");
283 }
284
285 Stmt::Class {
286 name,
287 extends,
288 methods,
289 ..
290 } => {
291 self.output.push_str("local ");
298 self.output.push_str(name);
299 self.output.push_str(" = {}");
300 self.output.push('\n');
301 self.write_indent();
302 self.output.push_str(name);
303 self.output.push_str(".__index = ");
304 self.output.push_str(name);
305 if let Some(base) = extends {
306 self.output.push('\n');
307 self.write_indent();
308 self.output.push_str("setmetatable(");
309 self.output.push_str(name);
310 self.output.push_str(", { __index = ");
311 self.output.push_str(base);
312 self.output.push_str(" })");
313 }
314 for method in methods {
315 self.output.push('\n');
316 self.write_indent();
317 if method.name == "constructor" || method.name == "__init__" {
318 self.output.push_str("function ");
320 self.output.push_str(name);
321 self.output.push_str(".new(");
322 let params: Vec<_> =
323 method.params.iter().filter(|p| p.name != "self").collect();
324 for (i, p) in params.iter().enumerate() {
325 if i > 0 {
326 self.output.push_str(", ");
327 }
328 self.output.push_str(&p.name);
329 }
330 self.output.push_str(")\n");
331 self.indent += 1;
332 self.write_indent();
333 self.output.push_str("local self = setmetatable({}, ");
334 self.output.push_str(name);
335 self.output.push_str(")\n");
336 for s in &method.body {
337 self.write_stmt(s);
338 self.output.push('\n');
339 }
340 self.write_indent();
341 self.output.push_str("return self\n");
342 self.indent -= 1;
343 self.write_indent();
344 self.output.push_str("end");
345 } else if method.is_static {
346 self.output.push_str("function ");
347 self.output.push_str(name);
348 self.output.push('.');
349 self.output.push_str(&method.name);
350 self.output.push('(');
351 for (i, p) in method.params.iter().enumerate() {
352 if i > 0 {
353 self.output.push_str(", ");
354 }
355 self.output.push_str(&p.name);
356 }
357 self.output.push_str(")\n");
358 self.indent += 1;
359 for s in &method.body {
360 self.write_stmt(s);
361 self.output.push('\n');
362 }
363 self.indent -= 1;
364 self.write_indent();
365 self.output.push_str("end");
366 } else {
367 self.output.push_str("function ");
368 self.output.push_str(name);
369 self.output.push(':');
370 self.output.push_str(&method.name);
371 self.output.push('(');
372 let params: Vec<_> =
373 method.params.iter().filter(|p| p.name != "self").collect();
374 for (i, p) in params.iter().enumerate() {
375 if i > 0 {
376 self.output.push_str(", ");
377 }
378 self.output.push_str(&p.name);
379 }
380 self.output.push_str(")\n");
381 self.indent += 1;
382 for s in &method.body {
383 self.write_stmt(s);
384 self.output.push('\n');
385 }
386 self.indent -= 1;
387 self.write_indent();
388 self.output.push_str("end");
389 }
390 }
391 }
392
393 Stmt::Comment { text, block, .. } => {
394 if *block {
395 self.output.push_str("--[[");
396 self.output.push_str(text);
397 self.output.push_str("]]");
398 } else {
399 self.output.push_str("-- ");
400 self.output.push_str(text);
401 }
402 }
403 }
404 }
405
406 fn write_stmt_body(&mut self, stmt: &Stmt) {
407 match stmt {
408 Stmt::Block(stmts) => {
409 for s in stmts {
410 self.write_stmt(s);
411 self.output.push('\n');
412 }
413 }
414 _ => {
415 self.write_stmt(stmt);
416 self.output.push('\n');
417 }
418 }
419 }
420
421 fn write_function(&mut self, f: &Function) {
422 if f.name.is_empty() {
423 self.output.push_str("function(");
424 } else {
425 self.output.push_str("function ");
426 self.output.push_str(&f.name);
427 self.output.push('(');
428 }
429 for (i, param) in f.params.iter().enumerate() {
430 if i > 0 {
431 self.output.push_str(", ");
432 }
433 self.output.push_str(¶m.name);
435 }
436 self.output.push_str(")\n");
437 self.indent += 1;
438 for stmt in &f.body {
439 self.write_stmt(stmt);
440 self.output.push('\n');
441 }
442 self.indent -= 1;
443 self.write_indent();
444 self.output.push_str("end");
445 }
446
447 fn write_expr(&mut self, expr: &Expr) {
448 match expr {
449 Expr::Literal(lit) => self.write_literal(lit),
450
451 Expr::Ident(name) => {
452 self.output.push_str(name);
453 }
454
455 Expr::Binary {
456 left, op, right, ..
457 } => {
458 self.output.push('(');
459 self.write_expr(left);
460 self.output.push(' ');
461 self.write_binary_op(*op);
462 self.output.push(' ');
463 self.write_expr(right);
464 self.output.push(')');
465 }
466
467 Expr::Unary { op, expr, .. } => {
468 self.write_unary_op(*op);
469 self.write_expr(expr);
470 }
471
472 Expr::Call { callee, args, .. } => {
473 self.write_expr(callee);
474 self.output.push('(');
475 for (i, arg) in args.iter().enumerate() {
476 if i > 0 {
477 self.output.push_str(", ");
478 }
479 self.write_expr(arg);
480 }
481 self.output.push(')');
482 }
483
484 Expr::Member {
485 object,
486 property,
487 computed,
488 ..
489 } => {
490 self.write_expr(object);
491 if *computed {
492 self.output.push('[');
493 self.write_expr(property);
494 self.output.push(']');
495 } else if let Expr::Literal(Literal::String(s)) = property.as_ref() {
496 self.output.push('.');
497 self.output.push_str(s);
498 } else {
499 self.output.push('[');
500 self.write_expr(property);
501 self.output.push(']');
502 }
503 }
504
505 Expr::Array(items) => {
506 self.output.push('{');
507 for (i, item) in items.iter().enumerate() {
508 if i > 0 {
509 self.output.push_str(", ");
510 }
511 self.write_expr(item);
512 }
513 self.output.push('}');
514 }
515
516 Expr::Object(pairs) => {
517 self.output.push('{');
518 for (i, (key, value)) in pairs.iter().enumerate() {
519 if i > 0 {
520 self.output.push_str(", ");
521 }
522 if is_lua_identifier(key) {
523 self.output.push_str(key);
525 self.output.push_str(" = ");
526 } else {
527 self.output.push_str("[\"");
529 self.output.push_str(&escape_string(key));
530 self.output.push_str("\"] = ");
531 }
532 self.write_expr(value);
533 }
534 self.output.push('}');
535 }
536
537 Expr::Function(f) => {
538 self.write_function(f);
539 }
540
541 Expr::Conditional {
542 test,
543 consequent,
544 alternate,
545 ..
546 } => {
547 self.output.push('(');
549 self.write_expr(test);
550 self.output.push_str(" and ");
551 self.write_expr(consequent);
552 self.output.push_str(" or ");
553 self.write_expr(alternate);
554 self.output.push(')');
555 }
556
557 Expr::Assign { target, value, .. } => {
558 self.write_expr(target);
559 self.output.push_str(" = ");
560 self.write_expr(value);
561 }
562
563 Expr::TemplateLiteral(parts) => {
564 if parts.is_empty() {
566 self.output.push_str("\"\"");
567 return;
568 }
569 let exprs: Vec<Expr> = parts
570 .iter()
571 .filter_map(|p| match p {
572 TemplatePart::Text(s) if s.is_empty() => None,
573 TemplatePart::Text(s) => Some(Expr::string(s.clone())),
574 TemplatePart::Expr(e) => Some(*e.clone()),
575 })
576 .collect();
577 if exprs.is_empty() {
578 self.output.push_str("\"\"");
579 return;
580 }
581 if exprs.len() == 1 {
582 self.write_expr(&exprs[0]);
583 return;
584 }
585 self.output.push('(');
586 for (i, e) in exprs.iter().enumerate() {
587 if i > 0 {
588 self.output.push_str(" .. ");
589 }
590 self.write_expr(e);
591 }
592 self.output.push(')');
593 }
594 }
595 }
596
597 fn write_literal(&mut self, lit: &Literal) {
598 match lit {
599 Literal::Null => self.output.push_str("nil"),
600 Literal::Bool(b) => self.output.push_str(if *b { "true" } else { "false" }),
601 Literal::Number(n) => self.output.push_str(&n.to_string()),
602 Literal::String(s) => {
603 self.output.push('"');
604 self.output.push_str(&escape_string(s));
605 self.output.push('"');
606 }
607 }
608 }
609
610 fn write_binary_op(&mut self, op: BinaryOp) {
611 let s = match op {
612 BinaryOp::Add => "+",
613 BinaryOp::Sub => "-",
614 BinaryOp::Mul => "*",
615 BinaryOp::Div => "/",
616 BinaryOp::Mod => "%",
617 BinaryOp::Eq => "==",
618 BinaryOp::Ne => "~=",
619 BinaryOp::Lt => "<",
620 BinaryOp::Le => "<=",
621 BinaryOp::Gt => ">",
622 BinaryOp::Ge => ">=",
623 BinaryOp::And => "and",
624 BinaryOp::Or => "or",
625 BinaryOp::Concat => "..",
626 };
627 self.output.push_str(s);
628 }
629
630 fn write_unary_op(&mut self, op: UnaryOp) {
631 let s = match op {
632 UnaryOp::Neg => "-",
633 UnaryOp::Not => "not ",
634 };
635 self.output.push_str(s);
636 }
637
638 fn write_lua_pat(&mut self, pat: &Pat) {
640 match pat {
641 Pat::Ident(name) => {
642 self.output.push_str(name);
643 }
644 Pat::Array(elements, rest) => {
645 for (i, elem) in elements.iter().enumerate() {
646 if i > 0 {
647 self.output.push_str(", ");
648 }
649 match elem {
650 None => self.output.push('_'),
651 Some(p) => self.write_lua_pat(p),
652 }
653 }
654 if let Some(rest_name) = rest {
655 if !elements.is_empty() {
656 self.output.push_str(", ");
657 }
658 self.output.push_str(rest_name);
659 }
660 }
661 Pat::Object(fields) => {
662 for (i, field) in fields.iter().enumerate() {
663 if i > 0 {
664 self.output.push_str(", ");
665 }
666 self.write_lua_pat(&field.pat);
667 }
668 }
669 Pat::Rest(inner) => {
670 self.write_lua_pat(inner);
671 }
672 }
673 }
674}
675
676impl Default for LuaWriter {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682fn escape_string(s: &str) -> String {
683 let mut out = String::with_capacity(s.len());
684 for ch in s.chars() {
685 match ch {
686 '\\' => out.push_str("\\\\"),
687 '"' => out.push_str("\\\""),
688 '\n' => out.push_str("\\n"),
689 '\r' => out.push_str("\\r"),
690 '\t' => out.push_str("\\t"),
691 '\0' => out.push_str("\\0"),
692 c => out.push(c),
693 }
694 }
695 out
696}
697
698fn is_lua_identifier(s: &str) -> bool {
700 if s.is_empty() {
701 return false;
702 }
703 let mut chars = s.chars();
704 let first = chars.next().unwrap();
705 if !first.is_ascii_alphabetic() && first != '_' {
706 return false;
707 }
708 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_simple_let() {
717 let program = Program::new(vec![Stmt::const_decl("x", Expr::number(42))]);
718 let lua = LuaWriter::emit(&program);
719 assert_eq!(lua.trim(), "local x = 42");
720 }
721
722 #[test]
723 fn test_function_call() {
724 let program = Program::new(vec![Stmt::expr(Expr::call(
725 Expr::member(Expr::ident("console"), "log"),
726 vec![Expr::string("hello")],
727 ))]);
728 let lua = LuaWriter::emit(&program);
729 assert_eq!(lua.trim(), "console.log(\"hello\")");
730 }
731
732 #[test]
733 fn test_binary_expr() {
734 let program = Program::new(vec![Stmt::const_decl(
735 "sum",
736 Expr::binary(Expr::number(1), BinaryOp::Add, Expr::number(2)),
737 )]);
738 let lua = LuaWriter::emit(&program);
739 assert_eq!(lua.trim(), "local sum = (1 + 2)");
740 }
741
742 #[test]
743 fn test_logical_operators_idiomatic() {
744 let program = Program::new(vec![Stmt::const_decl(
746 "b",
747 Expr::binary(
748 Expr::bool(true),
749 BinaryOp::And,
750 Expr::binary(Expr::bool(false), BinaryOp::Or, Expr::bool(true)),
751 ),
752 )]);
753 let lua = LuaWriter::emit(&program);
754 assert!(lua.contains("and"), "should use `and`, got: {lua}");
755 assert!(lua.contains("or"), "should use `or`, got: {lua}");
756 assert!(!lua.contains("&&"), "should not use `&&`, got: {lua}");
757 assert!(!lua.contains("||"), "should not use `||`, got: {lua}");
758 }
759
760 #[test]
761 fn test_inequality_idiomatic() {
762 let program = Program::new(vec![Stmt::expr(Expr::binary(
764 Expr::ident("a"),
765 BinaryOp::Ne,
766 Expr::ident("b"),
767 ))]);
768 let lua = LuaWriter::emit(&program);
769 assert!(lua.contains("~="), "should use `~=`, got: {lua}");
770 assert!(!lua.contains("!="), "should not use `!=`, got: {lua}");
771 }
772
773 #[test]
774 fn test_null_is_nil() {
775 let program = Program::new(vec![Stmt::const_decl("x", Expr::null())]);
776 let lua = LuaWriter::emit(&program);
777 assert!(lua.contains("nil"), "should use `nil`, got: {lua}");
778 assert!(!lua.contains("null"), "should not use `null`, got: {lua}");
779 }
780
781 #[test]
782 fn test_object_idiomatic_keys() {
783 let program = Program::new(vec![Stmt::const_decl(
785 "t",
786 Expr::object(vec![
787 ("x".to_string(), Expr::number(1)),
788 ("__index".to_string(), Expr::null()),
789 ("1".to_string(), Expr::number(99)), ]),
791 )]);
792 let lua = LuaWriter::emit(&program);
793 assert!(
794 lua.contains("x = 1"),
795 "plain key should be bare, got: {lua}"
796 );
797 assert!(
798 lua.contains("__index = nil"),
799 "metamethod key should be bare, got: {lua}"
800 );
801 assert!(
802 lua.contains("[\"1\"] = 99"),
803 "numeric key should use brackets, got: {lua}"
804 );
805 }
806
807 #[test]
808 fn test_string_escaping() {
809 let program = Program::new(vec![Stmt::const_decl(
810 "s",
811 Expr::string("line1\nline2\ttab\"quote\\backslash\0null"),
812 )]);
813 let lua = LuaWriter::emit(&program);
814 assert!(lua.contains("\\n"), "newline should be escaped");
815 assert!(lua.contains("\\t"), "tab should be escaped");
816 assert!(lua.contains("\\\""), "quote should be escaped");
817 assert!(lua.contains("\\\\"), "backslash should be escaped");
818 assert!(lua.contains("\\0"), "null byte should be escaped");
819 }
820
821 #[test]
822 fn test_not_operator() {
823 let program = Program::new(vec![Stmt::expr(Expr::unary(
824 UnaryOp::Not,
825 Expr::bool(true),
826 ))]);
827 let lua = LuaWriter::emit(&program);
828 assert!(lua.contains("not "), "should use `not `, got: {lua}");
829 assert!(!lua.contains('!'), "should not use `!`, got: {lua}");
830 }
831
832 #[test]
833 fn test_for_in_multi_var() {
834 let program = Program::new(vec![Stmt::for_in(
835 "k, v",
836 Expr::call(Expr::ident("pairs"), vec![Expr::ident("t")]),
837 Stmt::block(vec![]),
838 )]);
839 let lua = LuaWriter::emit(&program);
840 assert!(
841 lua.contains("for k, v in pairs"),
842 "should preserve both loop vars, got: {lua}"
843 );
844 }
845
846 #[test]
847 fn test_unicode_string_preserved() {
848 let program = Program::new(vec![Stmt::const_decl("s", Expr::string("こんにちは 🌍"))]);
849 let lua = LuaWriter::emit(&program);
850 assert!(
851 lua.contains("こんにちは 🌍"),
852 "unicode should pass through unescaped, got: {lua}"
853 );
854 }
855
856 #[test]
857 fn test_line_comment() {
858 let program = Program::new(vec![
859 Stmt::comment_line("This is a comment"),
860 Stmt::let_decl("x", Some(Expr::number(1))),
861 ]);
862 let lua = LuaWriter::emit(&program);
863 assert!(lua.contains("-- This is a comment"), "got: {lua}");
864 assert!(lua.contains("local x = 1"), "got: {lua}");
865 }
866
867 #[test]
868 fn test_block_comment() {
869 let program = Program::new(vec![Stmt::comment_block("block comment")]);
870 let lua = LuaWriter::emit(&program);
871 assert!(lua.contains("--[[block comment]]"), "got: {lua}");
872 }
873}