pub mod token;
pub mod lexer;
pub mod ast;
pub mod parser;
pub mod typecheck;
pub mod codegen;
pub mod loader;
pub fn compile(source: &str) -> Result<Vec<u8>, CompileError> {
let tokens = lexer::lex(source)?;
let module = parser::parse(&tokens)?;
let typed = typecheck::check(&module)?;
let wasm = codegen::emit(&typed)?;
Ok(wasm)
}
#[derive(Debug, Clone)]
pub struct CompileError {
pub message: String,
pub span: Option<Span>,
pub code: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
pub fn line_col(source: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(source.len());
let mut line = 1usize;
let mut col = 1usize;
for (i, ch) in source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
pub fn render_snippet(source: &str, span: Span) -> Option<String> {
if source.is_empty() {
return None;
}
let start = span.start.min(source.len());
let (line, col) = line_col(source, start);
let line_start = source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = source[line_start..]
.find('\n')
.map(|i| line_start + i)
.unwrap_or(source.len());
let line_text: String = source[line_start..line_end]
.chars()
.map(|c| if c == '\t' { ' ' } else { c })
.collect();
let span_end = span.end.clamp(start, line_end.max(start));
let width = source[start..span_end.min(source.len())].chars().count().max(1);
let line_chars = line_text.chars().count();
let pad = (col - 1).min(line_chars);
let carets = width.min((line_chars + 1).saturating_sub(pad)).max(1);
Some(format!(
"line {line}, col {col}\n {line_text}\n {}{}",
" ".repeat(pad),
"^".repeat(carets)
))
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(code) = self.code {
write!(f, "{}: ", crate::error_codes::fmt_label(code))?;
}
if let Some(span) = self.span {
write!(f, "{} [{}..{}]", self.message, span.start, span.end)
} else {
write!(f, "{}", self.message)
}
}
}
impl std::error::Error for CompileError {}
impl CompileError {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into(), span: None, code: None }
}
pub fn at(message: impl Into<String>, span: Span) -> Self {
Self { message: message.into(), span: Some(span), code: None }
}
pub fn at_code(code: u16, message: impl Into<String>, span: Span) -> Self {
Self { message: message.into(), span: Some(span), code: Some(code) }
}
pub fn new_code(code: u16, message: impl Into<String>) -> Self {
Self { message: message.into(), span: None, code: Some(code) }
}
pub fn with_code(mut self, code: u16) -> Self {
self.code = Some(code);
self
}
pub fn location(&self, source: &str) -> Option<String> {
let span = self.span?;
let (line, col) = line_col(source, span.start.min(source.len()));
Some(format!("line {line}, col {col}"))
}
pub fn render(&self, source: &str) -> String {
match self.span.and_then(|s| render_snippet(source, s)) {
Some(snippet) => format!("{self}\n{snippet}"),
None => self.to_string(),
}
}
}
impl From<String> for CompileError {
fn from(s: String) -> Self { Self::new(s) }
}
#[cfg(test)]
mod tests {
use super::{compile, lexer, parser, typecheck};
use crate::error_codes as codes;
fn compile_err(src: &str) -> super::CompileError {
lexer::lex(src)
.and_then(|toks| parser::parse(&toks))
.and_then(|m| typecheck::check(&m))
.and_then(|t| super::codegen::emit(&t))
.expect_err("expected a compile error")
}
#[test]
fn compile_errors_carry_their_lh0xxx_code() {
let e = compile_err("fn frame(t: i32) { let x = true + 1; host::display::present(); }");
assert_eq!(e.code, Some(codes::TYPE_MISMATCH), "{e}");
assert!(e.to_string().starts_with("LH0204:"), "surfaced: {e}");
let e = compile_err("fn frame(t: i32) { host::display::clear(NOPE); host::display::present(); }");
assert_eq!(e.code, Some(codes::UNDEFINED_VARIABLE), "{e}");
let e = compile_err("fn (t: i32) {}");
assert_eq!(e.code, Some(codes::UNEXPECTED_TOKEN), "{e}");
let e = compile_err("fn frame(t: i32) { 5 = 9; host::display::present(); }");
assert_eq!(e.code, Some(codes::INVALID_ASSIGN_TARGET), "{e}");
let e = compile_err("fn frame(t: i32) { host::display::nope(1); host::display::present(); }");
assert_eq!(e.code, Some(codes::UNKNOWN_FUNCTION), "{e}");
let e = compile_err("fn frame(t: i32) { let x = `; }");
assert_eq!(e.code, Some(codes::UNEXPECTED_BYTE), "{e}");
let e = compile_err("fn frame(t: i32) { let x = true as i32; host::display::clear(x); host::display::present(); }");
assert_eq!(e.code, Some(codes::BAD_CAST), "{e}");
assert!(e.to_string().starts_with("LH0"), "surfaced: {e}");
}
#[test]
fn line_col_is_one_based_and_clamped() {
let src = "ab\ncde\nf";
assert_eq!(super::line_col(src, 0), (1, 1));
assert_eq!(super::line_col(src, 1), (1, 2));
assert_eq!(super::line_col(src, 3), (2, 1)); assert_eq!(super::line_col(src, 5), (2, 3));
assert_eq!(super::line_col(src, 7), (3, 1));
assert_eq!(super::line_col(src, 999), (3, 2));
assert_eq!(super::line_col("", 0), (1, 1));
}
#[test]
fn render_snippet_places_the_caret_under_the_span() {
let src = "fn frame(t: i32) {\n let x = true + 1;\n}";
let start = src.find("true").unwrap();
let snip = super::render_snippet(src, super::Span { start, end: start + 8 }).unwrap();
let lines: Vec<&str> = snip.lines().collect();
assert_eq!(lines[0], "line 2, col 11", "{snip}");
assert_eq!(lines[1], " let x = true + 1;", "{snip}");
assert_eq!(lines[2], format!(" {}{}", " ".repeat(10), "^".repeat(8)), "{snip}");
}
#[test]
fn render_snippet_edge_cases_never_panic() {
let snip = super::render_snippet("let x;", super::Span { start: 4, end: 4 }).unwrap();
assert!(snip.ends_with("^"), "{snip}");
let src = "a\nbb\ncc";
let snip = super::render_snippet(src, super::Span { start: 2, end: 7 }).unwrap();
assert!(snip.contains("line 2, col 1"), "{snip}");
assert_eq!(snip.lines().last().unwrap().matches('^').count(), 2, "{snip}");
let src = "fn f() {";
let snip = super::render_snippet(src, super::Span { start: 8, end: 8 }).unwrap();
assert!(snip.contains("line 1, col 9"), "{snip}");
assert!(super::render_snippet("x", super::Span { start: 50, end: 60 }).is_some());
assert!(super::render_snippet("", super::Span { start: 0, end: 1 }).is_none());
let snip = super::render_snippet("\tlet q = ;", super::Span { start: 9, end: 10 }).unwrap();
assert!(!snip.contains('\t'), "{snip}");
}
#[test]
fn compile_error_render_carries_code_location_and_caret() {
let src = "fn frame(t: i32) {\n let x = true + 1;\n host::display::present();\n}";
let err = compile(src).expect_err("type mismatch must fail");
let rendered = err.render(src);
assert!(rendered.starts_with("LH0204:"), "{rendered}");
assert!(rendered.contains("line 2, col"), "{rendered}");
assert!(rendered.contains("let x = true + 1;"), "{rendered}");
assert!(rendered.lines().last().unwrap().trim_start().starts_with('^'), "{rendered}");
assert!(err.location(src).expect("typed errors carry a span").starts_with("line 2, col "));
let plain = super::CompileError::new("internal");
assert_eq!(plain.render(src), "internal");
assert_eq!(plain.location(src), None);
}
#[test]
fn const_resolves_and_is_order_independent() {
assert!(compile(
"fn frame(t: i32) { host::display::clear(W); host::display::present(); } const W: i32 = 256;"
)
.is_ok());
assert!(compile(
"const A: i32 = 2; const B: i32 = A * 3; fn frame(t: i32) { host::display::clear(B); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { host::display::clear(NOPE); host::display::present(); }"
)
.is_err());
}
#[test]
fn casts_between_numbers() {
assert!(compile(
"fn frame(t: i32) { let x = t as f64; let y = x as i32; host::display::clear(y + (3.7 as i32)); host::display::present(); }"
)
.is_ok());
}
#[test]
fn arrays_literal_and_index() {
assert!(compile(
"fn frame(t: i32) { let pal = [16711680, 65280, 255]; host::display::clear(pal[t % 3]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let x = 5; host::display::clear(x[0]); host::display::present(); }"
)
.is_err());
assert!(compile(
"fn frame(t: i32) { let mut a = [1, 2, 3]; a[0] = 9; host::display::clear(a[0]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let mut a = [0, 0, 0, 0]; a[t % 4] = t; host::display::clear(a[t % 4]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let mut a = [1, 2, 3]; a[0] = true; host::display::present(); }"
)
.is_err());
assert!(compile(
"fn frame(t: i32) { let a = [1, 2, 3]; a[0] = 9; host::display::present(); }"
)
.is_err());
assert!(compile(
"fn frame(t: i32) { let mut x = 5; x[0] = 9; host::display::present(); }"
)
.is_err());
}
#[test]
fn array_params_and_repeat_init() {
assert!(compile(
"fn sum3(a: [i32; 3]) -> i32 { a[0] + a[1] + a[2] } \
fn frame(t: i32) { let g = [10, 20, 30]; host::display::clear(sum3(g)); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn set0(a: [i32; 3], v: i32) { a[0] = v; } \
fn frame(t: i32) { let mut g = [0, 0, 0]; set0(g, 7); host::display::clear(g[0]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let mut g = [0; 64]; g[5] = 9; host::display::clear(g[5]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let g = [t * 2; 8]; host::display::clear(g[3]); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let g = [0; 0]; host::display::present(); }"
)
.is_err());
assert!(compile(
"fn f(a: [bool; 2]) {} fn frame(t: i32) { host::display::present(); }"
)
.is_err());
assert!(compile(
"fn frame(t: i32) { let g = [true; 4]; host::display::present(); }"
)
.is_err());
}
#[test]
fn else_less_if_in_value_position_is_rejected() {
let e = compile(
"fn frame(t: i32) { let x = if t > 0 { 5 }; host::display::clear(x); host::display::present(); }"
)
.expect_err("else-less if used as a value must be rejected");
assert_eq!(e.code, Some(codes::TYPE_MISMATCH), "{e}");
assert!(compile(
"fn frame(t: i32) { let mut x = 0; if t > 0 { x = 5; } host::display::clear(x); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let x = if t > 0 { 5 } else { 9 }; host::display::clear(x); host::display::present(); }"
)
.is_ok());
}
#[test]
fn short_circuit_with_break_in_rhs_compiles() {
assert!(compile(
"fn frame(t: i32) { let mut i = 0; loop { let go = t > 0 && break; i = i + 1; } host::display::clear(i); host::display::present(); }"
)
.is_ok());
assert!(compile(
"fn frame(t: i32) { let mut i = 0; while i < 3 { i = i + 1; let keep = i > 9 || continue; } host::display::clear(i); host::display::present(); }"
)
.is_ok());
}
#[test]
fn non_last_irrefutable_match_arm_is_rejected() {
let e = compile(
"fn frame(t: i32) { let v = match t { _ => 1, 0 => 2 }; host::display::clear(v); host::display::present(); }"
)
.expect_err("a non-last wildcard arm must be rejected");
assert_eq!(e.code, Some(codes::TYPE_MISMATCH), "{e}");
let e = compile(
"fn frame(t: i32) { let v = match t { n => n, 0 => 2 }; host::display::clear(v); host::display::present(); }"
)
.expect_err("a non-last binding arm must be rejected");
assert_eq!(e.code, Some(codes::TYPE_MISMATCH), "{e}");
assert!(compile(
"fn frame(t: i32) { let v = match t { 0 => 2, _ => 1 }; host::display::clear(v); host::display::present(); }"
)
.is_ok());
}
#[test]
fn array_return_type_is_rejected() {
let e = compile("fn mk(v: i32) -> [i32; 3] { [v, v, v] } fn frame(t: i32) { let a = mk(1); host::display::clear(a[0]); host::display::present(); }")
.expect_err("array return must be rejected");
assert_eq!(e.code, Some(codes::UNSUPPORTED_FEATURE), "{e}");
assert!(compile(
"fn frame(t: i32) { host::display::present(); } fn mk() -> [i32; 2] { [1, 2] }"
)
.is_err());
assert!(compile(
"fn fill(a: [i32; 3], v: i32) -> i32 { a[0] = v; a[0] } \
fn frame(t: i32) { let mut g = [0, 0, 0]; host::display::clear(fill(g, 9)); host::display::present(); }"
)
.is_ok());
}
}
#[cfg(all(test, feature = "native"))]
mod array_write_run_proof {
use super::compile;
#[test]
fn emits_wasm_for_node_proof() {
let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("scripts")
.join(".array-write-proof");
std::fs::create_dir_all(&out_dir).expect("create proof dir");
let cases: &[(&str, &str)] = &[
(
"single.wasm",
"fn frame(t: i32) { let mut a = [0, 0, 0, 0]; a[2] = 42; host::display::clear(a[2]); host::display::present(); }",
),
(
"loopfill.wasm",
"fn frame(t: i32) { let mut a = [0, 0, 0, 0, 0]; for i in 0..5 { a[i] = i * 10; } host::display::clear(a[3]); host::display::present(); }",
),
(
"loopfill_t.wasm",
"fn frame(t: i32) { let mut a = [0, 0, 0, 0, 0]; for i in 0..5 { a[i] = i * 10; } host::display::clear(a[t]); host::display::present(); }",
),
(
"overwrite.wasm",
"fn frame(t: i32) { let mut a = [0, 0]; a[0] = 7; a[0] = 99; host::display::clear(a[0]); host::display::present(); }",
),
(
"param_read.wasm",
"fn sum(a: [i32; 3]) -> i32 { a[0] + a[1] + a[2] } \
fn frame(t: i32) { let g = [3, 4, 5]; host::display::clear(sum(g)); host::display::present(); }",
),
(
"param_shared_write.wasm",
"fn set1(a: [i32; 3], v: i32) { a[1] = v; } \
fn frame(t: i32) { let mut g = [0, 0, 0]; set1(g, 77); host::display::clear(g[1]); host::display::present(); }",
),
(
"repeat_fill.wasm",
"fn frame(t: i32) { let g = [9; 16]; host::display::clear(g[7]); host::display::present(); }",
),
(
"repeat_then_write.wasm",
"fn frame(t: i32) { let mut g = [5; 8]; g[2] = 88; host::display::clear(g[t]); host::display::present(); }",
),
];
for (file, src) in cases {
let wasm = compile(src).unwrap_or_else(|e| panic!("compile {file}: {e}"));
std::fs::write(out_dir.join(file), &wasm).unwrap_or_else(|e| panic!("write {file}: {e}"));
}
}
}
#[cfg(all(test, feature = "native"))]
mod codegen_valid_run_proof {
use super::compile;
#[test]
fn emits_codegen_valid_proof() {
let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("scripts")
.join(".codegen-valid-proof");
std::fs::create_dir_all(&out_dir).expect("create proof dir");
let cases: &[(&str, &str)] = &[
(
"elseless_if_stmt.wasm",
"fn frame(t: i32) { let mut x = 0; if t > 0 { x = 5; } host::display::clear(x); host::display::present(); }",
),
(
"value_if_else.wasm",
"fn frame(t: i32) { let x = if t > 0 { 5 } else { 9 }; host::display::clear(x); host::display::present(); }",
),
(
"and_break_rhs.wasm",
"fn frame(t: i32) { let mut i = 0; loop { let go = t > 0 && break; i = i + 1; } host::display::clear(i); host::display::present(); }",
),
(
"or_continue_rhs.wasm",
"fn frame(t: i32) { let mut i = 0; while i < 3 { i = i + 1; let keep = i > 9 || continue; } host::display::clear(i); host::display::present(); }",
),
(
"match_wildcard_last.wasm",
"fn frame(t: i32) { let v = match t { 0 => 2, 1 => 7, _ => 1 }; host::display::clear(v); host::display::present(); }",
),
];
for (file, src) in cases {
let wasm = compile(src).unwrap_or_else(|e| panic!("compile {file}: {e}"));
std::fs::write(out_dir.join(file), &wasm).unwrap_or_else(|e| panic!("write {file}: {e}"));
}
}
}