use luna_jit::VmExt; use luna_jit::runtime::Value;
use luna_jit::version::LuaVersion;
use luna_jit::vm::Vm;
use std::io::{Read, Write};
const HELP: &str = "\
luna — a pure-Rust Lua runner
Usage:
luna interactive REPL
luna [opts] <script.lua> [args...] run a file
luna [opts] -e \"<code>\" [args...] run inline code
luna [opts] - read stdin to EOF
luna -h | --help print this help
Options:
--lua=X Select dialect (5.1 / 5.2 / 5.3 / 5.4 / 5.5; default 5.5)
--sandbox Open only safe stdlib subset (base/math/string/table/coroutine);
reject precompiled bytecode loading. Use for untrusted scripts.
--budget=N Cap dispatcher to N instructions before raising
\"instruction budget exceeded\".
--no-jit Install NullJitBackend (interpreter-only).
--profile On exit, print compiled-trace counters (trace_compiled_count,
trace_dispatched_count, ...) for tuning runs.
Extra positional args go into the `arg` global as PUC expects.
In REPL mode each line is first evaluated as an expression (prefixed
with `return`); on syntax error the line is re-evaluated as a
statement so assignments / function definitions work too.";
fn parse_version(arg: &str) -> Option<LuaVersion> {
match arg {
"5.1" => Some(LuaVersion::Lua51),
"5.2" => Some(LuaVersion::Lua52),
"5.3" => Some(LuaVersion::Lua53),
"5.4" => Some(LuaVersion::Lua54),
"5.5" => Some(LuaVersion::Lua55),
_ => None,
}
}
fn render(v: Value) -> String {
match v {
Value::Nil => "nil".into(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => format!("{f}"),
Value::Str(s) => format!("{:?}", String::from_utf8_lossy(s.as_bytes())),
Value::Table(_) => "<table>".into(),
Value::Closure(_) | Value::Native(_) => "<function>".into(),
Value::Coro(_) => "<thread>".into(),
Value::Userdata(_) => "<userdata>".into(),
Value::LightUserdata(_) => "<lightuserdata>".into(),
}
}
enum Source {
File(String),
Inline(String),
Stdin,
}
fn populate_arg(vm: &mut Vm, script_name: Option<&str>, extra: &[String]) {
let t = vm.heap.new_table();
if let Some(name) = script_name {
let k = Value::Str(vm.heap.intern(name.as_bytes()));
let _ = unsafe { t.as_mut() }.set(&mut vm.heap, Value::Int(0), k);
}
for (i, s) in extra.iter().enumerate() {
let v = Value::Str(vm.heap.intern(s.as_bytes()));
let _ = unsafe { t.as_mut() }.set(&mut vm.heap, Value::Int(i as i64 + 1), v);
}
vm.set_global("arg", Value::Table(t))
.expect("CLI arg setup");
}
const HISTORY_MAX_ENTRIES: usize = 1000;
fn history_path() -> Option<std::path::PathBuf> {
let home = std::env::var_os("HOME")?;
Some(std::path::PathBuf::from(home).join(".luna_history"))
}
fn load_history() -> Vec<String> {
let Some(p) = history_path() else {
return Vec::new();
};
match std::fs::read_to_string(&p) {
Ok(s) => s.lines().map(|l| l.to_string()).collect(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(_) => Vec::new(),
}
}
fn save_history(entries: &[String]) {
let Some(p) = history_path() else {
return;
};
let body = entries
.iter()
.rev()
.take(HISTORY_MAX_ENTRIES)
.rev()
.cloned()
.collect::<Vec<_>>()
.join("\n");
let _ = std::fs::write(&p, body);
}
fn is_incomplete_syntax(msg: &str) -> bool {
if msg.contains("'<eof>' expected") {
return false;
}
msg.contains(" near <eof>")
}
fn repl(version: LuaVersion) {
#[cfg(feature = "repl-line-editor")]
repl_rustyline(version);
#[cfg(not(feature = "repl-line-editor"))]
repl_plain(version);
}
fn repl_plain(version: LuaVersion) {
let mut vm = luna_jit::new_with_jit(version);
eprintln!(
"luna {} ({}) — interactive REPL. Ctrl-D to exit.",
env!("CARGO_PKG_VERSION"),
match version {
LuaVersion::Lua51 => "Lua 5.1",
LuaVersion::Lua52 => "Lua 5.2",
LuaVersion::Lua53 => "Lua 5.3",
LuaVersion::Lua54 => "Lua 5.4",
LuaVersion::MacroLua => "MacroLua (5.4 + @macros)",
LuaVersion::Lua55 => "Lua 5.5",
}
);
let stdin = std::io::stdin();
let mut history: Vec<String> = load_history();
let mut chunk = String::new();
let mut in_continuation = false;
loop {
eprint!("{}", if in_continuation { ">> " } else { "> " });
let _ = std::io::stderr().flush();
let mut line = String::new();
match stdin.read_line(&mut line) {
Ok(0) => {
if in_continuation {
eprintln!();
}
eprintln!();
save_history(&history);
return;
}
Ok(_) => {}
Err(e) => {
eprintln!("io error: {e}");
save_history(&history);
return;
}
}
if !in_continuation && line.trim().is_empty() {
continue;
}
if !chunk.is_empty() {
chunk.push('\n');
}
chunk.push_str(&line);
let as_expr = format!("return {chunk}");
let result = match vm.eval(&as_expr) {
Ok(vs) => Ok(vs),
Err(_) => vm.eval(chunk.as_str()),
};
match result {
Ok(vs) => {
for v in vs {
println!("{}", render(v));
}
history.push(chunk.trim_end().to_string());
chunk.clear();
in_continuation = false;
}
Err(e) => {
let msg = vm.error_text(&e);
if is_incomplete_syntax(&msg) {
in_continuation = true;
} else {
eprintln!("error: {}", msg);
history.push(chunk.trim_end().to_string());
chunk.clear();
in_continuation = false;
}
}
}
}
}
#[cfg(feature = "repl-line-editor")]
#[derive(Default)]
struct GlobalsSnapshot {
names: Vec<String>,
}
#[cfg(feature = "repl-line-editor")]
struct LuaHelper {
globals: std::rc::Rc<std::cell::RefCell<GlobalsSnapshot>>,
}
#[cfg(feature = "repl-line-editor")]
impl rustyline::completion::Completer for LuaHelper {
type Candidate = rustyline::completion::Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<rustyline::completion::Pair>)> {
let bytes = line.as_bytes();
let mut start = pos;
while start > 0 {
let c = bytes[start - 1];
if !(c.is_ascii_alphanumeric() || c == b'_') {
break;
}
start -= 1;
}
let prefix = &line[start..pos];
if prefix.is_empty() {
return Ok((pos, Vec::new()));
}
let snap = self.globals.borrow();
let mut matches: Vec<rustyline::completion::Pair> = snap
.names
.iter()
.filter(|n| n.starts_with(prefix))
.map(|n| rustyline::completion::Pair {
display: n.clone(),
replacement: n.clone(),
})
.collect();
matches.sort_by(|a, b| a.display.cmp(&b.display));
matches.dedup_by(|a, b| a.display == b.display);
Ok((start, matches))
}
}
#[cfg(feature = "repl-line-editor")]
impl rustyline::hint::Hinter for LuaHelper {
type Hint = String;
}
#[cfg(feature = "repl-line-editor")]
impl rustyline::validate::Validator for LuaHelper {}
#[cfg(feature = "repl-line-editor")]
impl rustyline::Helper for LuaHelper {}
#[cfg(feature = "repl-line-editor")]
impl rustyline::highlight::Highlighter for LuaHelper {
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> {
std::borrow::Cow::Owned(highlight_lua(line))
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> std::borrow::Cow<'b, str> {
std::borrow::Cow::Owned(format!("\x1b[2m{prompt}\x1b[0m"))
}
fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
true
}
}
#[cfg(feature = "repl-line-editor")]
fn repl_rustyline(version: LuaVersion) {
use rustyline::Editor;
use rustyline::error::ReadlineError;
use rustyline::history::DefaultHistory;
use std::cell::RefCell;
use std::rc::Rc;
let mut vm = luna_jit::new_with_jit(version);
eprintln!(
"luna {} ({}) — interactive REPL (rustyline). Ctrl-D to exit.",
env!("CARGO_PKG_VERSION"),
match version {
LuaVersion::Lua51 => "Lua 5.1",
LuaVersion::Lua52 => "Lua 5.2",
LuaVersion::Lua53 => "Lua 5.3",
LuaVersion::Lua54 => "Lua 5.4",
LuaVersion::MacroLua => "MacroLua (5.4 + @macros)",
LuaVersion::Lua55 => "Lua 5.5",
}
);
let globals = Rc::new(RefCell::new(GlobalsSnapshot::default()));
let helper = LuaHelper {
globals: globals.clone(),
};
let mut rl: Editor<LuaHelper, DefaultHistory> = match Editor::new() {
Ok(rl) => rl,
Err(e) => {
eprintln!("rustyline init failed ({e}); falling back to plain REPL");
repl_plain(version);
return;
}
};
rl.set_helper(Some(helper));
let hist_path = history_path();
if let Some(ref p) = hist_path {
let _ = rl.load_history(p);
}
let mut chunk = String::new();
let mut in_continuation = false;
loop {
refresh_globals_snapshot(&mut vm, &globals);
let prompt = if in_continuation { ">> " } else { "> " };
let line = match rl.readline(prompt) {
Ok(l) => l,
Err(ReadlineError::Eof) => {
if let Some(ref p) = hist_path {
let _ = rl.save_history(p);
}
return;
}
Err(ReadlineError::Interrupted) => {
chunk.clear();
in_continuation = false;
continue;
}
Err(e) => {
eprintln!("io error: {e}");
if let Some(ref p) = hist_path {
let _ = rl.save_history(p);
}
return;
}
};
if !in_continuation && line.trim().is_empty() {
continue;
}
if !chunk.is_empty() {
chunk.push('\n');
}
chunk.push_str(&line);
let as_expr = format!("return {chunk}");
let result = match vm.eval(&as_expr) {
Ok(vs) => Ok(vs),
Err(_) => vm.eval(chunk.as_str()),
};
match result {
Ok(vs) => {
for v in vs {
println!("{}", render(v));
}
let _ = rl.add_history_entry(chunk.trim_end());
chunk.clear();
in_continuation = false;
}
Err(e) => {
let msg = vm.error_text(&e);
if is_incomplete_syntax(&msg) {
in_continuation = true;
} else {
eprintln!("error: {msg}");
let _ = rl.add_history_entry(chunk.trim_end());
chunk.clear();
in_continuation = false;
}
}
}
}
}
#[cfg(feature = "repl-line-editor")]
fn refresh_globals_snapshot(vm: &mut Vm, snap: &std::rc::Rc<std::cell::RefCell<GlobalsSnapshot>>) {
let g = vm.globals();
let mut key: Value = Value::Nil;
let mut out: Vec<String> = Vec::new();
loop {
match g.next(key) {
Ok(Some((k, _v))) => {
if let Value::Str(s) = k {
let bytes = s.as_bytes();
if !bytes.is_empty()
&& bytes
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'_')
&& !bytes[0].is_ascii_digit()
{
out.push(String::from_utf8_lossy(bytes).into_owned());
}
}
key = k;
}
Ok(None) => break,
Err(_) => break,
}
}
snap.borrow_mut().names = out;
}
#[cfg(feature = "repl-line-editor")]
fn highlight_lua(src: &str) -> String {
const KEYWORDS: &[&str] = &[
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if",
"in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
];
const KW: &str = "\x1b[34m"; const STR: &str = "\x1b[33m"; const NUM: &str = "\x1b[35m"; const CMT: &str = "\x1b[2;37m"; const RST: &str = "\x1b[0m";
let bytes = src.as_bytes();
let mut out = String::with_capacity(src.len() + 16);
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'-' && i + 1 < bytes.len() && bytes[i + 1] == b'-' {
let start = i;
i += 2;
if i < bytes.len() && bytes[i] == b'[' {
let mut k = i + 1;
let mut level = 0;
while k < bytes.len() && bytes[k] == b'=' {
level += 1;
k += 1;
}
if k < bytes.len() && bytes[k] == b'[' {
let mut end = k + 1;
while end < bytes.len() {
if bytes[end] == b']' {
let mut m = end + 1;
let mut eq = 0;
while m < bytes.len() && bytes[m] == b'=' {
eq += 1;
m += 1;
}
if eq == level && m < bytes.len() && bytes[m] == b']' {
end = m + 1;
break;
}
}
end += 1;
}
let end = end.min(bytes.len());
out.push_str(CMT);
out.push_str(&src[start..end]);
out.push_str(RST);
i = end;
continue;
}
}
let mut end = i;
while end < bytes.len() && bytes[end] != b'\n' {
end += 1;
}
out.push_str(CMT);
out.push_str(&src[start..end]);
out.push_str(RST);
i = end;
continue;
}
if c == b'"' || c == b'\'' {
let quote = c;
let start = i;
i += 1;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if bytes[i] == quote {
i += 1;
break;
}
if bytes[i] == b'\n' {
break;
}
i += 1;
}
let end = i.min(bytes.len());
out.push_str(STR);
out.push_str(&src[start..end]);
out.push_str(RST);
continue;
}
if c == b'[' {
let mut k = i + 1;
let mut level = 0;
while k < bytes.len() && bytes[k] == b'=' {
level += 1;
k += 1;
}
if k < bytes.len() && bytes[k] == b'[' {
let start = i;
let mut end = k + 1;
while end < bytes.len() {
if bytes[end] == b']' {
let mut m = end + 1;
let mut eq = 0;
while m < bytes.len() && bytes[m] == b'=' {
eq += 1;
m += 1;
}
if eq == level && m < bytes.len() && bytes[m] == b']' {
end = m + 1;
break;
}
}
end += 1;
}
let end = end.min(bytes.len());
out.push_str(STR);
out.push_str(&src[start..end]);
out.push_str(RST);
i = end;
continue;
}
}
if c.is_ascii_digit() || (c == b'.' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit())
{
let start = i;
let hex =
c == b'0' && i + 1 < bytes.len() && (bytes[i + 1] == b'x' || bytes[i + 1] == b'X');
if hex {
i += 2;
let mut prev_exp = false;
while i < bytes.len() {
let b = bytes[i];
let is_sign_after_p = (b == b'+' || b == b'-') && prev_exp;
if b.is_ascii_hexdigit()
|| b == b'.'
|| matches!(b, b'p' | b'P')
|| is_sign_after_p
{
prev_exp = matches!(b, b'p' | b'P');
i += 1;
} else {
break;
}
}
} else {
let mut prev_exp = false;
while i < bytes.len() {
let b = bytes[i];
let is_sign_after_e = (b == b'+' || b == b'-') && prev_exp;
if b.is_ascii_digit()
|| b == b'.'
|| matches!(b, b'e' | b'E')
|| is_sign_after_e
{
prev_exp = matches!(b, b'e' | b'E');
i += 1;
} else {
break;
}
}
}
out.push_str(NUM);
out.push_str(&src[start..i]);
out.push_str(RST);
continue;
}
if c.is_ascii_alphabetic() || c == b'_' {
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
let word = &src[start..i];
if KEYWORDS.contains(&word) {
out.push_str(KW);
out.push_str(word);
out.push_str(RST);
} else {
out.push_str(word);
}
continue;
}
out.push(c as char);
i += 1;
}
out
}
fn main() {
let mut version = LuaVersion::Lua55;
let mut source: Option<Source> = None;
let mut extra: Vec<String> = Vec::new();
let mut consumed_source = false;
let mut sandbox = false;
let mut budget: Option<i64> = None;
let mut no_jit = false;
let mut profile = false;
let mut it = std::env::args().skip(1).peekable();
while let Some(a) = it.next() {
if consumed_source {
extra.push(a);
continue;
}
if a == "-h" || a == "--help" {
println!("{HELP}");
return;
}
if let Some(v) = a.strip_prefix("--lua=") {
version = parse_version(v).unwrap_or_else(|| {
eprintln!("error: unknown --lua={v} (use 5.1 / 5.2 / 5.3 / 5.4 / 5.5)");
std::process::exit(2);
});
continue;
}
if a == "--sandbox" {
sandbox = true;
continue;
}
if let Some(n) = a.strip_prefix("--budget=") {
budget = Some(n.parse().unwrap_or_else(|_| {
eprintln!("error: --budget=N expects an integer");
std::process::exit(2);
}));
continue;
}
if a == "--no-jit" {
no_jit = true;
continue;
}
if a == "--profile" {
profile = true;
continue;
}
if a == "-e" {
let code = it.next().unwrap_or_else(|| {
eprintln!("error: -e expects a code string");
std::process::exit(2);
});
source = Some(Source::Inline(code));
consumed_source = true;
continue;
}
if a == "-" {
source = Some(Source::Stdin);
consumed_source = true;
continue;
}
source = Some(Source::File(a));
consumed_source = true;
}
let Some(source) = source else {
repl(version);
return;
};
let (src, chunkname, script_for_arg) = match source {
Source::File(p) => {
let src = std::fs::read(&p).unwrap_or_else(|e| {
eprintln!("error: read {p}: {e}");
std::process::exit(1);
});
let cn = format!("@{p}");
(src, cn, Some(p))
}
Source::Inline(code) => (code.into_bytes(), "=(inline)".to_string(), None),
Source::Stdin => {
let mut buf = Vec::new();
std::io::stdin().read_to_end(&mut buf).unwrap_or_else(|e| {
eprintln!("error: read stdin: {e}");
std::process::exit(1);
});
(buf, "=stdin".to_string(), None)
}
};
let mut vm = if sandbox {
let mut vm = luna_jit::vm::Vm::sandbox(version)
.open_base()
.open_math()
.open_string()
.open_table()
.open_coroutine()
.build();
if !no_jit {
vm.install_default_jit();
}
vm
} else if no_jit {
let mut vm = luna_jit::vm::Vm::new(version);
vm.install_null_jit();
vm
} else {
luna_jit::new_with_jit(version)
};
if let Some(n) = budget {
vm.set_instr_budget(Some(n));
}
populate_arg(&mut vm, script_for_arg.as_deref(), &extra);
let color = std::env::var_os("NO_COLOR").is_none()
&& std::io::IsTerminal::is_terminal(&std::io::stderr());
let cl = match vm.load(&src, chunkname.as_bytes()) {
Ok(cl) => cl,
Err(e) => {
print_pretty_error(
&mut vm,
&format!("{e}"),
&src,
color,
true,
);
std::process::exit(1);
}
};
let result = vm.call_value(Value::Closure(cl), &[]);
if profile {
eprintln!("---");
eprintln!("profile (trace JIT counters):");
eprintln!(" trace_closed_count: {}", vm.jit.counters.closed);
eprintln!(" trace_compiled_count: {}", vm.jit.counters.compiled);
eprintln!(
" trace_compile_failed_count: {}",
vm.jit.counters.compile_failed
);
eprintln!(" trace_dispatched_count: {}", vm.jit.counters.dispatched);
eprintln!(" trace_deopt_count: {}", vm.jit.counters.deopt);
eprintln!(
" trace_side_trace_started_count: {}",
vm.jit.counters.side_trace_started
);
eprintln!(
" trace_side_trace_compiled_count: {}",
vm.jit.counters.side_trace_compiled
);
}
match result {
Ok(vals) => {
for v in vals {
println!("=> {}", render(v));
}
}
Err(e) => {
let msg = vm.error_text(&e);
print_pretty_error(&mut vm, &msg, &src, color, false);
std::process::exit(1);
}
}
}
fn print_pretty_error(vm: &mut Vm, msg: &str, src: &[u8], color: bool, compile_time: bool) {
let (red, dim, bold, reset) = if color {
("\x1b[31m", "\x1b[2m", "\x1b[1m", "\x1b[0m")
} else {
("", "", "", "")
};
let kind_label = if compile_time {
"compile error"
} else {
"runtime error"
};
let kind_str = if compile_time {
"syntax".to_string()
} else {
format!("{}", vm.error_kind())
};
eprintln!("{bold}{red}{kind_label}{reset} {dim}[{kind_str}]{reset}: {msg}");
if let Some((chunk, line)) = vm.error_source() {
eprintln!(" {dim}at {chunk}:{line}{reset}");
if let Some(snippet) = nth_line(src, line) {
eprintln!(" {dim}|{reset} {snippet}");
}
}
if let Some(tb) = vm.take_error_traceback() {
eprintln!("{dim}traceback:{reset}");
for line in tb.lines() {
eprintln!(" {line}");
}
}
}
fn nth_line(src: &[u8], n: u32) -> Option<String> {
if n == 0 {
return None;
}
let s = String::from_utf8_lossy(src);
s.lines().nth((n - 1) as usize).map(|l| l.to_string())
}