413 lines
14 KiB
Rust
413 lines
14 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
|
|
#[test]
|
|
fn array_fixture_emits_llvm_index_shape() {
|
|
let output = run_glagol(["../examples/array.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"compiler rejected array fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert!(
|
|
stdout.contains("define i32 @immediate_second()")
|
|
&& stdout.contains("insertvalue [3 x i32]")
|
|
&& stdout.contains("extractvalue [3 x i32]")
|
|
&& stdout.contains("%values.addr = alloca [3 x i32]")
|
|
&& stdout.contains("getelementptr inbounds [3 x i32]")
|
|
&& stdout.contains("load i32"),
|
|
"LLVM output did not contain expected array index shape\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(
|
|
!stdout.contains("immediate array index") && !stdout.contains("array local index"),
|
|
"compiler emitted test metadata into LLVM output\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn immediate_literal_array_index_evaluates_all_element_expressions_in_order() {
|
|
let fixture = write_fixture(
|
|
"immediate-literal-array-index-evaluation",
|
|
r#"(module main)
|
|
|
|
(fn first_value () -> i32
|
|
11)
|
|
|
|
(fn second_value () -> i32
|
|
22)
|
|
|
|
(fn third_value () -> i32
|
|
33)
|
|
|
|
(fn selected_immediate () -> i32
|
|
(index (array i32 (first_value) (second_value) (third_value)) 1))
|
|
|
|
(test "immediate literal index evaluates array"
|
|
(= (selected_immediate) 22))
|
|
|
|
(fn main () -> i32
|
|
(selected_immediate))
|
|
"#,
|
|
);
|
|
let fixture_arg = fixture.to_string_lossy().into_owned();
|
|
let output = run_glagol([fixture_arg.as_str()]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"compiler rejected immediate literal array index regression fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
|
|
let first_call = single_occurrence_index(&stdout, "call i32 @first_value()");
|
|
let second_call = single_occurrence_index(&stdout, "call i32 @second_value()");
|
|
let third_call = single_occurrence_index(&stdout, "call i32 @third_value()");
|
|
assert!(
|
|
first_call < second_call && second_call < third_call,
|
|
"LLVM output did not emit array element calls in source order\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(
|
|
stdout.contains("extractvalue [3 x i32]"),
|
|
"LLVM output did not select from the constructed aggregate\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
|
|
|
|
let run = run_glagol(["--run-tests", fixture_arg.as_str()]);
|
|
let run_stdout = String::from_utf8_lossy(&run.stdout);
|
|
let run_stderr = String::from_utf8_lossy(&run.stderr);
|
|
assert!(
|
|
run.status.success(),
|
|
"test runner rejected immediate literal array index regression fixture\nstdout:\n{}\nstderr:\n{}",
|
|
run_stdout,
|
|
run_stderr
|
|
);
|
|
assert_eq!(
|
|
run_stdout,
|
|
concat!(
|
|
"test \"immediate literal index evaluates array\" ... ok\n",
|
|
"1 test(s) passed\n",
|
|
),
|
|
"test runner output drifted"
|
|
);
|
|
assert!(
|
|
run_stderr.is_empty(),
|
|
"test runner wrote stderr:\n{}",
|
|
run_stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn array_fixture_runs_top_level_tests() {
|
|
let output = run_glagol(["--run-tests", "../examples/array.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"test runner rejected array fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert_eq!(
|
|
stdout,
|
|
concat!(
|
|
"test \"immediate array index\" ... ok\n",
|
|
"test \"array local index\" ... ok\n",
|
|
"2 test(s) passed\n",
|
|
),
|
|
"test runner output drifted"
|
|
);
|
|
assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn array_fixture_is_formatter_stable() {
|
|
let expected = fs::read_to_string("../tests/array.slo").expect("read fixture");
|
|
let output = run_glagol(["--format", "../tests/array.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"formatter rejected array fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert_eq!(stdout, expected, "formatter output drifted");
|
|
assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn array_fixture_prints_lowered_shape() {
|
|
let surface = run_glagol(["--inspect-lowering=surface", "../examples/array.slo"]);
|
|
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
|
|
let surface_stderr = String::from_utf8_lossy(&surface.stderr);
|
|
assert!(
|
|
surface.status.success(),
|
|
"surface lowering rejected array fixture\nstdout:\n{}\nstderr:\n{}",
|
|
surface_stdout,
|
|
surface_stderr
|
|
);
|
|
assert!(
|
|
surface_stdout.contains("array i32")
|
|
&& surface_stdout.contains("index")
|
|
&& surface_stdout.contains("local let values: (array i32 3)"),
|
|
"surface lowering output lost array shape\nstdout:\n{}",
|
|
surface_stdout
|
|
);
|
|
assert!(
|
|
surface_stderr.is_empty(),
|
|
"surface lowering wrote stderr:\n{}",
|
|
surface_stderr
|
|
);
|
|
|
|
let checked = run_glagol(["--inspect-lowering=checked", "../examples/array.slo"]);
|
|
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
|
|
let checked_stderr = String::from_utf8_lossy(&checked.stderr);
|
|
assert!(
|
|
checked.status.success(),
|
|
"checked lowering rejected array fixture\nstdout:\n{}\nstderr:\n{}",
|
|
checked_stdout,
|
|
checked_stderr
|
|
);
|
|
assert!(
|
|
checked_stdout.contains("array : (array i32 3)")
|
|
&& checked_stdout.contains("index : i32")
|
|
&& checked_stdout.contains("var values : (array i32 3)"),
|
|
"checked lowering output lost typed array shape\nstdout:\n{}",
|
|
checked_stdout
|
|
);
|
|
assert!(
|
|
checked_stderr.is_empty(),
|
|
"checked lowering wrote stderr:\n{}",
|
|
checked_stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn array_value_flow_fixture_emits_llvm_value_flow_and_bounds_shape() {
|
|
let output = run_glagol(["../examples/array-value-flow.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"compiler rejected array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert!(
|
|
stdout.contains("define [3 x i32] @make_values(i32 %base)")
|
|
&& stdout.contains("define i32 @first([3 x i32] %values)")
|
|
&& stdout.contains("define i32 @at([3 x i32] %values, i32 %i)")
|
|
&& stdout.contains("define [3 x i32] @echo([3 x i32] %values)")
|
|
&& stdout.contains("define i32 @parameter_local_copy([3 x i32] %values, i32 %i)")
|
|
&& stdout.contains("call [3 x i32] @make_values(i32 20)")
|
|
&& stdout.contains("call i32 @at([3 x i32]")
|
|
&& stdout.contains("load [3 x i32], ptr %values.addr"),
|
|
"LLVM output did not contain expected array value-flow shape\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(
|
|
stdout.contains("declare void @__glagol_array_bounds_trap()"),
|
|
"LLVM output did not declare the array bounds trap\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
let dynamic_index_function =
|
|
llvm_function(&stdout, "define i32 @at([3 x i32] %values, i32 %i)");
|
|
assert_contains_in_order(
|
|
dynamic_index_function,
|
|
&[
|
|
"icmp sge i32 %i, 0",
|
|
"icmp slt i32 %i, 3",
|
|
"br i1 %",
|
|
"array.index.trap",
|
|
"call void @__glagol_array_bounds_trap()",
|
|
"unreachable",
|
|
"array.index.ok",
|
|
"getelementptr inbounds [3 x i32]",
|
|
"i32 %i",
|
|
"load i32, ptr %",
|
|
],
|
|
"LLVM output did not emit bounds branch/trap before the dynamic access",
|
|
);
|
|
assert!(
|
|
!stdout.contains("array dynamic index") && !stdout.contains("array parameter local copy"),
|
|
"compiler emitted test metadata into LLVM output\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn array_value_flow_fixture_runs_top_level_tests() {
|
|
let output = run_glagol(["--run-tests", "../examples/array-value-flow.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"test runner rejected array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert_eq!(
|
|
stdout,
|
|
concat!(
|
|
"test \"array parameter value flow\" ... ok\n",
|
|
"test \"array dynamic index\" ... ok\n",
|
|
"test \"array local call value flow\" ... ok\n",
|
|
"test \"array parameter local copy\" ... ok\n",
|
|
"test \"array return call value flow\" ... ok\n",
|
|
"5 test(s) passed\n",
|
|
),
|
|
"test runner output drifted"
|
|
);
|
|
assert!(stderr.is_empty(), "test runner wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn array_value_flow_fixture_is_formatter_stable() {
|
|
let expected = fs::read_to_string("../tests/array-value-flow.slo").expect("read fixture");
|
|
let output = run_glagol(["--format", "../tests/array-value-flow.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"formatter rejected array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert_eq!(stdout, expected, "formatter output drifted");
|
|
assert!(stderr.is_empty(), "formatter wrote stderr:\n{}", stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn array_value_flow_fixture_prints_lowered_shape() {
|
|
let surface = run_glagol([
|
|
"--inspect-lowering=surface",
|
|
"../examples/array-value-flow.slo",
|
|
]);
|
|
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
|
|
let surface_stderr = String::from_utf8_lossy(&surface.stderr);
|
|
assert!(
|
|
surface.status.success(),
|
|
"surface lowering rejected array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
|
|
surface_stdout,
|
|
surface_stderr
|
|
);
|
|
assert!(
|
|
surface_stdout.contains("fn make_values(base: i32) -> (array i32 3)")
|
|
&& surface_stdout.contains("fn at(values: (array i32 3), i: i32) -> i32")
|
|
&& surface_stdout.contains("fn echo(values: (array i32 3)) -> (array i32 3)")
|
|
&& surface_stdout.contains("local let values: (array i32 3)")
|
|
&& surface_stdout.contains("index"),
|
|
"surface lowering output lost array value-flow shape\nstdout:\n{}",
|
|
surface_stdout
|
|
);
|
|
assert!(
|
|
surface_stderr.is_empty(),
|
|
"surface lowering wrote stderr:\n{}",
|
|
surface_stderr
|
|
);
|
|
|
|
let checked = run_glagol([
|
|
"--inspect-lowering=checked",
|
|
"../examples/array-value-flow.slo",
|
|
]);
|
|
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
|
|
let checked_stderr = String::from_utf8_lossy(&checked.stderr);
|
|
assert!(
|
|
checked.status.success(),
|
|
"checked lowering rejected array value-flow fixture\nstdout:\n{}\nstderr:\n{}",
|
|
checked_stdout,
|
|
checked_stderr
|
|
);
|
|
assert!(
|
|
checked_stdout.contains("fn make_values(base: i32) -> (array i32 3)")
|
|
&& checked_stdout.contains("fn at(values: (array i32 3), i: i32) -> i32")
|
|
&& checked_stdout.contains("call make_values : (array i32 3)")
|
|
&& checked_stdout.contains("local let values : unit")
|
|
&& checked_stdout.contains("index : i32"),
|
|
"checked lowering output lost typed array value-flow shape\nstdout:\n{}",
|
|
checked_stdout
|
|
);
|
|
assert!(
|
|
checked_stderr.is_empty(),
|
|
"checked lowering wrote stderr:\n{}",
|
|
checked_stderr
|
|
);
|
|
}
|
|
|
|
fn run_glagol<const N: usize>(args: [&str; N]) -> std::process::Output {
|
|
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
|
.args(args)
|
|
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
|
|
.output()
|
|
.expect("run glagol")
|
|
}
|
|
|
|
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
|
let mut path = std::env::temp_dir();
|
|
path.push(format!(
|
|
"glagol-array-index-{}-{}.slo",
|
|
name,
|
|
std::process::id()
|
|
));
|
|
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
|
path
|
|
}
|
|
|
|
fn single_occurrence_index(source: &str, needle: &str) -> usize {
|
|
let mut matches = source.match_indices(needle);
|
|
let (index, _) = matches
|
|
.next()
|
|
.unwrap_or_else(|| panic!("LLVM output omitted `{}`\nstdout:\n{}", needle, source));
|
|
assert!(
|
|
matches.next().is_none(),
|
|
"LLVM output contained `{}` more than once\nstdout:\n{}",
|
|
needle,
|
|
source
|
|
);
|
|
index
|
|
}
|
|
|
|
fn llvm_function<'a>(source: &'a str, signature: &str) -> &'a str {
|
|
let start = source
|
|
.find(signature)
|
|
.unwrap_or_else(|| panic!("LLVM output omitted `{}`\nstdout:\n{}", signature, source));
|
|
let tail = &source[start..];
|
|
let end = tail.find("\n}\n").unwrap_or_else(|| {
|
|
panic!(
|
|
"LLVM function `{}` was not closed\nstdout:\n{}",
|
|
signature, source
|
|
)
|
|
});
|
|
&tail[..end + "\n}\n".len()]
|
|
}
|
|
|
|
fn assert_contains_in_order(source: &str, needles: &[&str], message: &str) {
|
|
let mut cursor = 0;
|
|
for needle in needles {
|
|
let relative = source[cursor..]
|
|
.find(needle)
|
|
.unwrap_or_else(|| panic!("{}\nmissing `{}`\nsource:\n{}", message, needle, source));
|
|
cursor += relative + needle.len();
|
|
}
|
|
}
|