#![allow(clippy::too_many_lines)]
use valua_ast::{BinaryOp, Block, Call, Expression, FunctionBody, Statement, TableField, UnaryOp};
pub use error::CodeGenError;
mod error;
pub use valua_ast::LuaTarget;
#[derive(Debug, Clone)]
pub struct EmitOptions {
pub indent: String,
pub target: LuaTarget,
pub emit_header_comment: bool,
}
impl Default for EmitOptions {
fn default() -> Self {
Self {
indent: " ".to_string(),
target: LuaTarget::default(),
emit_header_comment: false,
}
}
}
pub trait CodeGen {
fn emit(&self, block: &Block) -> Result<String, CodeGenError>;
}
pub struct LuaEmitter {
pub options: EmitOptions,
}
impl LuaEmitter {
#[must_use]
pub fn new(options: EmitOptions) -> Self {
Self { options }
}
#[must_use]
pub fn lua51() -> Self {
Self::new(EmitOptions {
target: LuaTarget::Lua51,
..EmitOptions::default()
})
}
#[must_use]
pub fn luajit() -> Self {
Self::new(EmitOptions {
target: LuaTarget::LuaJIT,
..EmitOptions::default()
})
}
}
impl CodeGen for LuaEmitter {
fn emit(&self, block: &Block) -> Result<String, CodeGenError> {
let mut ctx = EmitContext::new(&self.options);
if self.options.emit_header_comment {
ctx.push("-- Generated by valua v");
ctx.push(env!("CARGO_PKG_VERSION"));
ctx.push("\n");
}
ctx.emit_block(block)?;
Ok(ctx.finish())
}
}
fn binary_op_str(op: BinaryOp) -> &'static str {
match op {
BinaryOp::Add => "+",
BinaryOp::Sub => "-",
BinaryOp::Mul => "*",
BinaryOp::Div => "/",
BinaryOp::Mod => "%",
BinaryOp::Pow => "^",
BinaryOp::IDiv => "//",
BinaryOp::Concat => "..",
BinaryOp::Lt => "<",
BinaryOp::Le => "<=",
BinaryOp::Gt => ">",
BinaryOp::Ge => ">=",
BinaryOp::Eq => "==",
BinaryOp::Ne => "~=",
BinaryOp::And => "and",
BinaryOp::Or => "or",
BinaryOp::BitwiseAnd => "&",
BinaryOp::BitwiseOr => "|",
BinaryOp::BitwiseXor => "~",
BinaryOp::Shl => "<<",
BinaryOp::Shr => ">>",
}
}
fn binary_op_prec(op: BinaryOp) -> u8 {
match op {
BinaryOp::Or => 1,
BinaryOp::And => 3,
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge | BinaryOp::Eq | BinaryOp::Ne => {
5
}
BinaryOp::BitwiseOr => 7,
BinaryOp::BitwiseXor => 9,
BinaryOp::BitwiseAnd => 11,
BinaryOp::Shl | BinaryOp::Shr => 13,
BinaryOp::Concat => 16,
BinaryOp::Add | BinaryOp::Sub => 17,
BinaryOp::Mul | BinaryOp::Div | BinaryOp::IDiv | BinaryOp::Mod => 19,
BinaryOp::Pow => 24,
}
}
fn is_right_assoc(op: BinaryOp) -> bool {
matches!(op, BinaryOp::Pow | BinaryOp::Concat)
}
fn expr_outer_prec(expr: &Expression) -> u8 {
match expr {
Expression::BinOp(_, op, _, _) => binary_op_prec(*op),
Expression::UnOp(_, _, _) => 21,
_ => u8::MAX,
}
}
pub(crate) struct EmitContext<'opts> {
options: &'opts EmitOptions,
buf: String,
depth: usize,
}
impl<'opts> EmitContext<'opts> {
pub(crate) fn new(options: &'opts EmitOptions) -> Self {
Self {
options,
buf: String::new(),
depth: 0,
}
}
pub(crate) fn finish(self) -> String {
self.buf
}
fn push(&mut self, s: &str) {
self.buf.push_str(s);
}
fn push_indent(&mut self) {
self.buf.push_str(&self.options.indent.repeat(self.depth));
}
pub(crate) fn emit_block(&mut self, block: &Block) -> Result<(), CodeGenError> {
for stmt in &block.stmts {
self.emit_statement(stmt)?;
}
Ok(())
}
pub(crate) fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CodeGenError> {
match stmt {
Statement::LocalDecl(d) => {
self.push_indent();
self.push("local ");
for (i, name) in d.names.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.push(&name.name);
}
if !d.values.is_empty() {
self.push(" = ");
for (i, val) in d.values.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(val)?;
}
}
self.push("\n");
}
Statement::Assign(a) => {
self.push_indent();
for (i, target) in a.targets.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(target)?;
}
self.push(" = ");
for (i, val) in a.values.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(val)?;
}
self.push("\n");
}
Statement::ExprStmt(e) => {
self.push_indent();
self.emit_expression(e)?;
self.push("\n");
}
Statement::Return(r) => {
self.push_indent();
self.push("return");
if !r.values.is_empty() {
self.push(" ");
for (i, val) in r.values.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(val)?;
}
}
self.push("\n");
}
Statement::Break(_) => {
self.push_indent();
self.push("break\n");
}
Statement::Goto(g) => {
self.push_indent();
self.push("goto ");
self.push(&g.label);
self.push("\n");
}
Statement::Label(l) => {
self.push_indent();
self.push("::");
self.push(&l.name);
self.push("::\n");
}
Statement::Do(d) => {
self.push_indent();
self.push("do\n");
self.depth += 1;
self.emit_block(&d.body)?;
self.depth -= 1;
self.push_indent();
self.push("end\n");
}
Statement::While(w) => {
self.push_indent();
self.push("while ");
self.emit_expression(&w.condition)?;
self.push(" do\n");
self.depth += 1;
self.emit_block(&w.body)?;
self.depth -= 1;
self.push_indent();
self.push("end\n");
}
Statement::Repeat(r) => {
self.push_indent();
self.push("repeat\n");
self.depth += 1;
self.emit_block(&r.body)?;
self.depth -= 1;
self.push_indent();
self.push("until ");
self.emit_expression(&r.condition)?;
self.push("\n");
}
Statement::If(i) => {
self.push_indent();
self.push("if ");
self.emit_expression(&i.condition)?;
self.push(" then\n");
self.depth += 1;
self.emit_block(&i.then_block)?;
self.depth -= 1;
for elseif in &i.elseif_clauses {
self.push_indent();
self.push("elseif ");
self.emit_expression(&elseif.condition)?;
self.push(" then\n");
self.depth += 1;
self.emit_block(&elseif.body)?;
self.depth -= 1;
}
if let Some(ref else_block) = i.else_block {
self.push_indent();
self.push("else\n");
self.depth += 1;
self.emit_block(else_block)?;
self.depth -= 1;
}
self.push_indent();
self.push("end\n");
}
Statement::NumericFor(f) => {
self.push_indent();
self.push("for ");
self.push(&f.var);
self.push(" = ");
self.emit_expression(&f.start)?;
self.push(", ");
self.emit_expression(&f.limit)?;
if let Some(ref step) = f.step {
self.push(", ");
self.emit_expression(step)?;
}
self.push(" do\n");
self.depth += 1;
self.emit_block(&f.body)?;
self.depth -= 1;
self.push_indent();
self.push("end\n");
}
Statement::GenericFor(f) => {
self.push_indent();
self.push("for ");
for (i, var) in f.vars.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.push(var);
}
self.push(" in ");
for (i, iter) in f.iterators.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(iter)?;
}
self.push(" do\n");
self.depth += 1;
self.emit_block(&f.body)?;
self.depth -= 1;
self.push_indent();
self.push("end\n");
}
Statement::FunctionDecl(f) => {
self.push_indent();
self.push("function ");
let parts = f.name.parts.join(".");
self.push(&parts);
if let Some(ref method) = f.name.method {
self.push(":");
self.push(method);
}
self.emit_function_body(&f.func)?;
self.push("\n");
}
Statement::LocalFunctionDecl(f) => {
self.push_indent();
self.push("local function ");
self.push(&f.name);
self.emit_function_body(&f.func)?;
self.push("\n");
}
}
Ok(())
}
fn emit_function_body(&mut self, func: &FunctionBody) -> Result<(), CodeGenError> {
self.push("(");
for (i, param) in func.params.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.push(¶m.name);
}
if func.is_vararg {
if !func.params.is_empty() {
self.push(", ");
}
self.push("...");
}
self.push(")\n");
self.depth += 1;
self.emit_block(&func.body)?;
self.depth -= 1;
self.push_indent();
self.push("end");
Ok(())
}
pub(crate) fn emit_expression(&mut self, expr: &Expression) -> Result<(), CodeGenError> {
self.emit_expr_prec(expr, 0)
}
fn emit_expr_prec(&mut self, expr: &Expression, min_prec: u8) -> Result<(), CodeGenError> {
let ep = expr_outer_prec(expr);
let needs_parens = ep < min_prec;
if needs_parens {
self.push("(");
}
self.emit_expr_inner(expr)?;
if needs_parens {
self.push(")");
}
Ok(())
}
fn emit_expr_inner(&mut self, expr: &Expression) -> Result<(), CodeGenError> {
match expr {
Expression::Nil(_) => self.push("nil"),
Expression::True(_) => self.push("true"),
Expression::False(_) => self.push("false"),
Expression::Vararg(_) => self.push("..."),
Expression::Integer(v, _) => {
let s = v.to_string();
self.push(&s);
}
Expression::Float(f, span) => {
if f.is_nan() {
return Err(CodeGenError::UnsupportedNode {
target: "Lua 5.1",
detail: "NaN has no literal representation; use `0/0` or avoid NaN"
.to_string(),
span: *span,
});
}
if f.is_infinite() {
return Err(CodeGenError::UnsupportedNode {
target: "Lua 5.1",
detail: "Infinity has no literal representation; use `math.huge`"
.to_string(),
span: *span,
});
}
let s = f.to_string();
self.push(&s);
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
self.push(".0");
}
}
Expression::String(s, _) => {
self.push("\"");
let escaped = emit_string_content(s);
self.push(&escaped);
self.push("\"");
}
Expression::Name(n, _) => self.push(n),
Expression::Index(base, field, _) => {
self.emit_expr_prec(base, u8::MAX)?;
self.push(".");
self.push(field);
}
Expression::IndexExpr(base, key, _) => {
self.emit_expr_prec(base, u8::MAX)?;
self.push("[");
self.emit_expression(key)?;
self.push("]");
}
Expression::BinOp(lhs, op, rhs, _) => {
let prec = binary_op_prec(*op);
let ra = is_right_assoc(*op);
let lhs_min = if ra { prec + 1 } else { prec };
let rhs_min = if ra { prec } else { prec + 1 };
self.emit_expr_prec(lhs, lhs_min)?;
self.push(" ");
self.push(binary_op_str(*op));
self.push(" ");
self.emit_expr_prec(rhs, rhs_min)?;
}
Expression::UnOp(op, operand, _) => {
match op {
UnaryOp::Neg => self.push("- "),
UnaryOp::Not => self.push("not "),
UnaryOp::Len => self.push("#"),
UnaryOp::BitwiseNot => self.push("~ "),
}
self.emit_expr_prec(operand, 21)?;
}
Expression::Call(call) => match call {
Call::Call { func, args, .. } => {
self.emit_expr_prec(func, u8::MAX)?;
self.push("(");
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(arg)?;
}
self.push(")");
}
Call::MethodCall {
obj, method, args, ..
} => {
self.emit_expr_prec(obj, u8::MAX)?;
self.push(":");
self.push(method);
self.push("(");
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.emit_expression(arg)?;
}
self.push(")");
}
},
Expression::Function(func) => {
self.push("function");
self.emit_function_body(func)?;
}
Expression::Table(t) => {
self.push("{");
for (i, field) in t.fields.iter().enumerate() {
if i > 0 {
self.push(", ");
}
match field {
TableField::ExprKey { key, value, .. } => {
self.push("[");
self.emit_expression(key)?;
self.push("] = ");
self.emit_expression(value)?;
}
TableField::NameKey { key, value, .. } => {
self.push(key);
self.push(" = ");
self.emit_expression(value)?;
}
TableField::Positional(val) => {
self.emit_expression(val)?;
}
}
}
self.push("}");
}
}
Ok(())
}
}
fn emit_string_content(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x07' => out.push_str("\\a"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
'\x0B' => out.push_str("\\v"),
c if (c as u32) < 32 => {
out.push('\\');
out.push_str(&(c as u32).to_string());
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emit_options_default() {
let opts = EmitOptions::default();
assert_eq!(opts.indent, " ");
assert_eq!(opts.target, LuaTarget::Lua51);
assert!(!opts.emit_header_comment);
}
#[test]
fn test_emit_empty_block() {
let block = valua_ast::Block {
stmts: vec![],
span: valua_diagnostics::Span::dummy(),
};
let out = LuaEmitter::lua51().emit(&block).unwrap();
assert_eq!(out, "");
}
#[test]
fn test_luajit_target_assumed() {
let emitter = LuaEmitter::luajit();
assert_eq!(emitter.options.target, LuaTarget::LuaJIT);
}
#[test]
fn test_emit_string_content_escapes() {
assert_eq!(emit_string_content("hello"), "hello");
assert_eq!(emit_string_content("a\"b"), "a\\\"b");
assert_eq!(emit_string_content("a\\b"), "a\\\\b");
assert_eq!(emit_string_content("a\nb"), "a\\nb");
}
fn emit_expr(expr: Expression) -> Result<String, CodeGenError> {
let opts = EmitOptions::default();
let mut ctx = EmitContext::new(&opts);
ctx.emit_expression(&expr)?;
Ok(ctx.finish())
}
fn int_expr(v: i64) -> Expression {
Expression::Integer(v, valua_diagnostics::Span::dummy())
}
fn float_expr(v: f64) -> Expression {
Expression::Float(v, valua_diagnostics::Span::dummy())
}
#[test]
fn integer_emitted_as_plain_decimal() {
assert_eq!(emit_expr(int_expr(0)).unwrap(), "0");
assert_eq!(emit_expr(int_expr(42)).unwrap(), "42");
assert_eq!(emit_expr(int_expr(-1)).unwrap(), "-1");
assert_eq!(emit_expr(int_expr(255)).unwrap(), "255");
}
#[test]
fn integer_max_emitted_as_decimal_not_hex() {
let s = emit_expr(int_expr(i64::MAX)).unwrap();
assert!(
!s.contains("0x") && !s.contains("0X"),
"must not emit hex: {s}"
);
assert_eq!(s, i64::MAX.to_string());
}
#[test]
fn float_with_fractional_part_emitted_verbatim() {
assert_eq!(emit_expr(float_expr(1.5)).unwrap(), "1.5");
assert_eq!(emit_expr(float_expr(3.14)).unwrap(), "3.14");
}
#[test]
fn float_without_fractional_part_gets_dot_zero_suffix() {
let s = emit_expr(float_expr(1.0)).unwrap();
assert!(
s.contains('.') || s.contains('e') || s.contains('E'),
"float 1.0 must have fractional marker: {s}"
);
assert_eq!(s, "1.0");
}
#[test]
fn float_every_emitted_value_has_decimal_marker() {
let cases = [0.0_f64, 1.0, -1.0, 42.0, 1e10, 1e15, 1e-10, 1e-300];
for v in cases {
let s = emit_expr(float_expr(v)).unwrap();
assert!(
s.contains('.') || s.contains('e') || s.contains('E'),
"float {v} emitted without decimal marker: {s}"
);
}
}
#[test]
fn float_nan_returns_unsupported_node_error() {
let err = emit_expr(float_expr(f64::NAN)).unwrap_err();
match err {
CodeGenError::UnsupportedNode { ref detail, .. } => {
assert!(detail.contains("NaN"), "error must mention NaN: {detail}");
}
other => panic!("expected UnsupportedNode, got: {other}"),
}
}
#[test]
fn float_positive_infinity_returns_unsupported_node_error() {
let err = emit_expr(float_expr(f64::INFINITY)).unwrap_err();
assert!(matches!(err, CodeGenError::UnsupportedNode { .. }));
}
#[test]
fn float_negative_infinity_returns_unsupported_node_error() {
let err = emit_expr(float_expr(f64::NEG_INFINITY)).unwrap_err();
assert!(matches!(err, CodeGenError::UnsupportedNode { .. }));
}
#[test]
fn float_no_runtime_type_wrapper_in_output() {
let s = emit_expr(float_expr(3.14)).unwrap();
assert!(
!s.contains("math"),
"emitted float must not reference math.*: {s}"
);
assert!(
!s.contains("type"),
"emitted float must not call type(): {s}"
);
assert!(
!s.contains("("),
"emitted float literal must not contain a call: {s}"
);
}
#[test]
fn integer_no_runtime_type_wrapper_in_output() {
let s = emit_expr(int_expr(42)).unwrap();
assert!(
!s.contains("math"),
"emitted integer must not reference math.*: {s}"
);
assert!(
!s.contains("("),
"emitted integer literal must not contain a call: {s}"
);
}
}