pub mod builtins_folding;
pub mod constant_fold;
pub mod cost_model;
pub mod ir;
pub mod value_tracking;
use crate::ast::node_id::NodeId;
use crate::bytecode::proto::Proto;
use crate::bytecode::serialize::serialize;
use crate::parser::{Parser, Stmt};
use std::collections::HashMap;
use std::fs;
use std::rc::Rc;
pub use constant_fold::fold_program;
pub use value_tracking::{TrackedValue, ValueTracker, value_tracking_cfg};
pub struct Parsed {
pub stmts: Vec<Stmt>,
pub lines: Rc<HashMap<NodeId, u32>>,
pub spans: crate::ast::span::SpanPool,
}
#[derive(Clone, Copy)]
pub struct Options {
pub check: bool,
pub inline: bool,
pub keep_locals: bool,
}
impl Options {
pub const RUN: Options = Options {
check: true,
inline: true,
keep_locals: false,
};
pub const DEBUG: Options = Options {
check: true,
inline: false,
keep_locals: true,
};
pub const UNCHECKED: Options = Options {
check: false,
inline: true,
keep_locals: false,
};
}
pub fn parse(source: &str) -> Result<Parsed, String> {
let (mut stmts, pool) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut parser = Parser::new(source);
parser.parse_program()
}))
.map_err(|payload| {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"syntax error".to_string()
}
})??;
crate::ast::visitor::check_ast_sanity(&stmts)?;
crate::ast::visitor_mut::normalize_ast(&mut stmts);
Ok(Parsed {
stmts,
lines: Rc::new(pool.start_lines()),
spans: pool,
})
}
pub fn compile_source(source: &str) -> Result<Vec<Stmt>, String> {
parse(source).map(|parsed| parsed.stmts)
}
pub fn compile_program(source: &str, chunk: &str, options: Options) -> Result<Proto, String> {
let parsed = match parse(source) {
Ok(p) => p,
Err(err) => {
let diag = crate::lint::syntax_error(&err);
return Err(crate::diagnostics::render_report(chunk, source, &[diag]));
}
};
if options.check {
check(&parsed.stmts, source, chunk, Some(&parsed.spans))?;
}
let mut proto = lower(&parsed.stmts, Some(parsed.lines), options)?;
proto.set_source(Rc::from(chunk));
Ok(proto)
}
pub fn compile_file(path: &str) -> Result<Vec<Stmt>, String> {
let source =
fs::read_to_string(path).map_err(|err| format!("failed to read {}: {}", path, err))?;
compile_source(&source)
}
fn check(
statements: &[Stmt],
source: &str,
chunk: &str,
spans: Option<&crate::ast::span::SpanPool>,
) -> Result<(), String> {
let diags = match spans {
Some(spans) => crate::sema::analyze_with_spans(statements, source, spans),
None => crate::sema::analyze(statements, source),
};
let errors: Vec<_> = diags
.into_iter()
.filter(|d| d.severity == crate::diagnostics::severity::Severity::Error)
.collect();
if !errors.is_empty() {
if !source.is_empty() {
return Err(crate::diagnostics::render_report(chunk, source, &errors));
} else {
return Err(format!("semantic error: {}", errors[0].message));
}
}
Ok(())
}
pub fn try_compile_to_proto(statements: &[Stmt]) -> Result<Proto, String> {
check(statements, "", "<anon>", None)?;
lower(statements, None, Options::RUN)
}
pub fn compile_bundled_to_proto(statements: &[Stmt]) -> Result<Proto, String> {
lower(statements, None, Options::UNCHECKED)
}
fn lower(
statements: &[Stmt],
lines: Option<Rc<HashMap<NodeId, u32>>>,
options: Options,
) -> Result<Proto, String> {
let optimized_stmts = fold_program(statements.to_vec());
let mut ir_module = ir::ast_to_ir_with(&optimized_stmts, lines, options.keep_locals);
if options.inline {
ir::inline_functions(&mut ir_module);
}
optimize_ir_function(&mut ir_module.main);
ir::verify_module(&ir_module)
.map_err(|errs| format!("IR verification error: {}", errs.join(", ")))?;
ir::ir_to_bytecode(&ir_module)
}
pub fn dump_ir(statements: &[Stmt]) -> String {
let optimized_stmts = fold_program(statements.to_vec());
let mut ir_module = ir::ast_to_ir(&optimized_stmts);
let mut out = String::from("=== before optimization ===\n");
out.push_str(&ir::pretty::print_module(&ir_module));
if let Err(errs) = ir::verify_module(&ir_module) {
out.push_str(&format!("\n[verification warnings: {}]\n", errs.join(", ")));
}
ir::inline_functions(&mut ir_module);
optimize_ir_function(&mut ir_module.main);
out.push_str("\n=== after optimization ===\n");
out.push_str(&ir::pretty::print_module(&ir_module));
if let Err(errs) = ir::verify_module(&ir_module) {
out.push_str(&format!("\n[verification warnings: {}]\n", errs.join(", ")));
}
out
}
fn optimize_ir_function(func: &mut ir::IrFunction) {
let mut cfg = ir::build_cfg(&func.instructions);
ir::optimize_cfg(&mut cfg);
func.instructions = cfg.to_flat_instructions();
func.cfg = Some(cfg);
for child in &mut func.protos {
optimize_ir_function(child);
}
}
pub fn compile_to_proto(statements: &[Stmt]) -> Proto {
try_compile_to_proto(statements).unwrap_or_else(|err| panic!("{}", err))
}
pub fn compile_source_to_bytecode(source: &str, chunk: &str) -> Result<Vec<u8>, String> {
Ok(serialize(&compile_program(source, chunk, Options::RUN)?))
}
pub fn compile_file_to_bytecode(path: &str) -> Result<Vec<u8>, String> {
let source =
fs::read_to_string(path).map_err(|err| format!("failed to read {}: {}", path, err))?;
compile_source_to_bytecode(&source, path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::MAGIC;
#[test]
fn test_compile_source_to_bytecode_magic() {
let code = "local a = 10\nlocal b = 20\nreturn a + b";
let bc = compile_source_to_bytecode(code, "test").expect("compilation failed");
assert!(bc.starts_with(MAGIC));
}
#[test]
fn test_compile_registers_and_proto() {
let code = "local x = 5\nreturn x";
let stmts = compile_source(code).expect("syntax error");
let proto = compile_to_proto(&stmts);
assert!(proto.max_registers >= 1);
assert!(!proto.instructions.is_empty());
}
#[test]
fn test_compile_error_on_invalid_interpolation() {
let code = "local s = `hello {name`";
let res = compile_source(code);
assert!(res.is_err());
assert!(res.unwrap_err().to_lowercase().contains("unfinished"));
let code2 = "local s = `hello {a; b}`";
let res2 = compile_source(code2);
assert!(res2.is_err());
assert!(res2.unwrap_err().contains("exactly one expression"));
}
#[test]
fn test_ir_pipeline_inlines_small_leaf_call() {
let code = "local function add(x, y) return x + y end\nlocal s = 0\nfor i = 1, 5 do s = s + add(i, i) end\nreturn s";
let stmts = compile_source(code).expect("syntax error");
let proto = try_compile_to_proto(&stmts).expect("IR compilation failed");
assert!(
!proto
.instructions
.iter()
.any(|i| matches!(i, crate::bytecode::instruction::Instruction::Call { .. })),
"expected inlined call, got:\n{}",
crate::bytecode::disasm::disassemble_proto(&proto, 0)
);
let mut vm = crate::vm::machine::VM::new();
let res = vm.execute(proto).expect("exec failed");
assert_eq!(res.to_string(), "30");
}
#[test]
fn test_multiple_returns_and_generic_for_state() {
let code = "local function rotate(a, b, c) return b, c, a end
local x, y, z = rotate(1, 2, 3)
local function step(t, i) i = i + 1 if t[i] == nil then return nil end return i, t[i] end
local function each(t) return step, t, 0 end
local sum = 0
for i, v in each({10, 20}) do sum = sum + i * v end
return x * 100 + y * 10 + z + sum";
let stmts = compile_source(code).expect("syntax error");
let proto = try_compile_to_proto(&stmts).expect("IR compilation failed");
let res = crate::vm::machine::VM::new()
.execute(proto)
.expect("exec failed");
assert_eq!(res.to_string(), "281");
}
#[test]
fn test_deepest_accepted_programs_fit_a_default_stack() {
type Shape = fn(usize) -> String;
let shapes: [(&str, Shape); 6] = [
("blocks", |n| {
format!("{}x = 1{}", "if true then\n".repeat(n), "\nend".repeat(n))
}),
("loops", |n| {
format!(
"{}x = 1{}",
"while x == nil do\n".repeat(n),
"\nend".repeat(n)
)
}),
("parentheses", |n| {
format!("x = {}1{}", "(".repeat(n), ")".repeat(n))
}),
("operator chain", |n| format!("x = 1{}", " + 1".repeat(n))),
("tables", |n| {
format!("x = {}{}", "{".repeat(n), "}".repeat(n))
}),
("calls", |n| {
format!("x = {}1{}", "tostring(".repeat(n), ")".repeat(n))
}),
];
let worker = std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(move || {
for (name, shape) in shapes {
let deepest = (1..=crate::parser::MAX_PARSE_DEPTH)
.rev()
.find(|&n| compile_source(&shape(n)).is_ok())
.unwrap_or_else(|| panic!("no {} program parses", name));
assert!(deepest >= 60, "{} only nest {} deep", name, deepest);
let source = format!("local x\n{}", shape(deepest));
let stmts = compile_source(&source).expect(name);
let proto = try_compile_to_proto(&stmts).expect(name);
crate::vm::machine::VM::new().execute(proto).expect(name);
}
})
.unwrap();
worker
.join()
.expect("a pass overflowed or failed on the deepest program");
}
}