From f09f27f553ed1c2ea5aa818f61fdd680cd0d18cb Mon Sep 17 00:00:00 2001 From: luaneko Date: Tue, 17 Jun 2025 11:23:59 +1000 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 55 +++ Cargo.toml | 9 + crates/luaffi/Cargo.toml | 6 + crates/luaffi/src/lib.rs | 14 + crates/luaify/Cargo.toml | 12 + crates/luaify/src/generate.rs | 861 +++++++++++++++++++++++++++++++++ crates/luaify/src/lib.rs | 36 ++ crates/luaify/src/transform.rs | 231 +++++++++ crates/luaify/src/utils.rs | 113 +++++ crates/luaify/tests/test.rs | 372 ++++++++++++++ src/main.rs | 3 + 12 files changed, 1713 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/luaffi/Cargo.toml create mode 100644 crates/luaffi/src/lib.rs create mode 100644 crates/luaify/Cargo.toml create mode 100644 crates/luaify/src/generate.rs create mode 100644 crates/luaify/src/lib.rs create mode 100644 crates/luaify/src/transform.rs create mode 100644 crates/luaify/src/utils.rs create mode 100644 crates/luaify/tests/test.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b79e132 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,55 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "luaffi" +version = "0.1.0" + +[[package]] +name = "luaify" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "luby" +version = "0.1.0" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..495befc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = ["crates/luaffi", "crates/luaify"] + +[package] +name = "luby" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/crates/luaffi/Cargo.toml b/crates/luaffi/Cargo.toml new file mode 100644 index 0000000..49ff931 --- /dev/null +++ b/crates/luaffi/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "luaffi" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/crates/luaffi/src/lib.rs b/crates/luaffi/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crates/luaffi/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/luaify/Cargo.toml b/crates/luaify/Cargo.toml new file mode 100644 index 0000000..47c88c3 --- /dev/null +++ b/crates/luaify/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "luaify" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = { version = "2.0.103", features = ["full", "visit-mut"] } diff --git a/crates/luaify/src/generate.rs b/crates/luaify/src/generate.rs new file mode 100644 index 0000000..18d528b --- /dev/null +++ b/crates/luaify/src/generate.rs @@ -0,0 +1,861 @@ +use crate::utils::{syn_assert, syn_error, wrap_expr_block}; +use std::fmt::Display; +use syn::{ext::*, punctuated::*, spanned::*, *}; + +pub fn generate(expr: &Expr) -> Result { + let mut f = Formatter::default(); + generate_expr(&mut f, expr, Context::expr(true))?; + f.done() +} + +#[derive(Debug, Default)] +struct Formatter { + buf: String, + space: bool, +} + +impl Formatter { + fn write(&mut self, s: impl Display) -> &mut Self { + fn sep(c: char) -> bool { + match c { + '(' | ')' | '[' | ']' | '+' | '-' | '*' | '/' | '%' | '^' | '#' | '=' | '~' + | '<' | '>' | ':' | ';' | '.' | ',' | '\'' | '"' | ' ' => true, + _ => false, + } + } + + let s = format!("{s}"); + if !s.is_empty() { + if self.space && !sep(s.chars().next().unwrap()) { + self.buf.push(' '); + } + self.buf.push_str(&s); + self.space = !sep(s.chars().last().unwrap()); + } + self + } + + fn done(self) -> Result { + Ok(self.buf) + } +} + +macro_rules! assert_no_attrs { + ($src:expr) => {{ syn_assert!($src.attrs.is_empty(), $src, "unsupported attribute") }}; +} + +macro_rules! assert_no_suffix { + ($src:expr) => {{ syn_assert!($src.suffix() == "", $src, "cannot have suffix") }}; +} + +#[derive(Debug, Clone, Copy)] +enum Context { + Stmt { ret: bool }, + Expr { multi: bool }, +} + +impl Context { + fn stmt(ret: bool) -> Self { + Self::Stmt { ret } + } + + fn expr(multi: bool) -> Self { + Self::Expr { multi } + } + + fn is_stmt(&self) -> bool { + matches!(self, Self::Stmt { .. }) + } + + fn is_ret(&self) -> bool { + matches!(self, Self::Stmt { ret: true, .. }) + } + + fn is_expr(&self) -> bool { + matches!(self, Self::Expr { .. }) + } + + fn is_multi_expr(&self) -> bool { + matches!(self, Self::Expr { multi: true, .. }) + } + + fn is_value(&self) -> bool { + self.is_expr() | self.is_ret() + } +} + +fn generate_expr(f: &mut Formatter, expr: &Expr, cx: Context) -> Result<()> { + match expr { + Expr::Assign(ass) => generate_expr_assign(f, ass, cx), + Expr::Binary(bin) => generate_expr_binary(f, bin, cx), + Expr::Block(block) => generate_expr_block(f, block, cx), + Expr::Break(brk) => generate_expr_break(f, brk, cx), + Expr::Call(call) => generate_expr_call(f, call, cx), + Expr::Closure(clo) => generate_expr_closure(f, clo, cx), + Expr::Continue(cont) => generate_expr_continue(f, cont, cx), + Expr::Field(field) => generate_expr_field(f, field, cx), + Expr::ForLoop(fo) => generate_expr_forloop(f, fo, cx), + Expr::If(xif) => generate_expr_if(f, xif, cx), + Expr::Index(index) => generate_expr_index(f, index, cx), + Expr::Infer(infer) => generate_expr_infer(f, infer, cx), + Expr::Lit(lit) => generate_expr_lit(f, lit, cx), + Expr::Loop(lo) => generate_expr_loop(f, lo, cx), + Expr::Macro(mac) => generate_expr_macro(f, mac, cx), + Expr::MethodCall(call) => generate_expr_method_call(f, call, cx), + Expr::Paren(paren) => generate_expr_paren(f, paren, cx), + Expr::Path(path) => generate_expr_path(f, path, cx), + Expr::Return(ret) => generate_expr_return(f, ret, cx), + Expr::Tuple(tuple) => generate_expr_tuple(f, tuple, cx), + Expr::Unary(un) => generate_expr_unary(f, un, cx), + Expr::While(whil) => generate_expr_while(f, whil, cx), + expr => syn_error!(expr, "unsupported expression"), + } +} + +fn generate_expr_assign(f: &mut Formatter, ass: &ExprAssign, cx: Context) -> Result<()> { + assert_no_attrs!(ass); + syn_assert!( + cx.is_stmt(), + ass, + "assignment must be in statement position" + ); + generate_expr(f, &ass.left, Context::expr(false))?; + f.write("="); + generate_expr(f, &ass.right, Context::expr(true))?; + Ok(()) +} + +fn generate_expr_binary(f: &mut Formatter, bin: &ExprBinary, cx: Context) -> Result<()> { + assert_no_attrs!(bin); + match bin.op { + BinOp::Add(_) + | BinOp::Sub(_) + | BinOp::Mul(_) + | BinOp::Div(_) + | BinOp::Rem(_) + | BinOp::BitAnd(_) + | BinOp::BitOr(_) + | BinOp::BitXor(_) + | BinOp::Shl(_) + | BinOp::Shr(_) + | BinOp::Eq(_) + | BinOp::Lt(_) + | BinOp::Le(_) + | BinOp::Ne(_) + | BinOp::Ge(_) + | BinOp::Gt(_) + | BinOp::And(_) + | BinOp::Or(_) => { + syn_assert!(cx.is_value(), bin, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + } + BinOp::AddAssign(_) + | BinOp::SubAssign(_) + | BinOp::MulAssign(_) + | BinOp::DivAssign(_) + | BinOp::RemAssign(_) + | BinOp::BitXorAssign(_) + | BinOp::BitAndAssign(_) + | BinOp::BitOrAssign(_) + | BinOp::ShlAssign(_) + | BinOp::ShrAssign(_) => { + syn_assert!(cx.is_stmt(), bin, "must be in statement position"); + } + op => syn_error!(op, "unsupported binary operator"), + } + + let cx = Context::expr(false); + + macro_rules! bin_op { + ($op:expr) => {{ + generate_expr(f, &bin.left, cx)?; + f.write($op); + generate_expr(f, &bin.right, cx)?; + }}; + } + + macro_rules! call_op { + ($name:expr) => {{ + f.write(format_args!("{}(", $name)); + generate_expr(f, &bin.left, cx)?; + f.write(","); + generate_expr(f, &bin.right, cx)?; + f.write(")"); + }}; + } + + macro_rules! assign_bin_op { + ($op:expr) => {{ + generate_expr(f, &bin.left, cx)?; + f.write("="); + bin_op!($op); + }}; + } + + macro_rules! assign_call_op { + ($name:expr) => {{ + generate_expr(f, &bin.left, cx)?; + f.write("="); + call_op!($name); + }}; + } + + match bin.op { + BinOp::Add(_) => bin_op!("+"), + BinOp::AddAssign(_) => assign_bin_op!("+"), + BinOp::Sub(_) => bin_op!("-"), + BinOp::SubAssign(_) => assign_bin_op!("-"), + BinOp::Mul(_) => bin_op!("*"), + BinOp::MulAssign(_) => assign_bin_op!("*"), + BinOp::Div(_) => bin_op!("/"), + BinOp::DivAssign(_) => assign_bin_op!("/"), + BinOp::Rem(_) => call_op!("math.fmod"), + BinOp::RemAssign(_) => assign_call_op!("math.fmod"), + BinOp::BitAnd(_) => call_op!("bit.band"), + BinOp::BitAndAssign(_) => assign_call_op!("bit.band"), + BinOp::BitOr(_) => call_op!("bit.bor"), + BinOp::BitOrAssign(_) => assign_call_op!("bit.bor"), + BinOp::BitXor(_) => call_op!("bit.bxor"), + BinOp::BitXorAssign(_) => assign_call_op!("bit.bxor"), + BinOp::Shl(_) => call_op!("bit.lshift"), + BinOp::ShlAssign(_) => assign_call_op!("bit.lshift"), + BinOp::Shr(_) => call_op!("bit.arshift"), + BinOp::ShrAssign(_) => assign_call_op!("bit.arshift"), + BinOp::Eq(_) => bin_op!("=="), + BinOp::Lt(_) => bin_op!("<"), + BinOp::Le(_) => bin_op!("<="), + BinOp::Ne(_) => bin_op!("~="), + BinOp::Ge(_) => bin_op!(">="), + BinOp::Gt(_) => bin_op!(">"), + BinOp::And(_) => bin_op!("and"), + BinOp::Or(_) => bin_op!("or"), + op => syn_error!(op, "unsupported binary operator"), + }; + + Ok(()) +} + +fn generate_expr_block(f: &mut Formatter, block: &ExprBlock, cx: Context) -> Result<()> { + assert_no_attrs!(block); + syn_assert!(cx.is_stmt(), block, "block must be in statement position"); + f.write("do"); + generate_block_body(f, &block.block, cx)?; + if let Some(ref label) = block.label { + generate_label_continue(f, label)?; + } + f.write("end"); + if let Some(ref label) = block.label { + generate_label_break(f, label)?; + } + Ok(()) +} + +fn generate_expr_break(f: &mut Formatter, brk: &ExprBreak, cx: Context) -> Result<()> { + assert_no_attrs!(brk); + syn_assert!(cx.is_stmt(), brk, "break must be in statement position"); + syn_assert!(brk.expr.is_none(), brk, "use return instead"); + match brk.label { + Some(ref label) => f.write(format_args!("goto {}_brk", label.ident.unraw())), + None => f.write("break"), + }; + Ok(()) +} + +fn generate_expr_call(f: &mut Formatter, call: &ExprCall, cx: Context) -> Result<()> { + assert_no_attrs!(call); + cx.is_ret().then(|| f.write("return")); + generate_expr(f, &call.func, Context::expr(false))?; + f.write("("); + generate_punctuated_expr(f, &call.args)?; + f.write(")"); + Ok(()) +} + +fn generate_expr_closure(f: &mut Formatter, clo: &ExprClosure, cx: Context) -> Result<()> { + assert_no_attrs!(clo); + syn_assert!(cx.is_value(), clo, "closure must be in expression position"); + + if let Some(ref bounds) = clo.lifetimes { + syn_error!(bounds, "cannot have lifetime bindings"); + } else if let Some(ref stat) = clo.movability { + syn_error!(stat, "cannot be static"); + } else if let Some(ref cons) = clo.constness { + syn_error!(cons, "cannot be const"); + } else if let Some(ref asyn) = clo.asyncness { + syn_error!(asyn, "cannot be async"); + } else if let Some(ref capt) = clo.capture { + syn_error!(capt, "cannot be move"); + } else if let ReturnType::Type(_, ref ty) = clo.output { + syn_error!(ty, "cannot have return type"); + } + + cx.is_ret().then(|| f.write("return")); + f.write("function("); + generate_punctuated_pat(f, &clo.inputs)?; + f.write(")"); + generate_block_body(f, &wrap_expr_block(&clo.body), Context::stmt(true))?; + f.write("end"); + Ok(()) +} + +fn generate_expr_continue(f: &mut Formatter, cont: &ExprContinue, cx: Context) -> Result<()> { + assert_no_attrs!(cont); + syn_assert!(cx.is_stmt(), cont, "continue must be in statement position"); + match cont.label { + Some(ref label) => f.write(format_args!("goto {}_cnt", label.ident.unraw())), + None => syn_error!(cont, "continue requires a label"), + }; + Ok(()) +} + +fn generate_expr_field(f: &mut Formatter, field: &ExprField, cx: Context) -> Result<()> { + assert_no_attrs!(field); + syn_assert!(cx.is_value(), field, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + generate_expr(f, &field.base, Context::expr(false))?; + match field.member { + Member::Named(ref ident) => { + f.write("."); + generate_ident(f, ident)?; + } + Member::Unnamed(ref index) => { + f.write("["); + generate_index(f, index)?; + f.write("]"); + } + } + Ok(()) +} + +fn generate_expr_forloop(f: &mut Formatter, fo: &ExprForLoop, cx: Context) -> Result<()> { + assert_no_attrs!(fo); + syn_assert!(cx.is_stmt(), fo, "for loop must be in statement position"); + f.write("for"); + match *fo.expr { + Expr::Range(ref range) => { + assert_no_attrs!(range); + // TODO: reverse ranges + let start = match range.start { + Some(ref start) => (**start).clone(), + None => parse_quote!(0), + }; + let end = match range.end { + Some(ref end) => match range.limits { + RangeLimits::HalfOpen(_) => parse_quote!(#end - 1), + RangeLimits::Closed(_) => (**end).clone(), + }, + None => syn_error!(range, "end of range must be specified"), + }; + generate_pat(f, &fo.pat, PatContext::Single)?; + f.write("="); + generate_expr(f, &start, Context::expr(false))?; + f.write(","); + generate_expr(f, &end, Context::expr(false))?; + } + ref expr => { + generate_pat(f, &fo.pat, PatContext::Multi)?; + f.write("in"); + generate_expr(f, expr, Context::expr(true))?; + } + } + f.write("do"); + generate_block_body(f, &fo.body, Context::stmt(false))?; + if let Some(ref label) = fo.label { + generate_label_continue(f, label)?; + } + f.write("end"); + if let Some(ref label) = fo.label { + generate_label_break(f, label)?; + } + Ok(()) +} + +fn generate_expr_if(f: &mut Formatter, mut xif: &ExprIf, cx: Context) -> Result<()> { + assert_no_attrs!(xif); + syn_assert!(cx.is_stmt(), xif, "if must be in statement position"); + f.write("if"); + loop { + generate_expr(f, &xif.cond, Context::expr(false))?; + f.write("then"); + generate_block_body(f, &xif.then_branch, cx)?; + if let Some((_, ref expr)) = xif.else_branch { + match **expr { + Expr::If(ref elseif) => { + f.write("elseif"); + xif = elseif; + continue; + } + ref els => { + f.write("else"); + generate_block_body(f, &wrap_expr_block(els), cx)?; + } + } + } + break; + } + f.write("end"); + Ok(()) +} + +fn generate_expr_index(f: &mut Formatter, index: &ExprIndex, cx: Context) -> Result<()> { + assert_no_attrs!(index); + syn_assert!(cx.is_value(), index, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + generate_expr(f, &index.expr, Context::expr(false))?; + f.write("["); + generate_expr(f, &index.index, Context::expr(false))?; + f.write("]"); + Ok(()) +} + +fn generate_expr_infer(f: &mut Formatter, infer: &ExprInfer, cx: Context) -> Result<()> { + assert_no_attrs!(infer); + syn_assert!(cx.is_value(), infer, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + f.write("_"); + Ok(()) +} + +fn generate_expr_lit(f: &mut Formatter, lit: &ExprLit, cx: Context) -> Result<()> { + assert_no_attrs!(lit); + syn_assert!(cx.is_value(), lit, "literal must be in expression position"); + cx.is_ret().then(|| f.write("return")); + generate_lit(f, &lit.lit) +} + +fn generate_expr_loop(f: &mut Formatter, lo: &ExprLoop, cx: Context) -> Result<()> { + assert_no_attrs!(lo); + syn_assert!(cx.is_stmt(), lo, "loop must be in statement position"); + f.write("while true do"); + generate_block_body(f, &lo.body, Context::stmt(false))?; + if let Some(ref label) = lo.label { + generate_label_continue(f, label)?; + } + f.write("end"); + if let Some(ref label) = lo.label { + generate_label_break(f, label)?; + } + Ok(()) +} + +fn generate_expr_macro(f: &mut Formatter, mac: &ExprMacro, cx: Context) -> Result<()> { + assert_no_attrs!(mac); + generate_macro(f, &mac.mac, cx) +} + +fn generate_expr_method_call(f: &mut Formatter, call: &ExprMethodCall, cx: Context) -> Result<()> { + assert_no_attrs!(call); + + if let Some(ref fish) = call.turbofish { + syn_error!(fish, "cannot be generic"); + } + + cx.is_ret().then(|| f.write("return")); + generate_expr(f, &call.receiver, Context::expr(false))?; + f.write(":"); + generate_ident(f, &call.method)?; + f.write("("); + generate_punctuated_expr(f, &call.args)?; + f.write(")"); + Ok(()) +} + +fn generate_expr_paren(f: &mut Formatter, paren: &ExprParen, cx: Context) -> Result<()> { + assert_no_attrs!(paren); + syn_assert!(cx.is_value(), paren, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + f.write("("); + generate_expr(f, &paren.expr, Context::expr(false))?; + f.write(")"); + Ok(()) +} + +fn generate_expr_path(f: &mut Formatter, path: &ExprPath, cx: Context) -> Result<()> { + assert_no_attrs!(path); + syn_assert!(cx.is_value(), path, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + match path.qself { + Some(ref qself) => syn_error!(qself, "cannot be generic"), + None => generate_path(f, &path.path), + } +} + +fn generate_expr_return(f: &mut Formatter, ret: &ExprReturn, cx: Context) -> Result<()> { + assert_no_attrs!(ret); + syn_assert!(cx.is_stmt(), ret, "return must be in statement position"); + f.write("return"); + match ret.expr { + Some(ref value) => generate_expr(f, value, Context::expr(true)), + None => Ok(()), + } +} + +fn generate_expr_tuple(f: &mut Formatter, tuple: &ExprTuple, cx: Context) -> Result<()> { + assert_no_attrs!(tuple); + syn_assert!(cx.is_value(), tuple, "tuple must be in expression position"); + cx.is_ret().then(|| f.write("return")); + match tuple.elems.len() { + 0 => { + f.write("nil"); + Ok(()) + } + _ if cx.is_multi_expr() => generate_punctuated_expr(f, &tuple.elems), + _ => syn_error!(tuple, "expected single-valued expression"), + } +} + +fn generate_expr_unary(f: &mut Formatter, un: &ExprUnary, cx: Context) -> Result<()> { + assert_no_attrs!(un); + syn_assert!(cx.is_value(), un, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + + let cx = Context::expr(false); + + macro_rules! un_op { + ($op:expr) => {{ + f.write($op); + generate_expr(f, &un.expr, cx)?; + }}; + } + + match un.op { + UnOp::Not(_) => un_op!("not"), + UnOp::Neg(_) => un_op!("-"), + op => syn_error!(op, "unsupported unary operator"), + } + + Ok(()) +} + +fn generate_expr_while(f: &mut Formatter, whil: &ExprWhile, cx: Context) -> Result<()> { + assert_no_attrs!(whil); + syn_assert!(cx.is_stmt(), whil, "while must be in statement position"); + f.write("while"); + generate_expr(f, &whil.cond, Context::expr(false))?; + f.write("do"); + generate_block_body(f, &whil.body, Context::stmt(false))?; + if let Some(ref label) = whil.label { + generate_label_continue(f, label)?; + } + f.write("end"); + if let Some(ref label) = whil.label { + generate_label_break(f, label)?; + } + Ok(()) +} + +fn generate_punctuated_expr(f: &mut Formatter, exprs: &Punctuated) -> Result<()> { + for (i, expr) in exprs.iter().enumerate() { + (i != 0).then(|| f.write(",")); + generate_expr(f, expr, Context::expr(false))?; + } + Ok(()) +} + +fn generate_ident(f: &mut Formatter, ident: &Ident) -> Result<()> { + // https://www.lua.org/manual/5.2/manual.html#3.1 + let name = format!("{}", ident.unraw()); + match name.as_str() { + "and" | "break" | "do" | "else" | "elseif" | "end" | "false" | "for" | "function" + | "goto" | "if" | "in" | "local" | "nil" | "not" | "or" | "repeat" | "return" | "then" + | "true" | "until" | "while" => { + syn_error!(ident, "'{name}' cannot be used as an identifier") + } + s => f.write(s), + }; + Ok(()) +} + +fn generate_index(f: &mut Formatter, index: &Index) -> Result<()> { + f.write(index.index); + Ok(()) +} + +fn generate_label_continue(f: &mut Formatter, label: &Label) -> Result<()> { + f.write(format_args!("::{}_cnt::", label.name.ident.unraw())); + Ok(()) +} + +fn generate_label_break(f: &mut Formatter, label: &Label) -> Result<()> { + f.write(format_args!("::{}_brk::", label.name.ident.unraw())); + Ok(()) +} + +fn generate_lit(f: &mut Formatter, lit: &Lit) -> Result<()> { + match lit { + Lit::Bool(b) => generate_lit_bool(f, b), + Lit::Byte(b) => generate_lit_byte(f, b), + Lit::Int(n) => generate_lit_int(f, n), + Lit::Float(n) => generate_lit_float(f, n), + Lit::Str(s) => generate_lit_str(f, s), + Lit::ByteStr(s) => generate_lit_byte_str(f, s), + Lit::CStr(s) => generate_lit_cstr(f, s), + lit => syn_error!(lit, "unsupported literal"), + } +} + +fn generate_lit_bool(f: &mut Formatter, b: &LitBool) -> Result<()> { + f.write(b.value()); + Ok(()) +} + +fn generate_lit_byte(f: &mut Formatter, b: &LitByte) -> Result<()> { + assert_no_suffix!(b); + f.write(b.value()); + Ok(()) +} + +fn generate_lit_int(f: &mut Formatter, n: &LitInt) -> Result<()> { + assert_no_suffix!(n); + f.write(n.base10_parse::()?); + Ok(()) +} + +fn generate_lit_float(f: &mut Formatter, n: &LitFloat) -> Result<()> { + assert_no_suffix!(n); + f.write(n.base10_parse::()?); + Ok(()) +} + +fn generate_lit_str(f: &mut Formatter, s: &LitStr) -> Result<()> { + assert_no_suffix!(s); + f.write(format!(r#""{}""#, escape_str(s.value()))); + Ok(()) +} + +fn generate_lit_byte_str(f: &mut Formatter, s: &LitByteStr) -> Result<()> { + assert_no_suffix!(s); + f.write(format!(r#""{}""#, escape_str(s.value()))); + Ok(()) +} + +fn generate_lit_cstr(f: &mut Formatter, s: &LitCStr) -> Result<()> { + assert_no_suffix!(s); + f.write(format!(r#""{}\0""#, escape_str(s.value().as_bytes()))); + Ok(()) +} + +fn escape_str(s: impl AsRef<[u8]>) -> String { + // this produces an escaped string with \xNN hexadecimal notation which lua 5.1 normally + // wouldn't understand, but luajit supports this unconditionally as an extension + // https://docs.rs/bstr/latest/bstr/trait.ByteSlice.html#method.escape_bytes + // https://luajit.org/extensions.html#lua52 + String::from_utf8( + s.as_ref() + .iter() + .flat_map(|b| std::ascii::escape_default(*b)) + .collect(), + ) + .unwrap() +} + +fn generate_path(f: &mut Formatter, path: &Path) -> Result<()> { + for (i, segment) in path.segments.iter().enumerate() { + (i != 0).then(|| f.write(".")); + generate_path_segment(f, segment)?; + } + Ok(()) +} + +fn generate_path_segment(f: &mut Formatter, seg: &PathSegment) -> Result<()> { + match seg.arguments { + PathArguments::AngleBracketed(ref arg) => syn_error!(arg, "cannot be generic"), + PathArguments::Parenthesized(ref arg) => syn_error!(arg, "cannot be generic"), + PathArguments::None => generate_ident(f, &seg.ident), + } +} + +fn generate_block_body(f: &mut Formatter, block: &Block, cx: Context) -> Result<()> { + let len = block.stmts.len(); + for (i, stmt) in block.stmts.iter().enumerate() { + match stmt { + Stmt::Local(local) => generate_local(f, local), + Stmt::Item(item) => generate_item(f, item), + Stmt::Expr(expr, semi) => match semi { + None if i == len - 1 && cx.is_ret() => generate_expr(f, expr, Context::stmt(true)), + _ => generate_expr(f, expr, Context::stmt(false)), + }, + Stmt::Macro(smac) => generate_macro(f, &smac.mac, cx), + }?; + f.write(";"); + } + Ok(()) +} + +fn generate_local(f: &mut Formatter, local: &Local) -> Result<()> { + assert_no_attrs!(local); + f.write("local"); + generate_pat(f, &local.pat, PatContext::Multi)?; + match local.init { + Some(ref init) => generate_local_init(f, init), + None => Ok(()), + } +} + +fn generate_local_init(f: &mut Formatter, init: &LocalInit) -> Result<()> { + match init.diverge { + Some((ref token, _)) => syn_error!(token, "let-else is not supported"), + None => { + f.write("="); + generate_expr(f, &init.expr, Context::expr(true)) + } + } +} + +fn generate_item(f: &mut Formatter, item: &Item) -> Result<()> { + match item { + Item::Fn(func) => generate_item_fn(f, func), + item => syn_error!(item, "unsupported item"), + } +} + +fn generate_item_fn(f: &mut Formatter, func: &ItemFn) -> Result<()> { + assert_no_attrs!(func); + let Visibility::Inherited = func.vis else { + syn_error!(func, "cannot have visibility"); + }; + f.write("local"); + generate_signature(f, &func.sig)?; + generate_block_body(f, &func.block, Context::stmt(true))?; + f.write("end"); + Ok(()) +} + +fn generate_signature(f: &mut Formatter, sig: &Signature) -> Result<()> { + if let Some(ref cons) = sig.constness { + syn_error!(cons, "cannot be const"); + } else if let Some(ref asyn) = sig.asyncness { + syn_error!(asyn, "cannot be async"); + } else if let Some(ref uns) = sig.unsafety { + syn_error!(uns, "cannot be unsafe"); + } else if let Some(ref abi) = sig.abi { + syn_error!(abi, "cannot be extern"); + } else if !sig.generics.params.is_empty() { + syn_error!(sig.generics.params, "cannot be generic"); + } else if let Some(ref wher) = sig.generics.where_clause { + syn_error!(wher, "cannot have where clause"); + } else if let ReturnType::Type(_, ref ty) = sig.output { + syn_error!(ty, "cannot have return type"); + } + + f.write("function"); + generate_ident(f, &sig.ident)?; + f.write("("); + for (i, param) in sig.inputs.iter().enumerate() { + (i != 0).then(|| f.write(",")); + generate_fn_arg(f, param)?; + } + f.write(")"); + Ok(()) +} + +fn generate_fn_arg(f: &mut Formatter, param: &FnArg) -> Result<()> { + match param { + FnArg::Receiver(recv) => generate_receiver(f, recv), + FnArg::Typed(typed) => generate_pat_typed(f, typed, PatContext::Single), + } +} + +fn generate_receiver(f: &mut Formatter, recv: &Receiver) -> Result<()> { + assert_no_attrs!(recv); + syn_assert!(recv.colon_token.is_none(), recv, "must be `self`"); + if let Some(ref l) = recv.lifetime() { + syn_error!(l, "cannot have lifetimes"); + } else if let Some(ref m) = recv.mutability { + syn_error!(m, "cannot be mut (implicitly mutable)"); + } + f.write("self"); + Ok(()) +} + +fn generate_macro(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> { + match format!("{}", mac.path.require_ident()?).as_str() { + "concat" => generate_macro_concat(f, mac, cx), + name => syn_error!(mac.path, "unknown macro '{name}'"), + } +} + +fn generate_macro_concat(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> { + syn_assert!(cx.is_value(), mac, "must be in expression position"); + cx.is_ret().then(|| f.write("return")); + let args = mac.parse_body_with(>::parse_terminated)?; + if args.is_empty() { + syn_error!(mac.path, "expected at least one argument") + } + for (i, arg) in args.iter().enumerate() { + (i != 0).then(|| f.write("..")); + generate_expr(f, arg, Context::expr(false))?; + } + Ok(()) +} + +#[derive(Debug, Clone, Copy)] +enum PatContext { + Single, + Multi, +} + +impl PatContext { + fn _is_single(&self) -> bool { + matches!(self, Self::Single) + } + + fn is_multi(&self) -> bool { + matches!(self, Self::Multi) + } +} + +fn generate_pat(f: &mut Formatter, pat: &Pat, cx: PatContext) -> Result<()> { + match pat { + Pat::Ident(ident) => generate_pat_ident(f, ident, cx), + Pat::Tuple(tuple) => generate_pat_tuple(f, tuple, cx), + Pat::Type(typed) => generate_pat_typed(f, typed, cx), + Pat::Wild(wild) => generate_pat_wild(f, wild, cx), + pat => syn_error!(pat, "unsupported pattern"), + } +} + +fn generate_pat_ident(f: &mut Formatter, ident: &PatIdent, _cx: PatContext) -> Result<()> { + assert_no_attrs!(ident); + + if let Some(ref r) = ident.by_ref { + syn_error!(r, "cannot be ref"); + } else if let Some(ref m) = ident.mutability { + syn_error!(m, "cannot be mut (implicitly mutable)"); + } else if let Some((_, ref pat)) = ident.subpat { + syn_error!(pat, "subpatterns are not supported"); + } + + generate_ident(f, &ident.ident) +} + +fn generate_pat_tuple(f: &mut Formatter, tuple: &PatTuple, cx: PatContext) -> Result<()> { + assert_no_attrs!(tuple); + match tuple.elems.len() { + 0 => syn_error!(tuple, "must have at least one element"), + _ if cx.is_multi() => generate_punctuated_pat(f, &tuple.elems), + _ => syn_error!(tuple, "expected single-valued pattern"), + } +} + +fn generate_pat_typed(f: &mut Formatter, typed: &PatType, cx: PatContext) -> Result<()> { + assert_no_attrs!(typed); + match *typed.ty { + Type::Infer(_) => generate_pat(f, &typed.pat, cx), + ref ty => syn_error!(ty, "cannot have type"), + } +} + +fn generate_pat_wild(f: &mut Formatter, wild: &PatWild, _cx: PatContext) -> Result<()> { + assert_no_attrs!(wild); + f.write("_"); + Ok(()) +} + +fn generate_punctuated_pat(f: &mut Formatter, pats: &Punctuated) -> Result<()> { + for (i, pat) in pats.iter().enumerate() { + (i != 0).then(|| f.write(",")); + generate_pat(f, pat, PatContext::Single)?; + } + Ok(()) +} diff --git a/crates/luaify/src/lib.rs b/crates/luaify/src/lib.rs new file mode 100644 index 0000000..ccbbd69 --- /dev/null +++ b/crates/luaify/src/lib.rs @@ -0,0 +1,36 @@ +use crate::{generate::generate, transform::transform}; +use proc_macro::TokenStream as TokenStream1; +use quote::{ToTokens, quote}; +use syn::parse_macro_input; + +mod generate; +mod transform; +mod utils; + +#[proc_macro] +pub fn luaify(input: TokenStream1) -> TokenStream1 { + let mut expr = parse_macro_input!(input); + match transform(&mut expr).and_then(|()| generate(&expr)) { + Ok(s) => quote!(#s).into_token_stream(), + Err(err) => err.into_compile_error().into_token_stream(), + } + .into() +} + +// #[derive(Debug)] +// struct TypeChecker { +// checks: Vec<(Ident, LuaType)>, +// } + +// impl TypeChecker { +// fn generate(&self, f: &mut Formatter) -> syn::Result<()> { +// for (ident, ty) in self.checks.iter() { +// generate_type_check(f, ident, *ty)?; +// } +// Ok(()) +// } +// } + +// fn generate_type_check(f: &mut Formatter, ident: &Ident, ty: LuaType) -> syn::Result<()> { +// Ok(()) +// } diff --git a/crates/luaify/src/transform.rs b/crates/luaify/src/transform.rs new file mode 100644 index 0000000..4e4b3cc --- /dev/null +++ b/crates/luaify/src/transform.rs @@ -0,0 +1,231 @@ +use std::mem; + +use crate::utils::{LuaType, syn_error, unwrap_expr_ident, unwrap_pat_ident, wrap_expr_block}; +use syn::{spanned::*, visit_mut::*, *}; + +pub fn transform(expr: &mut Expr) -> Result<()> { + let mut visitor = Visitor::new(); + visitor.visit_expr_mut(expr); + visitor.result +} + +#[derive(Debug)] +struct Visitor { + result: Result<()>, +} + +impl Visitor { + fn new() -> Self { + Self { result: Ok(()) } + } +} + +impl VisitMut for Visitor { + fn visit_expr_closure_mut(&mut self, clo: &mut ExprClosure) { + match self.transform_expr_closure(clo) { + res @ Err(_) => self.result = res, + _ => visit_expr_closure_mut(self, clo), + } + } + + fn visit_item_fn_mut(&mut self, func: &mut ItemFn) { + match self.transform_function(func) { + res @ Err(_) => self.result = res, + _ => visit_item_fn_mut(self, func), + } + } + + fn visit_expr_mut(&mut self, expr: &mut Expr) { + match self.transform_expr(expr) { + res @ Err(_) => self.result = res, + _ => visit_expr_mut(self, expr), + } + } + + fn visit_expr_unary_mut(&mut self, un: &mut ExprUnary) { + match self.transform_unary(un) { + res @ Err(_) => self.result = res, + _ => visit_expr_unary_mut(self, un), + } + } + + fn visit_expr_match_mut(&mut self, mat: &mut ExprMatch) { + match self.transform_match(mat) { + res @ Err(_) => self.result = res, + _ => visit_expr_match_mut(self, mat), + } + } +} + +impl Visitor { + fn transform_expr_closure(&mut self, clo: &mut ExprClosure) -> Result<()> { + // + // transforms a closure expression with input type annotations by removing the annotations + // and inserting `as` casts at the start. + // + // before: + // |a: string, b: number| { ... } + // after: + // |a, b| { a as string; b as number; ... } + // + let mut checks: Vec = vec![]; + for input in clo.inputs.iter_mut() { + match input { + Pat::Ident(_) => {} + Pat::Type(typed) => { + let ident = unwrap_pat_ident(&typed.pat)?; + let ty = mem::replace(&mut typed.ty, parse_quote!(_)); + match (&*ty).try_into()? { + LuaType::Any => {} + _ => checks.push(parse_quote! { #ident as #ty; }), + } + } + _ => {} + } + } + + if !checks.is_empty() { + let mut body = wrap_expr_block(&clo.body); + body.stmts.splice(..0, checks); + clo.body = Box::new(parse_quote! { #body }); + } + + Ok(()) + } + + fn transform_function(&mut self, func: &mut ItemFn) -> Result<()> { + // + // transforms a function item with input type annotations by removing the annotations + // and inserting `as` casts at the start. + // + // before: + // fn my_func(self: table, a: string) { ... } + // after: + // fn my_func(self: _, a: _) { self as table; a as string; ... } + // + let mut checks: Vec = vec![]; + for input in func.sig.inputs.iter_mut() { + if let Some((ident, ty)) = match input { + FnArg::Receiver(recv) if recv.colon_token.is_some() => { + let ty = mem::replace(&mut recv.ty, parse_quote!(_)); + recv.colon_token = None; + Some((Ident::new("self", recv.self_token.span()), ty)) + } + FnArg::Typed(typed) => { + let ident = unwrap_pat_ident(&typed.pat)?; + let ty = mem::replace(&mut typed.ty, parse_quote!(_)); + Some((ident, ty)) + } + _ => None, + } { + match (&*ty).try_into()? { + LuaType::Any => {} + _ => checks.push(parse_quote! { #ident as #ty; }), + } + }; + } + + func.block.stmts.splice(..0, checks); + Ok(()) + } + + fn transform_expr(&mut self, expr: &mut Expr) -> Result<()> { + self.transform_expr_cast(expr)?; + Ok(()) + } + + fn transform_expr_cast(&mut self, expr: &mut Expr) -> Result<()> { + // + // transforms an `as` cast expression into a block expression containing a runtime + // lua type check. + // + // before: + // var as string + // after: + // { if type(var) != "string" { error(...) } } + // + if let Expr::Cast(cast) = expr { + let arg = (*cast.expr).clone(); + let mut init: Option = None; + let ty: LuaType = (&*cast.ty).try_into()?; + let ty_str = format!("{ty}"); + let (ident, msg) = match unwrap_expr_ident(&arg).ok() { + Some(ident) => (ident.clone(), format!("{ty} expected in '{ident}', got ")), + None => { + let ident = Ident::new("_", arg.span()); + init = Some(parse_quote! { let #ident = #arg; }); + (ident, format!("{ty} expected, got ")) + } + }; + + let tmp = Ident::new(&format!("_{ident}"), ident.span()); + let span = cast.span(); + *expr = match ty { + LuaType::Any => parse_quote_spanned!(span => {}), + LuaType::Nil => parse_quote_spanned!(span => { + #init + if #ident != () { + return error(concat!(#msg, r#type(#ident))); + } + }), + LuaType::Number => parse_quote_spanned!(span => { + #init + let #tmp = #ident; + #ident = tonumber(#ident); + if #ident == () { + return error(concat!(#msg, r#type(#tmp))); + } + }), + LuaType::Integer => parse_quote_spanned!(span => { + #init + let #tmp = #ident; + #ident = tonumber(#ident); + if #ident == () || math::floor(#ident) != #ident { + return error(concat!(#msg, r#type(#tmp))); + } + }), + LuaType::String => parse_quote_spanned!(span => { + #init + if r#type(#ident) == "number" { + #ident = tostring(#ident); + } else if r#type(#ident) != "string" { + return error(concat!(#msg, r#type(#ident))); + } + }), + _ => parse_quote_spanned!(span => { + #init + if r#type(#ident) != #ty_str { + return error(concat!(#msg, r#type(#ident))); + } + }), + } + } + + Ok(()) + } + + fn transform_unary(&mut self, un: &mut ExprUnary) -> Result<()> { + // + // separates a nested negation unary operator with parentheses, because double hyphen + // `--` indicates a comment in lua. + // + // before: + // --a + // after: + // -(-a) + // + if let UnOp::Neg(_) = un.op + && let Expr::Unary(ref inner) = *un.expr + && let UnOp::Neg(_) = inner.op + { + un.expr = Box::new(parse_quote!((#inner))); + } + + Ok(()) + } + + fn transform_match(&mut self, mat: &mut ExprMatch) -> Result<()> { + // TODO: + syn_error!(mat, "match-to-if transformation not yet implemented"); + } +} diff --git a/crates/luaify/src/utils.rs b/crates/luaify/src/utils.rs new file mode 100644 index 0000000..bd0a422 --- /dev/null +++ b/crates/luaify/src/utils.rs @@ -0,0 +1,113 @@ +use std::fmt; +use syn::{ext::*, spanned::*, *}; + +macro_rules! syn_error { + ($src:expr, $($fmt:expr),+) => {{ + return Err(syn::Error::new($src.span(), format!($($fmt),*))); + }}; +} + +macro_rules! syn_assert { + ($cond:expr, $src:expr, $($fmt:expr),+) => {{ + if !$cond { + syn_error!($src, $($fmt),+); + } + }}; +} + +pub(crate) use {syn_assert, syn_error}; + +pub fn wrap_expr_block(expr: &Expr) -> Block { + // return the expr if it's a block, otherwise wrap it in a block + match expr { + Expr::Block(block) if block.label.is_none() => block.block.clone(), + expr => parse_quote!({ #expr }), + } +} + +pub fn unwrap_expr_ident(expr: &Expr) -> Result<&Ident> { + match expr { + Expr::Path(path) => path.path.require_ident(), + _ => syn_error!(expr, "expected ident"), + } +} + +pub fn unwrap_pat_ident(pat: &Pat) -> Result { + Ok(match pat { + Pat::Ident(ident) => match ident.subpat { + Some((_, ref subpat)) => syn_error!(subpat, "unexpected subpattern"), + None => ident.ident.clone(), + }, + Pat::Wild(wild) => Ident::new("_", wild.span()), + _ => syn_error!(pat, "expected ident"), + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LuaType { + Any, + Nil, + Boolean, + Lightuserdata, + Number, + Integer, + String, + Table, + Function, + Userdata, + Thread, + Cdata, +} + +impl fmt::Display for LuaType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LuaType::Any => write!(f, "any"), + LuaType::Nil => write!(f, "nil"), + LuaType::Boolean => write!(f, "boolean"), + LuaType::Lightuserdata => write!(f, "lightuserdata"), + LuaType::Number => write!(f, "number"), + LuaType::Integer => write!(f, "integer"), + LuaType::String => write!(f, "string"), + LuaType::Table => write!(f, "table"), + LuaType::Function => write!(f, "function"), + LuaType::Userdata => write!(f, "userdata"), + LuaType::Thread => write!(f, "thread"), + LuaType::Cdata => write!(f, "cdata"), + } + } +} + +impl TryFrom<&Ident> for LuaType { + type Error = Error; + + fn try_from(value: &Ident) -> Result { + Ok(match format!("{}", value.unraw()).as_str() { + "any" => Self::Any, + "nil" => Self::Nil, + "boolean" => Self::Boolean, + "lightuserdata" => Self::Lightuserdata, + "number" => Self::Number, + "integer" => Self::Integer, + "string" => Self::String, + "table" => Self::Table, + "function" => Self::Function, + "userdata" => Self::Userdata, + "thread" => Self::Thread, + "cdata" => Self::Cdata, + _ => syn_error!(value, "invalid lua type"), + }) + } +} + +impl TryFrom<&Type> for LuaType { + type Error = Error; + + fn try_from(value: &Type) -> Result { + match value { + Type::Infer(_) => Ok(Self::Any), + Type::Path(path) if path.qself.is_none() => path.path.require_ident()?.try_into(), + _ => syn_error!(value, "invalid lua type"), + } + } +} diff --git a/crates/luaify/tests/test.rs b/crates/luaify/tests/test.rs new file mode 100644 index 0000000..58a37e9 --- /dev/null +++ b/crates/luaify/tests/test.rs @@ -0,0 +1,372 @@ +use luaify::luaify; + +#[test] +fn raw_ident() { + assert_eq!(luaify!(r#ref), r#"ref"#); + assert_eq!(luaify!(x.r#ref), r#"x.ref"#); + assert_eq!(luaify!(r#mut::r#ref), r#"mut.ref"#); + assert_eq!(luaify!(x.r#ref()), r#"x:ref()"#); + assert_eq!(luaify!(r#mut.r#ref()), r#"mut:ref()"#); + assert_eq!(luaify!(r#mut::r#ref()), r#"mut.ref()"#); +} + +#[test] +fn indexing() { + assert_eq!(luaify!(table.0), r#"table[0]"#); + assert_eq!(luaify!(table[0]), r#"table[0]"#); + assert_eq!(luaify!(table["0"]), r#"table["0"]"#); + assert_eq!(luaify!(table.field), r#"table.field"#); + assert_eq!(luaify!(table.field.nested), r#"table.field.nested"#); + assert_eq!(luaify!(table::field.nested), r#"table.field.nested"#); + assert_eq!(luaify!(table::field::nested), r#"table.field.nested"#); + assert_eq!(luaify!(table::field["nested"]), r#"table.field["nested"]"#); + assert_eq!(luaify!(table.0.nested), r#"table[0].nested"#); + assert_eq!(luaify!(table.0["nested"]), r#"table[0]["nested"]"#); + assert_eq!(luaify!(table[0].nested), r#"table[0].nested"#); + assert_eq!(luaify!(table[0]["nested"]), r#"table[0]["nested"]"#); + assert_eq!(luaify!(table["field"].nested), r#"table["field"].nested"#); + + assert_eq!(luaify!(table[field]), r#"table[field]"#); + assert_eq!(luaify!(table[x.y]), r#"table[x.y]"#); + assert_eq!(luaify!(table[x.1.y.2]), r#"table[x[1].y[2]]"#); + assert_eq!(luaify!(table[x.1.y[2]]), r#"table[x[1].y[2]]"#); + assert_eq!(luaify!(table[x[1].y.2]), r#"table[x[1].y[2]]"#); + assert_eq!(luaify!(table[x[1].y[2]]), r#"table[x[1].y[2]]"#); + + assert_eq!(luaify!((x.y)[z.w]), r#"(x.y)[z.w]"#); + assert_eq!(luaify!((x[y])[z.w]), r#"(x[y])[z.w]"#); + assert_eq!(luaify!((x["y"])[z.w]), r#"(x["y"])[z.w]"#); +} + +#[test] +fn locals() { + assert_eq!( + luaify!(|| { + let x = 30; + }), + r#"function()local x=30;end"# + ); + assert_eq!( + luaify!(|| { + let (x, y, z) = (1, 2, 3); + }), + r#"function()local x,y,z=1,2,3;end"# + ); + assert_eq!( + luaify!(|| { + let (a, b, c) = (1, call(), call2(), 4); + return (a, b, c); + }), + r#"function()local a,b,c=1,call(),call2(),4;return a,b,c;end"# + ); +} + +#[test] +fn local_fn() { + assert_eq!( + luaify!(|a, b| { + fn inner(c: _, d: _) {} + inner + }), + r#"function(a,b)local function inner(c,d)end;return inner;end"# + ); + assert_eq!( + luaify!(|| { + fn check(self: string, arg: number) {} + inner + }), + r#"function()local function check(self,arg)do if type(self)=="number"then self=tostring(self);elseif type(self)~="string"then return error("string expected in \'self\', got "..type(self));end;end;do local _arg=arg;arg=tonumber(arg);if arg==nil then return error("number expected in \'arg\', got "..type(_arg));end;end;end;return inner;end"# + ); +} + +#[test] +fn call() { + assert_eq!(luaify!(table.field()), r#"table:field()"#); + assert_eq!(luaify!(table::field()), r#"table.field()"#); + assert_eq!(luaify!(table[field]()), r#"table[field]()"#); + assert_eq!(luaify!(a::b.c.field()), r#"a.b.c:field()"#); + assert_eq!(luaify!(a::b::c.field()), r#"a.b.c:field()"#); + assert_eq!(luaify!(a::b::c::field()), r#"a.b.c.field()"#); + assert_eq!( + luaify!(coroutine::r#yield("string")), + r#"coroutine.yield("string")"# + ); + + assert_eq!(luaify!(call(a(), (b)(), c.d())), r#"call(a(),(b)(),c:d())"#); +} + +#[test] +fn closure() { + assert_eq!(luaify!(|| hello), r#"function()return hello;end"#); + assert_eq!(luaify!(|| { hello }), r#"function()return hello;end"#); + assert_eq!( + luaify!(|| { + return hello; + }), + r#"function()return hello;end"# + ); + + assert_eq!( + luaify!(|| { { hello } }), + r#"function()do return hello;end;end"# + ); + + assert_eq!( + luaify!(|a, b| |c, d| hello), + r#"function(a,b)return function(c,d)return hello;end;end"# + ); +} + +#[test] +fn labels() { + assert_eq!( + luaify!(|| { + 'label: { + break 'label; + a(); + b() + } + }), + r#"function()do goto label_brk;a();return b();::label_cnt::end::label_brk::;end"# + ); + assert_eq!( + luaify!(|| { + 'label: { + continue 'label; + a(); + b() + } + }), + r#"function()do goto label_cnt;a();return b();::label_cnt::end::label_brk::;end"# + ); +} + +#[test] +fn loops() { + assert_eq!( + luaify!(|| { + loop { + break; + } + }), + r#"function()while true do break;end;end"# + ); + assert_eq!( + luaify!(|| { + 'label: loop { + break; + break 'label; + continue 'label; + a(); + b() + } + }), + r#"function()while true do break;goto label_brk;goto label_cnt;a();b();::label_cnt::end::label_brk::;end"# + ); + assert_eq!( + luaify!(|| { while cond {} }), + r#"function()while cond do end;end"# + ); + assert_eq!( + luaify!(|| { + 'label: while cond { + continue 'label; + break 'label; + a(); + b() + } + }), + r#"function()while cond do goto label_cnt;goto label_brk;a();b();::label_cnt::end::label_brk::;end"# + ); + assert_eq!( + luaify!(|| { + for (k, v) in pairs(table) { + print(k, v); + a(); + b() + } + }), + r#"function()for k,v in pairs(table)do print(k,v);a();b();end;end"# + ); + assert_eq!( + luaify!(|| { + for i in 0..=5 { + print(i) + } + for i in ..10 { + print(i) + } + }), + r#"function()for i=0,5 do print(i);end;for i=0,10-1 do print(i);end;end"# + ); +} + +#[test] +fn type_checks() { + assert_eq!(luaify!(|s| {}), r#"function(s)end"#); + assert_eq!( + luaify!(|s: table| {}), + r#"function(s)do if type(s)~="table"then return error("table expected in \'s\', got "..type(s));end;end;end"# + ); + assert_eq!( + luaify!(|s| { s as string }), + r#"function(s)do if type(s)=="number"then s=tostring(s);elseif type(s)~="string"then return error("string expected in \'s\', got "..type(s));end;end;end"# + ); + assert_eq!( + luaify!(|s| { s as number }), + r#"function(s)do local _s=s;s=tonumber(s);if s==nil then return error("number expected in \'s\', got "..type(_s));end;end;end"# + ); + assert_eq!( + luaify!(|s| { s as nil }), + r#"function(s)do if s~=nil then return error("nil expected in \'s\', got "..type(s));end;end;end"# + ); + assert_eq!(luaify!(|s| { s as any }), r#"function(s)do end;end"#); + + assert_eq!( + luaify!(|s| { + let (ok, res) = coroutine::r#yield(thread); + ok as boolean; + res as nil; + }), + r#"function(s)local ok,res=coroutine.yield(thread);do if type(ok)~="boolean"then return error("boolean expected in \'ok\', got "..type(ok));end;end;do if res~=nil then return error("nil expected in \'res\', got "..type(res));end;end;end"# + ); +} + +#[test] +fn concat() { + assert_eq!(luaify!(concat!(a)), r#"a"#); + assert_eq!(luaify!(concat!(a, b)), r#"a..b"#); + assert_eq!(luaify!(concat!(a, b, c)), r#"a..b..c"#); + assert_eq!( + luaify!(|a, b, c| concat!(a, b, c)), + r#"function(a,b,c)return a..b..c;end"# + ); +} + +#[test] +fn blocks() { + assert_eq!( + luaify!(|| { { "result" } }), + r#"function()do return"result";end;end"# + ); + assert_eq!( + luaify!(|| { + {} + { "result" } + }), + r#"function()do end;do return"result";end;end"# + ); + assert_eq!( + luaify!(|| { { { { "result" } } } }), + r#"function()do do do return"result";end;end;end;end"# + ); +} + +#[test] +fn ops() { + assert_eq!(luaify!(|| a = b), r#"function()a=b;end"#); + assert_eq!(luaify!(|| a + b), r#"function()return a+b;end"#); + assert_eq!(luaify!(|| a += b), r#"function()a=a+b;end"#); + assert_eq!(luaify!(|| a - b), r#"function()return a-b;end"#); + assert_eq!(luaify!(|| a -= b), r#"function()a=a-b;end"#); + assert_eq!(luaify!(|| a * b), r#"function()return a*b;end"#); + assert_eq!(luaify!(|| a *= b), r#"function()a=a*b;end"#); + assert_eq!(luaify!(|| a / b), r#"function()return a/b;end"#); + assert_eq!(luaify!(|| a /= b), r#"function()a=a/b;end"#); + assert_eq!(luaify!(|| a = b % c), r#"function()a=math.fmod(b,c);end"#); + assert_eq!(luaify!(|| a = b << c), r#"function()a=bit.lshift(b,c);end"#); + assert_eq!( + luaify!(|| a <<= b << c), + r#"function()a=bit.lshift(a,bit.lshift(b,c));end"# + ); + assert_eq!( + luaify!(|| a = b >> c), + r#"function()a=bit.arshift(b,c);end"# + ); + assert_eq!( + luaify!(|| a >>= b >> c), + r#"function()a=bit.arshift(a,bit.arshift(b,c));end"# + ); + assert_eq!(luaify!(|| a && b), r#"function()return a and b;end"#); + assert_eq!(luaify!(|| a || b), r#"function()return a or b;end"#); + assert_eq!( + luaify!(|| (a && b) || c), + r#"function()return(a and b)or c;end"# + ); + assert_eq!( + luaify!(|| (a || b) && c), + r#"function()return(a or b)and c;end"# + ); + assert_eq!( + luaify!(|| a && (b || c)), + r#"function()return a and(b or c);end"# + ); + assert_eq!( + luaify!(|| a || (b && c)), + r#"function()return a or(b and c);end"# + ); + assert_eq!( + luaify!(|| -a || !--b && c >> d), + r#"function()return-a or not-(-b)and bit.arshift(c,d);end"# + ); + assert_eq!( + luaify!(|| -a || !(--b && c) >> d), + r#"function()return-a or bit.arshift(not(-(-b)and c),d);end"# + ); + assert_eq!( + luaify!(|| a >> b << c >> d), + r#"function()return bit.arshift(bit.lshift(bit.arshift(a,b),c),d);end"# + ); +} + +#[test] +fn ifs() { + assert_eq!( + luaify!(|| { + if a == b { + c + } + }), + r#"function()if a==b then return c;end;end"# + ); + assert_eq!( + luaify!(|| { if a == b { c } else { d } }), + r#"function()if a==b then return c;else return d;end;end"# + ); + assert_eq!( + luaify!(|| { + if a == b { + c() + } else if b == c { + a() + } else { + d() + }; + }), + r#"function()if a==b then c();elseif b==c then a();else d();end;end"# + ); + assert_eq!( + luaify!(|| { + if a == b { + c() + } else if b == c { + a() + } else { + d() + } + }), + r#"function()if a==b then return c();elseif b==c then return a();else return d();end;end"# + ); + assert_eq!( + luaify!(|| { + if a == b { + c(); + } else if b == c { + a() + } else { + d(); + } + }), + r#"function()if a==b then c();elseif b==c then return a();else d();end;end"# + ); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}