use luna_core::frontend::error::SyntaxError;
use luna_core::frontend::lexer::Lexer;
use luna_core::frontend::macro_expander::{Macro, MacroCtx};
use luna_core::frontend::span::Span;
use luna_core::frontend::token::{Token, TokenInfo};
use luna_core::frontend::{parse, parse_tokens};
use luna_core::runtime::Value;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
#[test]
fn puc_dialects_reject_at_token() {
for v in [
LuaVersion::Lua51,
LuaVersion::Lua52,
LuaVersion::Lua53,
LuaVersion::Lua54,
LuaVersion::Lua55,
] {
let err = parse(b"x = @foo()", v).expect_err(&format!("{v:?} should reject @"));
let msg = String::from_utf8_lossy(&err.msg);
assert!(
msg.contains("unexpected symbol"),
"{v:?}: expected 'unexpected symbol', got {msg}"
);
}
}
#[test]
fn macro_lua_lexer_emits_at() {
let src = b"@foo(1)";
let mut lex = Lexer::new(src, LuaVersion::MacroLua);
let mut tokens = Vec::new();
loop {
let t = lex.next_token().expect("lex");
if matches!(t.tok, Token::Eof) {
break;
}
tokens.push(t.tok);
}
assert_eq!(
tokens,
vec![
Token::At,
Token::Name("foo".into()),
Token::LParen,
Token::Int(1),
Token::RParen,
]
);
}
#[test]
fn macro_lua_lexer_emits_quote_block_sigils() {
let src = b"@{ x = 1 }@";
let mut lex = Lexer::new(src, LuaVersion::MacroLua);
let mut tokens = Vec::new();
loop {
let t = lex.next_token().expect("lex");
if matches!(t.tok, Token::Eof) {
break;
}
tokens.push(t.tok);
}
assert_eq!(
tokens,
vec![
Token::MacroBraceOpen,
Token::Name("x".into()),
Token::Assign,
Token::Int(1),
Token::MacroBraceClose,
]
);
}
#[test]
fn macro_lua_quote_roundtrip() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let r = vm.eval("local x = @quote{ 42 }; return x").expect("eval");
assert_eq!(r.len(), 1);
assert!(
matches!(r[0], Value::Int(42)),
"expected Int(42), got {:?}",
r[0]
);
}
#[test]
fn macro_lua_gensym_unique() {
let src = b"local a = 1 local b = 2";
let mac_src = b"local @gensym = 1 local @gensym = 2";
let mut vm = Vm::new(LuaVersion::MacroLua);
vm.load(mac_src, b"=test").expect("load with two @gensym");
let _ = src; }
#[test]
fn macro_lua_gensym_distinct_in_token_stream() {
use luna_core::frontend::macro_expander::MacroRegistry;
let mut r = MacroRegistry::with_builtins();
let src = b"local x = @gensym local y = @gensym";
let mut lex = Lexer::new(src, LuaVersion::MacroLua);
let mut raw = Vec::new();
loop {
let t = lex.next_token().expect("lex");
if matches!(t.tok, Token::Eof) {
break;
}
raw.push(t);
}
let out = r.expand(raw).expect("expand");
let gensyms: Vec<&str> = out
.iter()
.filter_map(|t| match &t.tok {
Token::Name(n) if n.starts_with("__lm_") => Some(n.as_ref()),
_ => None,
})
.collect();
assert_eq!(gensyms.len(), 2, "expected 2 gensym names");
assert_ne!(gensyms[0], gensyms[1]);
}
#[test]
fn macro_lua_conditional_compile_true_arm() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let r = vm
.eval("@if(true, @quote{ return 1 }, @quote{ return 2 })")
.unwrap_or_else(|e| panic!("eval @if true: {}", e));
assert_eq!(r.len(), 1);
assert!(matches!(r[0], Value::Int(1)));
}
#[test]
fn macro_lua_conditional_compile_false_arm() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let r = vm
.eval("@if(false, @quote{ return 1 }, @quote{ return 2 })")
.expect("eval @if false");
assert_eq!(r.len(), 1);
assert!(matches!(r[0], Value::Int(2)));
}
#[test]
fn macro_lua_conditional_literal_eq() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let r = vm
.eval(r#"@if("a" == "a", @quote{ return 7 }, @quote{ return 8 })"#)
.expect("eval @if str-eq");
assert!(matches!(r[0], Value::Int(7)));
}
struct DoubleMacro;
impl Macro for DoubleMacro {
fn expand(
&self,
args: &[Vec<TokenInfo>],
ctx: &mut MacroCtx<'_>,
) -> Result<Vec<TokenInfo>, SyntaxError> {
let arg = match args.len() {
1 => args[0].clone(),
_ => {
return Err(SyntaxError::new(
ctx.line,
format!("@double expects 1 arg, got {}", args.len()).into_bytes(),
));
}
};
let span = ctx.span;
let line = ctx.line;
let mk = |tok| TokenInfo { tok, span, line };
let mut out = vec![mk(Token::LParen)];
out.extend(arg);
out.extend([mk(Token::Star), mk(Token::Int(2)), mk(Token::RParen)]);
Ok(out)
}
}
#[test]
fn macro_lua_host_registered() {
let mut vm = Vm::new(LuaVersion::MacroLua);
vm.define_macro("double", Box::new(DoubleMacro));
let r = vm.eval("return @double(21)").expect("eval @double(21)");
assert_eq!(r.len(), 1);
assert!(matches!(r[0], Value::Int(42)));
}
#[test]
fn macro_lua_nested_expansion() {
let mut vm = Vm::new(LuaVersion::MacroLua);
vm.define_macro("double", Box::new(DoubleMacro));
let r = vm
.eval("local y = 5; return @double(y)")
.expect("nested arg expansion");
assert!(matches!(r[0], Value::Int(10)));
}
#[test]
fn macro_lua_unknown_macro_errors() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let err = vm
.eval("return @nope(1)")
.expect_err("unknown macro should error");
let msg = err.to_string();
assert!(
msg.contains("unknown macro") && msg.contains("nope"),
"expected 'unknown macro ... nope' in error, got: {msg}"
);
}
#[test]
fn macro_lua_at_with_no_name_errors() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let err = vm.eval("return @ + 1").expect_err("bare @ should error");
let msg = err.to_string();
assert!(
msg.contains("macro name expected"),
"expected 'macro name expected' in error, got: {msg}"
);
}
#[test]
fn macro_lua_unterminated_arg_list_errors() {
let mut vm = Vm::new(LuaVersion::MacroLua);
let err = vm
.eval("return @double(21")
.expect_err("unterminated args should error");
let msg = err.to_string();
assert!(msg.contains("unterminated macro arg list"), "got: {msg}");
}
#[test]
fn macro_lua_inherits_lua54_base() {
let mut vm = Vm::new(LuaVersion::MacroLua);
vm.eval("local x <const> = 7; return x")
.expect("MacroLua should accept <const>");
let err = vm
.eval("global x = 1")
.expect_err("MacroLua should reject 5.5 'global' keyword");
let _ = err; }
#[test]
fn macro_lua_parse_tokens_entry_point() {
let src = b"local x = 42";
let mut lex = Lexer::new(src, LuaVersion::Lua54);
let mut tokens = Vec::new();
loop {
let t = lex.next_token().expect("lex");
if matches!(t.tok, Token::Eof) {
break;
}
tokens.push(t);
}
parse_tokens(tokens, src, LuaVersion::Lua54).expect("parse_tokens parses");
}
#[test]
fn macro_lua_synthetic_spans_dont_crash_error_reporting() {
let src = b"";
let toks = vec![TokenInfo {
tok: Token::Plus,
span: Span::new(0, 0),
line: 1,
}];
let err = parse_tokens(toks, src, LuaVersion::Lua54).expect_err("parse should fail");
assert_eq!(err.line, 1);
}