use crate::emitter::AsmDialect;
use crate::lower::compile_source;
fn assembly(source: &str) -> String {
compile_source(source, AsmDialect::LinuxElf, false)
.expect("program should lower")
.text
}
fn macho_assembly(source: &str) -> String {
compile_source(source, AsmDialect::MachO, false)
.expect("program should lower")
.text
}
fn error_message(source: &str) -> String {
compile_source(source, AsmDialect::LinuxElf, false).expect_err("program should be rejected")
}
#[test]
fn snapshot_integer_arithmetic() {
insta::assert_snapshot!(assembly("1 + 2 * 3;"));
}
#[test]
fn snapshot_global_rebinding() {
insta::assert_snapshot!(assembly("let x = 1; let x = x + 2; x;"));
}
#[test]
fn snapshot_if_else_and_comparison() {
insta::assert_snapshot!(assembly("if (1 < 2) { 10 } else { 20 };"));
}
#[test]
fn snapshot_recursion() {
insta::assert_snapshot!(assembly(
"let fib = fn(n) { if (n < 2) { n } else { fib(n - 1) + fib(n - 2) } };\nfib(10);"
));
}
#[test]
fn snapshot_named_function_self_reference() {
insta::assert_snapshot!(assembly(
"let f = fn(n) { if (n > 0) { f(n - 1) } else { 0 } };\nf(3);"
));
}
#[test]
fn snapshot_closure_capture() {
insta::assert_snapshot!(assembly("let adder = fn(x) { fn(y) { x + y } };\nadder(1)(2);"));
}
#[test]
fn snapshot_builtins_first_class() {
insta::assert_snapshot!(assembly("let p = puts; p(len(\"abc\"));"));
}
#[test]
fn snapshot_aggregates_and_index() {
insta::assert_snapshot!(assembly("let a = [1, 2];\nlet h = {\"k\": a[0]};\nh[\"k\"];"));
}
#[test]
fn snapshot_classes() {
insta::assert_snapshot!(assembly(
"class Counter {\n constructor(start) { this.count = start; }\n inc() { this.count = this.count + 1; this.count }\n}\nlet c = new Counter(5);\nc.inc();\nc.count;"
));
}
#[test]
fn snapshot_return_paths() {
insta::assert_snapshot!(assembly("let f = fn() { return 1; 2; };\nf();"));
}
#[test]
fn snapshot_debugger_transparency() {
insta::assert_snapshot!(assembly(
"let f = fn(n) { n * 2; debugger; };\ndebugger;\nputs(f(21));"
));
}
#[test]
fn snapshot_boxed_integer_literal() {
insta::assert_snapshot!(assembly("9223372036854775807;"));
}
#[test]
fn snapshot_observe_mode() {
insta::assert_snapshot!(
compile_source("1 + 2;", AsmDialect::LinuxElf, true)
.unwrap()
.text
);
}
#[test]
fn snapshot_macho_dialect() {
insta::assert_snapshot!(macho_assembly(
"let msg = \"hé\";\nlet inc = fn(n) { n + 1 };\nif (inc(1) < 3) { msg } else { \"no\" };"
));
}
#[test]
fn macho_never_leaks_elf_spellings() {
let text = macho_assembly(
"class Counter {\n constructor(start) { this.count = start; }\n inc() { this.count = this.count + 1; this.count }\n}\nlet c = new Counter(5);\nc.inc();\nlet a = [c.count, len(\"x\")];\n-a[0];\n!true;\nputs(a);"
);
assert!(!text.contains(":lo12:"));
assert!(!text.contains(".L"));
assert!(!text.contains("bl rt_"));
assert!(text.contains("bl _rt_call"));
assert!(text.contains(".zerofill __DATA,__bss,g_globals,"));
}
#[test]
fn line_spans_point_into_the_source() {
let source = "let x = 41;\nx + 1;";
let assembly = compile_source(source, AsmDialect::LinuxElf, false).unwrap();
assert_eq!(assembly.text.lines().count(), assembly.line_spans.len());
let mut spanned = 0;
for span in assembly.line_spans.iter().flatten() {
let (start, end) = *span;
assert!(start < end && end <= source.len(), "span out of range: {:?}", span);
spanned += 1;
}
assert!(spanned > 0, "expected some lines to carry source spans");
let lines: Vec<&str> = assembly.text.lines().collect();
let add_line = lines
.iter()
.position(|line| line.contains("bl rt_add"))
.unwrap();
let (start, end) = assembly.line_spans[add_line].unwrap();
assert_eq!(&source[start..end], "x + 1");
}
#[test]
fn parameter_limits_are_rejected() {
assert_eq!(
error_message("fn(a, b, c, d, e, f, g, h) { 0 };"),
"functions accept at most 7 parameters"
);
assert_eq!(
error_message("class C { m(a, b, c, d, e, f, g) { 0 } }"),
"methods accept at most 6 parameters"
);
assert!(compile_source("fn(a, b, c, d, e, f, g) { 0 };", AsmDialect::LinuxElf, false).is_ok());
assert!(compile_source("class C { m(a, b, c, d, e, f) { 0 } }", AsmDialect::LinuxElf, false)
.is_ok());
}
#[test]
fn validation_runs_before_lowering() {
assert!(error_message("missing;").contains("undefined variable 'missing'"));
assert!(error_message("this;").contains("this is only available inside a method"));
assert!(error_message("class C { constructor() { return 1; } }")
.contains("constructor cannot return a value"));
}
#[test]
fn builtins_do_not_occupy_global_slots() {
let text = assembly("puts(1);");
assert!(text.contains(".skip 0"));
assert!(text.contains("movz x0, #0xd"));
}
#[test]
fn type_annotations_lower_to_identical_assembly() {
let pairs = [
(
"let add = fn(a: int, b: int): int { a + b }; add(1, 2);",
"let add = fn(a, b) { a + b }; add(1, 2);",
),
(
"class C { constructor(x: int) { this.x = x; } get(): int { this.x } } new C(1).get();",
"class C { constructor(x) { this.x = x; } get() { this.x } } new C(1).get();",
),
("let xs: [int] = [1, 2]; xs[0];", "let xs = [1, 2]; xs[0];"),
];
for (annotated, erased) in pairs {
assert_eq!(
strip_comments(&assembly(annotated)),
strip_comments(&assembly(erased)),
"assembly differs for {}",
annotated
);
assert_eq!(
strip_comments(&macho_assembly(annotated)),
strip_comments(&macho_assembly(erased)),
"Mach-O assembly differs for {}",
annotated
);
}
assert!(assembly("let x: int = 1;").contains("// let x: int = 1;"));
}
fn strip_comments(text: &str) -> String {
return text
.lines()
.map(|line| line.split("//").next().unwrap_or("").trim_end())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n");
}