use crate::analyzer::Analyzer;
use crate::lexer::Lexer;
use crate::parser::ast::Type;
use crate::parser::Parser;
use super::{CodeGenerator, LibBlock};
fn compile_to_asm(source: &str) -> String {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens).with_source("unit_test.vox", source);
let mut program = parser
.parse()
.expect("test snippet should parse cleanly");
let mut analyzer = Analyzer::new().with_source("unit_test.vox", source);
analyzer.analyze(&mut program);
assert!(
analyzer.errors.is_empty(),
"test snippet should analyze cleanly, got: {:?}",
analyzer.errors
);
let mut gen = CodeGenerator::new();
gen.generate(&program)
}
fn compile_to_asm_shared(source: &str) -> String {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens).with_source("lib_test.vox", source);
let mut program = parser
.parse()
.expect("shared test snippet should parse cleanly");
let mut analyzer = Analyzer::new()
.with_source("lib_test.vox", source)
.with_shared_mode(true);
analyzer.analyze(&mut program);
assert!(
analyzer.errors.is_empty(),
"shared test snippet should analyze cleanly, got: {:?}",
analyzer.errors
);
let mut gen = CodeGenerator::new();
gen.set_shared_lib_mode(true);
gen.generate(&program)
}
#[test]
fn quoted_zero_arg_identifier_in_expression_position_calls_not_reads() {
let asm = compile_to_asm(
"To 'get five', Return a number, 5.\n\
a number called x is 'get five'.\n\
print x.\n",
);
assert!(
asm.contains("call get_five"),
"a bare zero-arg identifier in expression position must call the function: {}",
asm
);
}
#[test]
fn whole_list_print_routes_to_list_print() {
let asm = compile_to_asm("a list called xs is [1, 2, 3].\nprint xs.\n");
assert!(
asm.contains("call _list_print"),
"a whole-list print must route to _list_print, not PRINT_INT"
);
assert_eq!(
asm.matches("call _list_print").count(),
1,
"expected exactly one `call _list_print`"
);
}
#[test]
fn list_format_interpolation_routes_to_list_print() {
let asm = compile_to_asm("a list called xs is [1, 2, 3].\nprint \"xs: {xs}\".\n");
assert!(
asm.contains("call _list_print"),
"a {{list}} interpolation must route to _list_print"
);
assert_eq!(asm.matches("call _list_print").count(), 1);
}
#[test]
fn mixed_list_whole_print_routes_to_list_print() {
let asm = compile_to_asm("a list called m is [1, \"two\", 3.5].\nprint m.\n");
assert!(asm.contains("call _list_print"));
assert_eq!(asm.matches("call _list_print").count(), 1);
}
#[test]
fn non_list_print_does_not_route_to_list_print() {
let asm = compile_to_asm("a number called n is 5.\nprint n.\n");
assert!(
!asm.contains("call _list_print"),
"a non-list print must not route to _list_print"
);
}
#[test]
fn multiple_list_prints_each_route_to_list_print() {
let asm = compile_to_asm(
"a list called xs is [1, 2, 3].\nprint xs.\nprint \"xs: {xs}\".\n",
);
assert_eq!(asm.matches("call _list_print").count(), 2);
}
#[test]
fn homogeneous_int_list_keeps_fast_path() {
let asm =
compile_to_asm("a list called xs is [1, 2, 3].\nprint element 1 of xs.\n");
assert!(
!asm.contains("mixp_"),
"a homogeneous int list must not emit mixed-dispatch labels"
);
assert!(
!asm.contains("movzx r11, byte"),
"a homogeneous int list read must not load a runtime tag into r11"
);
}
#[test]
fn mixed_list_emits_dispatch() {
let asm =
compile_to_asm("a list called m is [1, \"two\"].\nprint element 1 of m.\n");
assert!(
asm.contains("mixp_"),
"a mixed list read must emit mixed-dispatch labels"
);
}
#[test]
fn declared_text_function_append_tagged_string() {
let asm = compile_to_asm(
"To greet with a number called x.\n Return a text, \"hi\".\n\
a list called items is [].\n\
append 1 to items.\n\
append greet of 0 to items.\n\
print element 1 of items.\n\
print element 2 of items.\n",
);
assert!(
asm.contains("mov edx, 1 ; element type tag"),
"the text-returning function result must be written with TAG_STRING (1)"
);
assert!(
asm.contains("mixp_"),
"the list widened to Mixed (int + text function result)"
);
}
#[test]
fn read_of_widened_list_does_not_prove_a_type() {
let asm = compile_to_asm(
"a list called m is [1, 2].\n\
append \"hi\" to m.\n\
a list called out is [0, 0].\n\
set element 1 of out to element 3 of m.\n\
print element 1 of out.\n",
);
assert!(
asm.contains("mixp_"),
"reading an element of a widened list must widen the destination"
);
}
#[test]
fn declared_type_does_not_forge_a_string_tag() {
let asm = compile_to_asm(
"a list called m is [\"a\", \"b\"].\n\
append 42 to m.\n\
a text called s is element 3 of m.\n\
a list called out is [].\n\
append s to out.\n",
);
assert!(
!asm.contains("mov edx, 1 ; element type tag"),
"an unprovable value must not be written with TAG_STRING"
);
}
#[test]
fn declared_type_still_tags_a_provable_string() {
let asm = compile_to_asm(
"a text called s is \"hello\".\n\
a list called out is [].\n\
append s to out.\n",
);
assert!(
asm.contains("mov edx, 1 ; element type tag"),
"a provably-text value must still be written with TAG_STRING"
);
}
#[test]
fn format_string_append_tags_string() {
let asm = compile_to_asm(
"a list called out is [].\n\
a number called k is 7.\n\
append \"n {k}\" to out.\n",
);
assert!(
asm.contains("mov edx, 1 ; element type tag"),
"a format-string append must be written with TAG_STRING"
);
}
#[test]
fn format_string_append_does_not_spuriously_widen_list() {
let asm = compile_to_asm(
"a list called out is [].\n\
a number called k is 7.\n\
append \"n {k}\" to out.\n\
append \"literal\" to out.\n\
print element 1 of out.\n",
);
assert!(
!asm.contains("mixp_"),
"a format-string append must not widen the list to Mixed"
);
}
#[test]
fn format_string_local_appended_by_name_tags_string() {
let asm = compile_to_asm(
"a text called greeting is \"hi\".\n\
a text called tok is \"fmt {greeting}\".\n\
a list called out is [].\n\
append tok to out.\n",
);
assert!(
asm.contains("mov edx, 1 ; element type tag"),
"a name-forwarded format-string value must be written with TAG_STRING"
);
}
#[test]
fn unknowable_value_append_widens() {
let asm = compile_to_asm(
"a value called tally is 5.\n\
a list called items is [].\n\
append \"hello\" to items.\n\
append tally to items.\n\
print element 1 of items.\n\
print element 2 of items.\n",
);
assert!(
asm.contains("mixp_"),
"an unknowable (value) append must widen the list to Mixed"
);
}
#[test]
fn type_predicate_mixed_emits_runtime_compare() {
let asm = compile_to_asm(
"a list called m is [1, \"x\"].\n\
For each item in m, if item is a text, print item.\n",
);
assert!(
asm.contains("cmp r11, 1"),
"a mixed-element type predicate must compare the runtime tag against TAG_STRING"
);
assert!(
asm.contains("movzx r11, byte [rbp-"),
"the for-each variable's tag must be loaded from its shadow slot"
);
}
#[test]
fn mangle_symbol_produces_c_identifiers() {
use super::mangle_symbol;
assert_eq!(mangle_symbol("greet user"), "greet_user");
assert_eq!(mangle_symbol("my.helper"), "my_helper");
assert_eq!(mangle_symbol("flags_0.1_hasflag"), "flags_0_1_hasflag");
assert_eq!(mangle_symbol("parse-line"), "parse_line");
assert_eq!(mangle_symbol("add%"), "add_");
assert_eq!(mangle_symbol("2fast"), "_2fast");
assert_eq!(mangle_symbol("already_fine"), "already_fine");
}
#[test]
fn mangle_library_symbol_joins_three_components() {
use super::mangle_library_symbol;
assert_eq!(
mangle_library_symbol("mathkit", "1.0", "add two numbers"),
"mathkit_1_0_add_two_numbers"
);
assert_eq!(
mangle_library_symbol("my.lib", "2.0", "greet"),
"my_lib_2_0_greet"
);
assert_eq!(
mangle_library_symbol("lib", "1", "greet"),
"lib_1_greet"
);
}
#[test]
fn shared_lib_mangles_exported_labels_by_library_and_version() {
let src = "\
Library mathkit version \"1.0\".\n\
To 'add two numbers' with a number called n.\n Return n add 2.\n\
To greet.\n Print \"hello from libmath\".\n\
To makebuf.\n Create a buffer called b.\n Append \"hello\" to b.\n Return b's size.\n";
let asm = compile_to_asm_shared(src);
assert!(
asm.contains("mathkit_1_0_add_two_numbers:"),
"the 'add two numbers' definition must emit the mangled label"
);
assert!(
asm.contains("mathkit_1_0_greet:"),
"the 'greet' definition must emit the mangled label"
);
assert!(
asm.contains("mathkit_1_0_makebuf:"),
"the 'makebuf' definition must emit the mangled label"
);
assert!(
asm.contains("global mathkit_1_0_add_two_numbers:function"),
"the mangled label must be the exported symbol, not the bare name"
);
assert!(
asm.contains("global mathkit_1_0_greet:function"),
"greet must be exported under its mangled name"
);
assert!(
asm.contains("global mathkit_1_0_makebuf:function"),
"makebuf must be exported under its mangled name"
);
assert!(
!asm.contains("\nadd_two_numbers:"),
"the bare label must not be defined alongside the mangled one"
);
assert!(
!asm.contains("global greet:function"),
"the bare 'greet' must not be exported"
);
}
#[test]
fn shared_lib_call_site_targets_mangled_label() {
let src = "\
Library mathkit version \"1.0\".\n\
To double with a number called n.\n Return a number, n add n.\n\
To run.\n Print double of 21.\n";
let asm = compile_to_asm_shared(src);
assert!(
asm.contains("call mathkit_1_0_double"),
"an intra-library call must target the mangled label, not the bare name"
);
assert!(
!asm.contains("call double\n") && !asm.contains("call double "),
"the bare name must not be the call target in shared mode"
);
}
#[test]
fn two_versions_of_one_library_coexist_in_one_unit() {
let src = "\
Library flags version \"0.1\".\n\
To hasflag with a number called n.\n Return n add 1.\n\
Library flags version \"1.0\".\n\
To hasflag with a number called n.\n Return n add 100.\n";
let asm = compile_to_asm_shared(src);
assert!(
asm.contains("flags_0_1_hasflag:"),
"version 0.1 must emit its own mangled label"
);
assert!(
asm.contains("flags_1_0_hasflag:"),
"version 1.0 must emit its own mangled label"
);
assert!(
asm.contains("global flags_0_1_hasflag:function"),
"version 0.1 must be exported under its mangled name"
);
assert!(
asm.contains("global flags_1_0_hasflag:function"),
"version 1.0 must be exported under its mangled name"
);
assert_eq!(
asm.matches("\nflags_0_1_hasflag:").count(),
1,
"version 0.1 label defined exactly once"
);
assert_eq!(
asm.matches("\nflags_1_0_hasflag:").count(),
1,
"version 1.0 label defined exactly once"
);
}
#[test]
fn two_version_call_resolves_within_its_own_library() {
let src = "\
Library flags version \"0.1\".\n\
To hasflag with a number called n.\n Return n add 1.\n\
To call0.\n Return hasflag of 5.\n\
Library flags version \"1.0\".\n\
To hasflag with a number called n.\n Return n add 100.\n\
To call1.\n Return hasflag of 5.\n";
let asm = compile_to_asm_shared(src);
assert!(
asm.contains("call flags_0_1_hasflag"),
"a call in the 0.1 library must target flags_0_1_hasflag"
);
assert!(
asm.contains("call flags_1_0_hasflag"),
"a call in the 1.0 library must target flags_1_0_hasflag"
);
}
fn first_print_after(asm: &str, anchor: &str) -> &'static str {
let i = asm
.find(anchor)
.unwrap_or_else(|| panic!("anchor {:?} not found in asm", anchor));
let rest = &asm[i..];
let int = rest.find("PRINT_INT ");
let cstr = rest.find("PRINT_CSTR ");
match (int, cstr) {
(Some(a), Some(b)) => {
if a < b {
"PRINT_INT"
} else {
"PRINT_CSTR"
}
}
(Some(_), None) => "PRINT_INT",
(None, Some(_)) => "PRINT_CSTR",
(None, None) => panic!("no PRINT_* found after anchor {:?}", anchor),
}
}
#[test]
fn two_versions_differing_signatures_resolve_per_library() {
let src = "\
Library sig version \"0.1\".\n\
To 'get' with a number called n.\n Return a number, n add 1.\n\
To useit with a number called n.\n Print 'get' of n.\n\n\
Library sig version \"1.0\".\n\
To 'get' with a number called n.\n Return a text, \"hello\".\n\
To useit2 with a number called n.\n Print 'get' of n.\n";
let asm = compile_to_asm_shared(src);
assert!(
asm.contains("call sig_0_1_get"),
"a call in the 0.1 library must target sig_0_1_get"
);
assert!(
asm.contains("call sig_1_0_get"),
"a call in the 1.0 library must target sig_1_0_get"
);
assert_eq!(
first_print_after(&asm, "call sig_0_1_get"),
"PRINT_INT",
"0.1's get returns a number, so its result must print as PRINT_INT"
);
assert_eq!(
first_print_after(&asm, "call sig_1_0_get"),
"PRINT_CSTR",
"1.0's get returns text, so its result must print as PRINT_CSTR"
);
}
fn compile_shared_with_libs(source: &str) -> (Vec<LibBlock>, Vec<String>) {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens).with_source("lib_test.vox", source);
let mut program = parser
.parse()
.expect("shared test snippet should parse cleanly");
let mut analyzer = Analyzer::new()
.with_source("lib_test.vox", source)
.with_shared_mode(true);
analyzer.analyze(&mut program);
assert!(
analyzer.errors.is_empty(),
"shared test snippet should analyze cleanly, got: {:?}",
analyzer.errors
);
let mut gen = CodeGenerator::new();
gen.set_shared_lib_mode(true);
gen.generate(&program);
(gen.library_blocks().to_vec(), gen.exported_functions().to_vec())
}
#[test]
fn lib_file_two_version_round_trip() {
let src = "\
Library flags version \"0.1\".\n\
To hasflag with a number called n.\n Return a number, n add 1.\n\
Library flags version \"1.0\".\n\
To hasflag with a number called n.\n Return a number, n add 100.\n";
let (blocks, exports) = compile_shared_with_libs(src);
assert_eq!(blocks.len(), 2, "two inputs -> two Library blocks in one .lib");
assert_eq!(blocks[0].lib, "flags");
assert_eq!(blocks[0].version, "0.1");
assert_eq!(blocks[1].lib, "flags");
assert_eq!(blocks[1].version, "1.0");
let toc_mangled: Vec<String> = blocks
.iter()
.flat_map(|b| {
b.funcs
.iter()
.map(|f| super::mangle_library_symbol(&b.lib, &b.version, &f.name))
})
.collect();
let mut got = toc_mangled.clone();
got.sort();
let mut exp = exports.clone();
exp.sort();
assert_eq!(
got, exp,
"ToC mangled names must match exported_functions one-for-one"
);
let lib = super::render_lib_file(&blocks, "libflags.so");
let expected = "Library flags version \"0.1\".\nLocation \"./libflags.so\".\n\nTable of Contents:\n To hasflag with a number called n, returning a number.\n\nLibrary flags version \"1.0\".\nLocation \"./libflags.so\".\n\nTable of Contents:\n To hasflag with a number called n, returning a number.\n";
assert_eq!(lib, expected, "emitted .lib must match the normative format");
}
#[test]
fn lib_file_return_first_statement_still_carries_return_type() {
let src = "\
Library ga version \"1.0\".\n\
To ga with a number called x.\n Return a number, x.\n";
let (blocks, _exports) = compile_shared_with_libs(src);
let lib = super::render_lib_file(&blocks, "libga.so");
assert!(
lib.contains("To ga with a number called x, returning a number."),
"Return as the first statement must still record its return type; got:\n{}",
lib
);
}
#[test]
fn lib_file_return_after_other_statement_carries_return_type() {
let src = "\
Library gb version \"1.0\".\n\
To gbnum with a number called x.\n a number called y is x add x.\n Return a number, y.\n\
To gbtext with a text called s.\n a text called t is s.\n Return a text, t.\n\
To gbbool with a number called x.\n a boolean called ok is true.\n Return a boolean, ok.\n";
let (blocks, _exports) = compile_shared_with_libs(src);
let lib = super::render_lib_file(&blocks, "libgb.so");
assert!(
lib.contains("To gbnum with a number called x, returning a number."),
"a number return after a preceding statement must not evaporate to void; got:\n{}",
lib
);
assert!(
lib.contains("To gbtext with a text called s, returning a text."),
"a text return after a preceding statement must not evaporate to void; got:\n{}",
lib
);
assert!(
lib.contains("To gbbool with a number called x, returning a boolean."),
"a boolean return after a preceding statement must not evaporate to void; got:\n{}",
lib
);
}
#[test]
fn lib_file_signatures_carry_params_names_types_and_return() {
let src = "\
Library m version \"2.0\".\n\
To f with a number called aa and a text called s and a value called v.\n Return a value, v.\n";
let (blocks, _exports) = compile_shared_with_libs(src);
assert_eq!(blocks.len(), 1);
let lib = super::render_lib_file(&blocks, "libm.so");
assert!(
lib.contains("To f with a number called aa and a text called s and a value called v, returning a value."),
"multi-param entry joined with ` and `, value param/return by type name only; got:\n{}",
lib
);
}
#[test]
fn lib_file_parameterless_and_void_return_omits_clauses() {
let src = "\
Library m version \"1.0\".\n\
To greet.\n Print \"hi\".\n\n\
To makebuf.\n Return a number, 7.\n";
let (blocks, _exports) = compile_shared_with_libs(src);
let lib = super::render_lib_file(&blocks, "libm.so");
assert!(
lib.contains("To greet."),
"a parameterless void-return function reads `To greet.`; got:\n{}",
lib
);
assert!(
lib.contains("To makebuf, returning a number."),
"a parameterless function with a return reads `To makebuf, returning a number.`; got:\n{}",
lib
);
}
#[test]
fn bodyless_function_does_not_absorb_successor() {
let src = "\
Library toc version \"1.0\".\n\
To aa. Return a number, 1.\n\
To greet.\n\
To c. Return a number, 3.\n";
let (blocks, exports) = compile_shared_with_libs(src);
assert_eq!(blocks.len(), 1);
let names: Vec<&str> = blocks[0].funcs.iter().map(|f| f.name.as_str()).collect();
assert_eq!(
names, vec!["aa", "greet", "c"],
"a bodyless `greet` must not absorb `c` into its body"
);
let toc_mangled: Vec<String> = blocks[0]
.funcs
.iter()
.map(|f| super::mangle_library_symbol(&blocks[0].lib, &blocks[0].version, &f.name))
.collect();
let mut got = toc_mangled.clone();
got.sort();
let mut exp = exports.clone();
exp.sort();
assert_eq!(got, exp, "ToC count must equal exported function count");
}
#[test]
fn non_shared_builds_keep_plain_labels() {
let src = "\
To greet.\n Print \"hi\".\n\
To double with a number called n.\n Return a number, n add n.\n\
To run.\n Print double of 21.\n";
let asm = compile_to_asm(src);
assert!(
asm.contains("greet:"),
"non-shared builds keep the plain mangled label"
);
assert!(
asm.contains("call double"),
"non-shared call sites target the plain mangled name"
);
assert!(
!asm.contains("_1_0_"),
"no library/version mangling outside shared mode"
);
assert!(
!asm.contains(":function"),
"the :function export tag is shared-mode only"
);
}
#[test]
fn unprovable_guard_never_suppresses_a_list_tag() {
let asm = compile_to_asm(
"a list called one is [1, 2].\n\
a list called two is [3].\n\
set two to one.\n\
a list called outer is [].\n\
append two to outer.\n",
);
assert!(
asm.contains("mov edx, 4 ; element type tag"),
"appending a list must write TAG_LIST even when the pre-scan could \
not prove the alias's contents"
);
}
#[test]
fn type_predicate_never_compares_an_unset_r11() {
let asm = compile_to_asm(
"To opaque with a number called n.\n Return n add 1.\n\
if opaque of 4 is a text, print \"t\".\n",
);
assert!(
!asm.contains("cmp r11,"),
"a tagless operand must not be compared against r11"
);
}
#[test]
fn type_predicate_on_unprovable_scalar_uses_declared_type() {
let asm = compile_to_asm(
"a list called m is [\"a\", \"b\"].\n\
append 42 to m.\n\
a text called s is element 3 of m.\n\
print \"sep\".\n\
if s is a text, print \"t\".\n",
);
assert!(
!asm.contains("cmp r11,"),
"an unprovable scalar predicate must fold, not compare a stale r11"
);
}
#[test]
fn type_predicate_static_folds() {
let asm_true = compile_to_asm(
"a number called x is 5.\nif x is a number, print \"n\".\n",
);
assert!(
!asm_true.contains("cmp r11,"),
"a statically-true predicate must fold (no runtime tag compare)"
);
let asm_false = compile_to_asm(
"a number called x is 5.\nif x is a text, print \"t\".\n",
);
assert!(
!asm_false.contains("cmp r11,"),
"a statically-false predicate must fold (no runtime tag compare)"
);
assert!(
asm_false.contains("statically false"),
"a statically-false predicate must emit a fold-time jump to the false label"
);
}
#[test]
fn type_predicate_result_appends_tagged_boolean() {
let asm = compile_to_asm(
"a list called m is [1, 2, 3].\n\
a list called flags is [].\n\
For each item in m, append item is a number to flags.\n\
For each f in flags, if f is a boolean, print \"B\".\n",
);
assert!(
asm.contains("mov edx, 3"),
"an appended predicate result must be tagged TAG_BOOLEAN (edx=3)"
);
assert!(
!asm.contains("mixp_"),
"a list of predicate results must not widen to mixed"
);
}
#[test]
fn value_param_carries_tag_inbound() {
let asm = compile_to_asm(
"To describe with a value called v.\n\
If v is a number, print \"N\". Otherwise print \"T\".\n\
a list called m is [1, \"two\"].\n\
For each item in m, describe of item.\n",
);
assert!(
asm.contains("value param tag word"),
"a value param must push a 2nd (tag) word at the call site"
);
assert!(
asm.contains("param value tag"),
"the callee must store the inbound value tag byte to a shadow slot"
);
assert!(
asm.contains("cmp r11, 0"),
"the `v is a number` predicate must compare the loaded tag to 0"
);
}
#[test]
fn value_return_leaves_tag_in_r11() {
let asm = compile_to_asm(
"To id with a value called v. Return a value, v.\n\
a list called m is [1, \"two\"].\n\
a list called out is [].\n\
For each item in m, append id of item to out.\n",
);
assert!(
asm.contains("value tag (shadow slot)\n push rax ; save return value"),
"the return path must load the value tag into r11 before the epilogue"
);
assert!(
!asm.contains("pop r11"),
"the value return tag rides in r11 across leave;ret with no spill"
);
}
#[test]
fn value_param_two_words_in_call() {
let asm = compile_to_asm(
"To f with a number called aa and a number called b and \
a number called c and a number called d and a number called \
e and a value called v.\n\
If v is a text, print \"T\". Otherwise print \"N\".\n\
f of 1 and 2 and 3 and 4 and 5 and \"hi\".\n",
);
assert!(
asm.contains("value param tag word"),
"a value argument must push a tag word in addition to its payload"
);
assert!(
asm.contains("pop r9") && asm.contains("pop rdi"),
"the 6 register words must be popped into rdi..r9"
);
assert!(
asm.contains("align stack before call"),
"a 7-word call (1 stack word) must pad the stack before the call"
);
assert!(
asm.contains("add rsp, 16"),
"cleanup must release the stack word plus the alignment pad"
);
}
#[test]
fn append_fresh_mixed_element_keeps_tag() {
let asm = compile_to_asm(
"To id with a value called v. Return a value, v.\n\
a list called m is [1, \"two\"].\n\
a list called out is [].\n\
For each item in m, append id of item to out.\n",
);
assert!(
asm.contains("mov edx, r11d"),
"appending a value-returning call must forward its tag from r11"
);
assert!(
!asm.contains("xor edx, edx"),
"the value-append must not zero the tag (the 3f latent-bug fix)"
);
}
#[test]
fn nested_list_literal_tags_slot_4() {
let asm = compile_to_asm(
"a list called nested is [1, [2, 3], \"four\"].\n\
print element 2 of nested.\n",
);
assert!(
asm.contains("4 ; slot 2 type tag"),
"a nested list literal element's slot must carry tag 4 (LIST)"
);
assert!(
asm.contains("mixp_"),
"a mixed-list element read must use the mixed print dispatch"
);
assert!(
asm.contains("cmp r11, 4"),
"the mixed dispatch must branch on tag 4 (LIST)"
);
assert!(
asm.contains("call _list_print"),
"the tag-4 branch must recurse into _list_print"
);
}
#[test]
fn homogeneous_list_of_lists_not_mixed() {
let asm = compile_to_asm(
"a list called lol is [[1, 2], [3, 4]].\n\
for each row in lol, print row.\n",
);
assert!(
!asm.contains("mixp_"),
"a homogeneous list-of-lists must not widen to mixed"
);
assert!(
asm.contains("call _list_print"),
"a list-typed for-each loop var must print via _list_print"
);
}
#[test]
fn is_a_list_predicate_compiles_to_cmp_4() {
let asm = compile_to_asm(
"a list called m is [1, [2, 3], \"x\"].\n\
if element 2 of m is a list, print \"L\".\n",
);
assert!(
asm.contains("cmp r11, 4"),
"`is a list` on a mixed element must compare the runtime tag to 4"
);
let asm = compile_to_asm(
"a list called xs is [1, 2, 3].\n\
if xs is a number\n\
print \"yes\"\n\
otherwise\n\
print \"no\".\n",
);
assert!(
asm.contains("is a number statically false (static tag 4)"),
"a static list (tag 4) must fold `is a number` to false"
);
assert!(
!asm.contains("cmp r11, 4"),
"a static list must not emit a runtime tag compare"
);
}
#[test]
fn append_list_value_forwards_tag_4() {
let asm = compile_to_asm(
"a list called inner is [9, 8].\n\
a list called outer is [].\n\
append inner to outer.\n",
);
assert!(
asm.contains("mov edx, 4 ; element type tag"),
"appending a list value must forward tag 4 (LIST)"
);
assert!(
!asm.contains("xor edx, edx"),
"appending a list value must not fall back to the integer tag"
);
}
#[test]
fn list_print_has_depth_guard() {
let list_asm = include_str!("../../coreasm/x86_64/list.asm");
assert!(
list_asm.contains("%define LIST_TAG_LIST 4"),
"_list_print must define the LIST tag constant"
);
assert!(
list_asm.contains("cmp qword [rel _print_depth], 64"),
"_list_print must cap recursion at depth 64 (shared _print_depth, stage 1e2)"
);
assert!(
list_asm.contains("mov qword [rel _last_error], 1"),
"the depth-guard path must set the error flag"
);
assert!(
list_asm.contains("je .lp_list") && list_asm.contains("call _list_print"),
"_list_print must recurse on the LIST tag"
);
}
#[test]
fn map_literal_emits_map_insert() {
let asm = compile_to_asm("a map called m is {\"a\": 1, \"b\": 2}.\n");
assert!(
asm.contains("call _map_new"),
"a map literal must allocate via _map_new"
);
assert_eq!(
asm.matches("call _map_insert").count(),
2,
"expected one _map_insert per pair"
);
assert!(
asm.contains("%include \"coreasm/x86_64/map.asm\""),
"map usage must include map.asm"
);
}
#[test]
fn map_literal_empty_emits_map_new() {
let asm = compile_to_asm("a map called m is {}.\nprint m.\n");
assert!(
asm.contains("call _map_new"),
"an empty map literal must still allocate via _map_new"
);
assert_eq!(
asm.matches("call _map_insert").count(),
0,
"an empty map literal must not insert anything"
);
assert!(
asm.contains("call _map_print"),
"printing a map must route to _map_print"
);
}
#[test]
fn is_a_map_compiles_to_cmp_5() {
let asm = compile_to_asm(
"for each item in [{\"a\": 1}, 2]\n if item is a map, print \"M\".\n",
);
assert!(
asm.contains("cmp r11, 5"),
"`is a map` on a runtime-tagged value must compare against tag 5"
);
}
#[test]
fn is_a_map_folds_on_static_map() {
let asm = compile_to_asm(
"a map called m is {\"a\": 1}.\nif m is a map, print \"yes\".\n",
);
assert!(
!asm.contains("cmp r11, 5"),
"`is a map` on a static map variable must fold (no runtime cmp)"
);
assert!(
asm.contains("PRINT_STR") || asm.contains("PRINT_CSTR"),
"the folded-true branch's print must still be emitted"
);
}
#[test]
fn map_access_emits_map_lookup_and_sets_r11() {
let asm = compile_to_asm("a map called m is {\"a\": 1}.\nprint m's \"a\".\n");
assert!(
asm.contains("call _map_lookup"),
"map key access must call _map_lookup"
);
assert!(
asm.contains("cmp r11, 1"),
"map access print must dispatch on the r11 tag"
);
}
#[test]
fn map_missing_key_emits_last_error_path() {
let asm = compile_to_asm(
"a map called m is {\"a\": 1}.\nprint m's \"nope\".\non error print \"miss\".\n",
);
assert!(asm.contains("call _map_lookup"));
assert!(
asm.contains("_last_error"),
"the on-error handler must reference _last_error"
);
}
#[test]
fn map_print_dispatch_tag_5() {
let asm = compile_to_asm(
"To 'show' with a value called v.\n print v.\n\na map called m is {\"a\": 1}.\n'show' of m.\n",
);
assert!(
asm.contains("cmp r11, 5") && asm.contains("call _map_print"),
"mixed print dispatch must branch on tag 5 to _map_print"
);
}
#[test]
fn map_asm_has_fnv_constants() {
let map_asm = include_str!("../../coreasm/x86_64/map.asm");
assert!(
map_asm.contains("0xcbf29ce484222325"),
"map.asm must define the FNV-1a 64-bit offset basis"
);
assert!(
map_asm.contains("0x100000001b3"),
"map.asm must define the FNV-1a 64-bit prime"
);
}
#[test]
fn map_print_depth_guard_shared() {
let list_asm = include_str!("../../coreasm/x86_64/list.asm");
let map_asm = include_str!("../../coreasm/x86_64/map.asm");
assert!(
!list_asm.contains("_list_print_depth"),
"list.asm must no longer reference the old _list_print_depth"
);
assert!(
list_asm.contains("_print_depth"),
"list.asm must reference the shared _print_depth"
);
assert!(
map_asm.contains("_print_depth"),
"map.asm must reference the shared _print_depth"
);
}
#[test]
fn homogeneous_map_values_dont_widen() {
let asm = compile_to_asm("a map called m is {\"a\": 1, \"b\": 2}.\nprint m.\n");
assert!(
asm.contains("call _map_print"),
"whole-map print must route to _map_print"
);
assert!(
!asm.contains("mixp_"),
"a homogeneous whole-map print must not emit mixp_ dispatch"
);
}
#[test]
fn keys_values_sets_uses_lists() {
let asm = compile_to_asm("a map called m is {\"a\": 1}.\nprint m's keys.\n");
assert!(
asm.contains("%include \"coreasm/x86_64/map.asm\""),
"keys/values must include map.asm"
);
assert!(
asm.contains("%include \"coreasm/x86_64/list.asm\""),
"keys/values must also include list.asm (they build a list)"
);
assert!(
asm.contains("call _map_keys"),
"map's keys must call _map_keys"
);
}
#[test]
fn nothing_lit_emits_tag_6() {
let asm = compile_to_asm("a list called xs is [1, nothing, 2].\n");
assert!(
asm.contains("xor rax, rax ; nothing literal, payload 0"),
"a nothing literal must emit payload 0 with the nothing-literal comment"
);
assert!(
asm.contains(", 6 ; slot 2 type tag"),
"the nothing list element's slot must carry tag 6 (TAG_NOTHING)"
);
}
#[test]
fn is_nothing_emits_tag_compare() {
let asm = compile_to_asm(
"To check with a value called v.\n\
If v is nothing, print \"y\".\n\
check of nothing.\n",
);
assert!(
asm.contains("cmp r11, 6"),
"`is nothing` on a value must compare the runtime tag against 6"
);
assert!(
!asm.contains("cmp rax, rbx"),
"`is nothing` must NOT use the numeric payload compare (0 is nothing must be false)"
);
let asm_fold = compile_to_asm("a number called n is 0.\nif n is nothing, print \"bug\".\n");
assert!(
asm_fold.contains("is not nothing folded (static tag 0)"),
"a static integer (tag 0) must fold `is nothing` to false"
);
assert!(
!asm_fold.contains("cmp r11, 6"),
"a statically-folded `is nothing` must not emit a runtime tag compare"
);
}
#[test]
fn print_dispatch_has_nothing_arm() {
let asm = compile_to_asm(
"To 'show' with a value called v.\n print v.\n\n\
a map called m is {\"k\": nothing}.\n'show' of m's \"k\".\n",
);
assert!(
asm.contains("cmp r11, 6") && asm.contains("mixp_nothing"),
"mixed print dispatch must branch on tag 6 to a nothing arm"
);
let asm_lit = compile_to_asm("print nothing.\n");
assert!(
asm_lit.contains("db 'nothing'") || asm_lit.contains("db \"nothing\""),
"`print nothing.` must materialize a `nothing` rodata string"
);
assert!(
!asm_lit.contains("PRINT_INT"),
"`print nothing.` must not fall through to PRINT_INT"
);
}
#[test]
fn list_and_map_print_have_nothing_arms() {
let list_asm = include_str!("../../coreasm/x86_64/list.asm");
assert!(
list_asm.contains("%define LIST_TAG_NOTHING 6"),
"list.asm must define LIST_TAG_NOTHING (6)"
);
assert!(
list_asm.contains("cmp r8, LIST_TAG_NOTHING") && list_asm.contains(".lp_nothing:"),
"_list_print must dispatch the nothing tag to a .lp_nothing label"
);
assert!(
list_asm.contains("%define LIST_TAG_MAP 5")
&& list_asm.contains(".lp_map:"),
"_list_print must also carry the map (tag 5) arm closed in 1e3"
);
let map_asm = include_str!("../../coreasm/x86_64/map.asm");
assert!(
map_asm.contains("%define MAP_TAG_NOTHING 6"),
"map.asm must define MAP_TAG_NOTHING (6)"
);
assert!(
map_asm.contains("cmp r8, MAP_TAG_NOTHING") && map_asm.contains(".mp_nothing:"),
"_map_print must dispatch the nothing tag to a .mp_nothing label"
);
}
#[test]
fn nothing_keyword_reserved() {
use crate::lexer::{Lexer, Token};
let toks: Vec<Token> = Lexer::new("nothing null nil empty")
.tokenize()
.into_iter()
.map(|ti| ti.token)
.filter(|t| !matches!(t, Token::EOF))
.collect();
assert_eq!(toks.len(), 4, "the four words must each produce one token");
assert!(toks.iter().all(|t| matches!(t, Token::Nothing | Token::Empty)),
"nothing/null/nil -> Token::Nothing; empty -> Token::Empty");
assert_eq!(toks[0], Token::Nothing);
assert_eq!(toks[1], Token::Nothing);
assert_eq!(toks[2], Token::Nothing);
assert_eq!(toks[3], Token::Empty);
assert_eq!(Token::Nothing.as_keyword(), Some("nothing"));
assert_eq!(Token::Empty.as_keyword(), Some("empty"));
assert_eq!(Token::string_is_keyword("nothing"), Some("nothing"));
assert_eq!(Token::string_is_keyword("null"), Some("nothing"));
assert_eq!(Token::string_is_keyword("nil"), Some("nothing"));
assert_eq!(Token::string_is_keyword("empty"), Some("empty"));
use crate::parser::Parser;
fn parse_snippet(src: &str) -> Result<(), String> {
let toks = Lexer::new(src).tokenize();
match Parser::new(toks).parse() {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
let bare = parse_snippet("a number called nothing is 1.");
assert!(bare.is_err(), "bare `nothing` as a name must be rejected");
assert!(
bare.unwrap_err().to_lowercase().contains("reserved"),
"the error must call `nothing` a reserved keyword"
);
let quoted = parse_snippet("a number called 'nothing' is 1.");
assert!(quoted.is_ok(), "quoted 'nothing' is a legal non-canonical name; only the bare form is reserved");
}
#[test]
fn plan_296_full_type_vocabulary_round_trips_both_positions() {
let cases: &[(&str, Type)] = &[
("number", Type::Integer),
("float", Type::Float),
("text", Type::String),
("boolean", Type::Boolean),
("list", Type::List(Box::new(Type::Unknown))),
("map", Type::Map(Box::new(Type::Unknown))),
("buffer", Type::Buffer),
("file", Type::File),
("time", Type::Time),
("timer", Type::Timer),
("value", Type::Value),
];
for (noun, expected) in cases {
let src = format!(
"Library matrix version \"1.0\".\nTo f with a {} called x.\n Return a {}, x.\n",
noun, noun
);
let (blocks, _exports) = compile_shared_with_libs(&src);
assert_eq!(blocks.len(), 1, "case {}: one Library block", noun);
assert_eq!(blocks[0].funcs.len(), 1, "case {}: one function", noun);
let f = &blocks[0].funcs[0];
assert_eq!(f.params[0].1, *expected, "case {}: emitted parameter type", noun);
assert_eq!(f.return_type, *expected, "case {}: emitted return type", noun);
let emitted = super::render_lib_file(&blocks, "libmatrix.so");
let reparsed = crate::lib_file::parse_lib_text(&emitted).unwrap_or_else(|e| {
panic!("case {}: emitted .lib failed to reparse: {}\n{}", noun, e, emitted)
});
let rf = &reparsed[0].funcs[0];
assert_eq!(rf.params[0].1, *expected, "case {}: round-tripped parameter type; emitted:\n{}", noun, emitted);
assert_eq!(rf.return_type, *expected, "case {}: round-tripped return type; emitted:\n{}", noun, emitted);
}
}
#[test]
fn plan_296_list_element_type_matrix_parameter() {
let cases: &[(&str, Type)] = &[
("number", Type::Integer),
("float", Type::Float),
("text", Type::String),
("boolean", Type::Boolean),
("file", Type::File),
("buffer", Type::Buffer),
("time", Type::Time),
("timer", Type::Timer),
("value", Type::Value),
];
for (noun, expected) in cases {
let src = format!(
"Library elemkit version \"1.0\".\nTo f with a {} called s and a list called out.\n Append s to out.\n",
noun
);
let (blocks, _exports) = compile_shared_with_libs(&src);
let f = &blocks[0].funcs[0];
let want = Type::List(Box::new(expected.clone()));
assert_eq!(
f.params[1].1, want,
"case {}: inferred parameter element type; got params {:?}", noun, f.params
);
let emitted = super::render_lib_file(&blocks, "libelem.so");
assert!(
emitted.contains(&format!("a list of {} called out", noun)),
"case {}: emitted .lib must render 'list of {}'; got:\n{}", noun, noun, emitted
);
let reparsed = crate::lib_file::parse_lib_text(&emitted).expect("reparse emitted .lib");
assert_eq!(
reparsed[0].funcs[0].params[1].1, want,
"case {}: round-tripped parameter element type", noun
);
}
}
#[test]
fn plan_296_list_element_type_matrix_return() {
let cases: &[(&str, Type)] = &[
("number", Type::Integer),
("float", Type::Float),
("text", Type::String),
("boolean", Type::Boolean),
("file", Type::File),
("buffer", Type::Buffer),
("time", Type::Time),
("timer", Type::Timer),
("value", Type::Value),
];
for (noun, expected) in cases {
let src = format!(
"Library elemretkit version \"1.0\".\nTo f with a {} called s.\n a list called out is [].\n Append s to out.\n Return a list, out.\n",
noun
);
let (blocks, _exports) = compile_shared_with_libs(&src);
let f = &blocks[0].funcs[0];
let want = Type::List(Box::new(expected.clone()));
assert_eq!(
f.return_type, want,
"case {}: inferred return element type; got {:?}", noun, f.return_type
);
let emitted = super::render_lib_file(&blocks, "libelemret.so");
assert!(
emitted.contains(&format!(", returning a list of {}", noun)),
"case {}: emitted .lib must render 'returning a list of {}'; got:\n{}", noun, noun, emitted
);
let reparsed = crate::lib_file::parse_lib_text(&emitted).expect("reparse emitted .lib");
assert_eq!(
reparsed[0].funcs[0].return_type, want,
"case {}: round-tripped return element type", noun
);
}
}
#[test]
fn plan_296_list_element_type_stays_unknown_on_disagreement_or_no_evidence() {
let disagreeing = "\
Library mixedkit version \"1.0\".\n\
To f with a text called s and a number called n and a list called out.\n \
Append s to out.\n Append n to out.\n";
let (blocks, _) = compile_shared_with_libs(disagreeing);
assert_eq!(blocks[0].funcs[0].params[2].1, Type::List(Box::new(Type::Unknown)));
let no_evidence = "\
Library emptykit version \"1.0\".\n\
To f with a list called out.\n Print \"noop\".\n";
let (blocks, _) = compile_shared_with_libs(no_evidence);
assert_eq!(blocks[0].funcs[0].params[0].1, Type::List(Box::new(Type::Unknown)));
}
#[test]
fn plan_303_local_declared_type_credits_element_parameter() {
let src = "\
Library elemkit version \"1.0\".\n\
To f with a list called out.\n \
a text called s is \"literal\".\n \
Append s to out.\n";
let (blocks, _) = compile_shared_with_libs(src);
assert_eq!(
blocks[0].funcs[0].params[0].1,
Type::List(Box::new(Type::String)),
"a local's declared text type must be credited"
);
}
#[test]
fn plan_303_call_declared_return_type_credits_element_parameter() {
let src = "\
Library elemkit version \"1.0\".\n\
To helper with a number called n.\n Return a text, \"hi\".\n\
To f with a list called out.\n \
Append helper of 1 to out.\n";
let (blocks, _) = compile_shared_with_libs(src);
assert_eq!(
blocks[0].funcs[1].params[0].1,
Type::List(Box::new(Type::String)),
"a same-library call's declared text return must be credited"
);
}
#[test]
fn plan_303_format_string_credits_element_parameter() {
let src = "\
Library elemkit version \"1.0\".\n\
To f with a list called out.\n \
a number called n is 7.\n \
Append \"n {n}\" to out.\n";
let (blocks, _) = compile_shared_with_libs(src);
assert_eq!(
blocks[0].funcs[0].params[0].1,
Type::List(Box::new(Type::String)),
"a format-string append must be credited as text"
);
}
#[test]
fn plan_303_newly_credited_shapes_in_return_position() {
let local_literal = "\
Library elemkit version \"1.0\".\n\
To f.\n a list called out is [].\n \
a text called s is \"literal\".\n Append s to out.\n \
Return a list, out.\n";
let (blocks, _) = compile_shared_with_libs(local_literal);
assert_eq!(
blocks[0].funcs[0].return_type,
Type::List(Box::new(Type::String)),
"a local's declared text type must be credited in return position"
);
let call_return = "\
Library elemkit version \"1.0\".\n\
To helper with a number called n.\n Return a text, \"hi\".\n\
To f.\n a list called out is [].\n Append helper of 1 to out.\n \
Return a list, out.\n";
let (blocks, _) = compile_shared_with_libs(call_return);
assert_eq!(
blocks[0].funcs[1].return_type,
Type::List(Box::new(Type::String)),
"a same-library call's declared return must be credited in return position"
);
let format_string = "\
Library elemkit version \"1.0\".\n\
To f.\n a list called out is [].\n a number called n is 7.\n \
Append \"n {n}\" to out.\n Return a list, out.\n";
let (blocks, _) = compile_shared_with_libs(format_string);
assert_eq!(
blocks[0].funcs[0].return_type,
Type::List(Box::new(Type::String)),
"a format-string append must be credited as text in return position"
);
}
#[test]
fn plan_303_function_call_return_type_scoped_per_library() {
let src = "\
Library liba version \"1.0\".\n\
To helper with a number called n.\n Return a number, n.\n\
To f with a list called out.\n Append helper of 1 to out.\n\n\
Library libb version \"1.0\".\n\
To helper with a number called n.\n Return a text, \"hi\".\n\
To g with a list called out.\n Append helper of 1 to out.\n";
let (blocks, _) = compile_shared_with_libs(src);
assert_eq!(blocks.len(), 2, "two Library blocks");
assert_eq!(
blocks[0].funcs[1].params[0].1,
Type::List(Box::new(Type::Integer)),
"liba's f must credit liba's own number-returning helper"
);
assert_eq!(
blocks[1].funcs[1].params[0].1,
Type::List(Box::new(Type::String)),
"libb's g must credit libb's own text-returning helper, not liba's"
);
}
#[test]
fn plan_303_local_declared_type_conflict_stays_unknown() {
let src = "\
Library elemkit version \"1.0\".\n\
To f with a boolean called cond and a list called out.\n \
If cond, a text called s is \"a\", append s to out. \
Otherwise, a number called s is 1, append s to out.\n";
let (blocks, _) = compile_shared_with_libs(src);
assert_eq!(
blocks[0].funcs[0].params[1].1,
Type::List(Box::new(Type::Unknown)),
"a local declared with conflicting types across branches must not be credited"
);
}
#[test]
fn stringy_vs_non_stringy_condition_never_dereferences() {
let asm = compile_to_asm(
"If \"abc\" is equal to 3.5 then, print \"a\". Otherwise, print \"b\".\n",
);
assert!(
!asm.contains("call _str_eq") && !asm.contains("call _mem_eq"),
"a stringy-vs-float mismatch must not reach the byte-comparison path"
);
}
#[test]
fn stringy_vs_non_stringy_expression_never_dereferences() {
let asm = compile_to_asm(
"To f.\n Return a boolean, \"abc\" is equal to 3.\n",
);
assert!(
!asm.contains("call _str_eq") && !asm.contains("call _mem_eq"),
"a stringy-vs-integer mismatch in expression position must not \
reach the byte-comparison path"
);
}
#[test]
fn both_stringy_equality_still_dereferences_correctly() {
let asm = compile_to_asm(
"a text called t is \"hi\".\nIf t is equal to \"hi\" then, print \"a\". Otherwise, print \"b\".\n",
);
assert!(
asm.contains("call _str_eq"),
"a text-vs-literal equality must still use byte comparison"
);
}
#[test]
fn b4_exact_fill_probe_is_removed_from_runtime() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("coreasm").join("x86_64").join("resource.asm");
let asm = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e));
assert!(!asm.contains("Probe for additional data using a scratch byte"),
"the consuming one-byte probe comment must be removed from resource.asm");
assert!(!asm.contains("mov rsi, -1"),
"the probe's lseek(fd, -1, SEEK_CUR) put-back must be removed from resource.asm");
assert!(!asm.contains(".no_more_data"),
"the old probe's .no_more_data branch must be removed from resource.asm");
assert!(asm.contains("fd_mode_table"), "fd_mode_table cache must be present");
assert!(asm.contains("fd_size_table"), "fd_size_table cache must be present");
assert!(asm.contains("S_IFREG"), "regular-file type check must be present");
assert!(asm.contains("SYS_FSTAT"), "fstat syscall constant must be present");
assert!(asm.contains(".exact_fit_success"),
"the seekability-aware exact-fit decision must be present");
}
#[test]
fn last_error_runtime_helpers_clear_on_success() {
let int_asm = include_str!("../../coreasm/x86_64/int.asm");
let float_asm = include_str!("../../coreasm/x86_64/float.asm");
let list_asm = include_str!("../../coreasm/x86_64/list.asm");
let map_asm = include_str!("../../coreasm/x86_64/map.asm");
let file_asm = include_str!("../../coreasm/x86_64/file.asm");
fn function_body_has_clear(asm: &str, name: &str) -> bool {
let label = format!("{}:", name);
let start = asm
.find(&label)
.unwrap_or_else(|| panic!("{} label not found", name));
let rest = &asm[start..];
let lines: Vec<&str> = rest.lines().collect();
let end = lines
.iter()
.position(|l| l.trim() == "ret")
.unwrap_or_else(|| panic!("{} has no ret", name));
lines[..end].join("\n").contains("mov qword [rel _last_error], 0")
}
fn macro_body_has_clear(asm: &str, name: &str) -> bool {
let open = format!("%macro {} 0", name);
let start = asm
.find(&open)
.unwrap_or_else(|| panic!("%macro {} not found", name));
let rest = &asm[start..];
let lines: Vec<&str> = rest.lines().collect();
let end = lines
.iter()
.position(|l| l.trim() == "%endmacro")
.unwrap_or_else(|| panic!("%endmacro for {} not found", name));
lines[..end].join("\n").contains("mov qword [rel _last_error], 0")
}
assert!(
function_body_has_clear(int_asm, "_parse_i64"),
"_parse_i64 must clear _last_error on success"
);
assert!(
function_body_has_clear(int_asm, "_parse_int_radix"),
"_parse_int_radix must clear _last_error on success"
);
assert!(
function_body_has_clear(int_asm, "_parse_i64_bounded"),
"_parse_i64_bounded must clear _last_error on success"
);
assert!(
function_body_has_clear(int_asm, "_parse_int_radix_bounded"),
"_parse_int_radix_bounded must clear _last_error on success"
);
assert!(
macro_body_has_clear(int_asm, "INT_DIV"),
"INT_DIV must clear _last_error on its non-zero-divisor path"
);
assert!(
macro_body_has_clear(int_asm, "INT_MOD"),
"INT_MOD must clear _last_error on its non-zero-divisor path"
);
assert!(
function_body_has_clear(float_asm, "_parse_f64"),
"_parse_f64 must clear _last_error on success"
);
assert!(
function_body_has_clear(float_asm, "_parse_f64_bounded"),
"_parse_f64_bounded must clear _last_error on success"
);
assert!(
function_body_has_clear(list_asm, "_list_print"),
"_list_print must clear _last_error on its normal return path"
);
assert!(
function_body_has_clear(map_asm, "_map_lookup"),
"_map_lookup must clear _last_error on a hit"
);
assert!(
function_body_has_clear(map_asm, "_map_print"),
"_map_print must clear _last_error on its normal return path"
);
assert!(
macro_body_has_clear(file_asm, "RECORD_WRITE_RESULT"),
"RECORD_WRITE_RESULT must clear _last_error on a complete write"
);
}
#[test]
fn write_macros_record_their_syscall_result() {
let file_asm = include_str!("../../coreasm/x86_64/file.asm");
for name in ["FILE_WRITE_STR", "FILE_WRITE_BUF", "FILE_WRITE_NEWLINE"] {
let open = format!("%macro {} ", name);
let start = file_asm
.find(&open)
.unwrap_or_else(|| panic!("%macro {} not found", name));
let rest = &file_asm[start..];
let end = rest
.find("%endmacro")
.unwrap_or_else(|| panic!("%endmacro for {} not found", name));
let body = &rest[..end];
assert!(
body.contains("syscall"),
"{} must issue a write syscall",
name
);
assert!(
body.contains("RECORD_WRITE_RESULT"),
"{} must record its write syscall's result in _last_error",
name
);
}
}
#[test]
fn printing_a_thing_bakes_its_fields_into_the_program() {
let asm = compile_to_asm(
"A thing called point has\n \
a number called x is 0,\n \
a number called y is 0.\n\
a point called origin.\n\
Print origin.\n",
);
assert!(
asm.contains("db '{', 0") && asm.contains("db '}', 0"),
"the braces around a printed thing are string constants: {}",
asm
);
assert!(
asm.contains("db 'x: ', 0") && asm.contains("db 'y: ', 0"),
"each field's name is a string constant, in definition order: {}",
asm
);
assert_eq!(
asm.matches("PRINT_INT rdi").count(),
2,
"one print per field, written out - not a loop over a descriptor"
);
assert!(
!asm.contains("call _map_print") && !asm.contains("call _list_print"),
"a thing prints map-STYLE, but through none of the map runtime"
);
}
#[test]
fn a_function_member_takes_no_part_in_printing() {
let asm = compile_to_asm(
"A thing called point has\n \
a function called 'placed at',\n \
a number called x is 0.\n\
To do the point's 'placed at', with a number called x.\n \
a point called plotted.\n \
Set plotted's x to x.\n \
Return a point, plotted.\n\
a point called origin.\n\
Print origin.\n",
);
assert!(
!asm.contains("db 'placed at: ', 0"),
"a manifest member is not one of the printed fields: {}",
asm
);
assert!(
asm.contains("db 'x: ', 0"),
"the data field is still printed: {}",
asm
);
}
#[test]
fn a_multi_word_field_name_prints_in_its_quotes() {
let asm = compile_to_asm(
"A thing called stamp has\n \
a number called 'day sent' is 25.\n\
a stamp called posted.\n\
Print posted.\n",
);
assert!(
asm.contains("db '', 39, 'day sent', 39, ': ', 0"),
"a multi-word field name keeps its quotes: {}",
asm
);
}
#[test]
fn comparing_two_things_expands_to_one_compare_per_slot() {
let asm = compile_to_asm(
"A thing called point has\n \
a number called x is 0,\n \
a number called y is 0.\n\
A thing called segment has\n \
a point called start,\n \
a point called end.\n\
a segment called span.\n\
a segment called 'the detour'.\n\
If span is 'the detour' then,\n \
Print \"same\".\n",
);
assert_eq!(
asm.matches("jne .things_differ").count(),
4,
"two segments are four scalar slots, compared one at a time: {}",
asm
);
assert!(
!asm.contains("call _mem_eq") && !asm.contains("call _str_eq"),
"a thing comparison calls nothing at runtime: {}",
asm
);
}
#[test]
fn a_float_field_is_compared_as_a_float() {
let asm = compile_to_asm(
"A thing called measurement has\n \
a float called celsius is 0.0.\n\
a measurement called 'the first sample'.\n\
a measurement called 'the second sample'.\n\
If 'the first sample' is 'the second sample' then,\n \
Print \"same\".\n",
);
assert!(
asm.contains("FLOAT_EQ"),
"a float field compares through FLOAT_EQ, not a bitwise cmp: {}",
asm
);
}
#[test]
fn a_buffer_reloads_its_destination_after_an_argv_property() {
let asm = compile_to_asm(
"a buffer called built is 64 bytes in size.\n\
copy \"{arguments's first}\" to built.\n",
);
let resolved_at = asm
.find("call _get_arg")
.expect("the argv property resolves through _get_arg");
let appended_at = asm
.find("call _buffer_append_cstr")
.expect("a text value appends through _buffer_append_cstr");
assert!(
resolved_at < appended_at,
"the value is resolved before it is appended: {}",
asm
);
assert!(
asm[resolved_at..appended_at].contains("mov rdi, [rbp-"),
"the destination buffer is reloaded into rdi between resolving \
the argument and appending it: {}",
&asm[resolved_at..appended_at]
);
}
#[test]
fn list_in_a_text_initializer_routes_to_the_shared_renderer() {
let asm = compile_to_asm(
"a list called flat is [1, 2, 3].\n\
a text called captured is \"{flat}\".\n\
print captured.\n",
);
assert!(
asm.contains("call _list_render_to_buffer"),
"a list in a text initializer must render through \
_list_render_to_buffer: {}",
asm
);
assert!(
!asm.contains("call _buffer_append_formatted_int"),
"a list part must never reach the integer formatter: {}",
asm
);
assert!(
asm.contains("%include \"coreasm/x86_64/list.asm\""),
"rendering a list into a buffer must include list.asm"
);
}
#[test]
fn map_in_a_buffer_copy_routes_to_the_shared_renderer() {
let asm = compile_to_asm(
"a map called person is {\"name\": \"Ada\"}.\n\
a buffer called sink is 64 bytes in size.\n\
copy \"{person}\" to sink.\n\
print sink.\n",
);
assert!(
asm.contains("call _map_render_to_buffer"),
"a map copied into a buffer must render through \
_map_render_to_buffer: {}",
asm
);
assert!(
!asm.contains("call _buffer_append_formatted_int"),
"a map part must never reach the integer formatter: {}",
asm
);
}
#[test]
fn quoted_list_name_in_print_routes_to_list_print() {
let asm = compile_to_asm(
"a list called 'the running total' is [1, 2].\n\
print \"{'the running total'}\".\n",
);
assert!(
asm.contains("call _list_print"),
"a quoted list name in a format string must render through \
_list_print: {}",
asm
);
assert!(
!asm.contains("PRINT_INT rdi"),
"a quoted list name must not print the list pointer as an \
integer: {}",
asm
);
}
#[test]
fn a_pad_width_past_i32_max_still_reaches_the_padded_printer() {
let asm = compile_to_asm("a number called n is 255.\nPrint \"{n:2147483648}\".\n");
assert!(
asm.contains("PRINT_INT_PADDED rdi, 2147483648"),
"the width the author wrote is the width emitted: {}",
asm
);
}
#[test]
fn a_pad_width_at_i32_max_still_reaches_the_padded_printer() {
let asm = compile_to_asm("a number called n is 255.\nPrint \"{n:2147483647}\".\n");
assert!(
asm.contains("PRINT_INT_PADDED rdi, 2147483647"),
"the width the author wrote is the width emitted: {}",
asm
);
}
#[test]
fn a_precision_past_i32_max_still_reaches_the_precision_printer() {
let asm = compile_to_asm("a float called f is 3.5.\nPrint \"{f:.3000000000}\".\n");
assert!(
asm.contains("mov rdi, 3000000000")
&& asm.contains("call _print_float_precision"),
"the precision the author wrote is the precision emitted: {}",
asm
);
}
#[test]
fn a_count_too_large_to_hold_is_reported_not_dropped() {
let (spec, fault) = super::read_format_spec(Some("99999999999999999999"));
assert_eq!(spec.width, Some(super::FORMAT_MAX_COUNT));
assert_eq!(
fault,
Some(super::FormatSpecFault::WidthTooLarge(
"99999999999999999999".to_string()
))
);
let (spec, fault) = super::read_format_spec(Some(".99999999999999999999"));
assert_eq!(spec.precision, Some(super::FORMAT_MAX_COUNT));
assert_eq!(
fault,
Some(super::FormatSpecFault::PrecisionTooLarge(
"99999999999999999999".to_string()
))
);
}
#[test]
fn a_spec_that_is_not_a_count_is_not_a_fault() {
let (spec, fault) = super::read_format_spec(Some(".2z"));
assert_eq!(spec.precision, None);
assert_eq!(fault, None);
}
#[test]
fn extra_leading_zeros_still_leave_the_base_specifier() {
let (spec, fault) = super::read_format_spec(Some("004x"));
assert_eq!(spec.width, Some(4));
assert!(spec.zero_pad);
assert_eq!(spec.base, super::IntegerBase::HexLower);
assert_eq!(fault, None);
}