309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
use dever_backend_bridge::{LinkKind, Target, emit_object, link};
|
|
use std::fs;
|
|
|
|
#[path = "dever-tests/tests/support/llvm.rs"]
|
|
mod llvm;
|
|
use llvm::lower;
|
|
|
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
#[path = "dever-tests/tests/support/process.rs"]
|
|
mod process;
|
|
#[path = "dever-tests/tests/support/temp.rs"]
|
|
mod temp;
|
|
|
|
const AGGREGATE_SOURCE: &str = r#"type Pair {
|
|
enabled: Bool
|
|
amount: Float
|
|
count: Int
|
|
}
|
|
type Payload {
|
|
Value(value: Pair)
|
|
Empty
|
|
}
|
|
type Envelope {
|
|
Present(value: Payload)
|
|
Missing
|
|
}
|
|
increment(value: Int) (answer: Int) { answer = value + 1 }
|
|
apply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) }
|
|
score(value: true) (answer: Int) { answer = 1 }
|
|
score(value: false) (answer: Int) { answer = 0 }
|
|
read(value: Payload.Value(payload)) (answer: Int) { answer = payload.count + score(payload.enabled and payload.amount == 3.5e0) }
|
|
read(value: Payload.Empty) (answer: Int) { answer = 0 }
|
|
unwrap(value: Envelope.Present(payload)) (answer: Int) { answer = read(payload) }
|
|
unwrap(value: Envelope.Missing) (answer: Int) { answer = -1 }
|
|
optional(value: Int) (answer: Int?) { answer = value }
|
|
absent() (answer: Int?) { answer = null }
|
|
public main() (answer: Int) {
|
|
pair = Pair {
|
|
count = apply(increment, 40)
|
|
enabled = true
|
|
amount = 3.5e0
|
|
}
|
|
first = Payload.Value(pair)
|
|
second = Payload.Value(pair)
|
|
equal = first == second and first != Payload.Empty and optional(1) == optional(1) and absent() == absent() and optional(1) != absent()
|
|
answer = unwrap(Envelope.Present(first)) + score(equal)
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn checked_dever_functions_emit_and_link_on_all_six_targets() {
|
|
let source = r#"double(value: Int) (answer: Int) { answer = value * 2 }
|
|
public main() (answer: Int) { answer = double(21) }
|
|
"#;
|
|
let directory = temp::TemporaryDirectory::new();
|
|
let ir = lower(source);
|
|
for (index, target) in Target::ALL.into_iter().enumerate() {
|
|
let object = directory.path().join(format!("{index}.o"));
|
|
fs::write(&object, emit_object(&ir, target).unwrap()).unwrap();
|
|
let output = directory.path().join(format!("{index}.image"));
|
|
link(target, LinkKind::Shared, &[object], &output, None).unwrap();
|
|
assert!(fs::metadata(output).unwrap().len() > 128);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn checked_aggregate_functions_emit_real_objects_on_all_six_targets() {
|
|
let ir = lower(AGGREGATE_SOURCE);
|
|
for target in Target::ALL {
|
|
assert!(emit_object(&ir, target).unwrap().len() > 128);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn windows_aggregate_link_requires_target_runtime_support() {
|
|
// LLVM emits the real Windows stack-probe and floating-point CRT references.
|
|
// A signed target runtime pack must provide them; no fixture stubs replace it.
|
|
let directory = temp::TemporaryDirectory::new();
|
|
let object = directory.path().join("aggregate.obj");
|
|
fs::write(
|
|
&object,
|
|
emit_object(&lower(AGGREGATE_SOURCE), Target::WindowsX86_64).unwrap(),
|
|
)
|
|
.unwrap();
|
|
let output = directory.path().join("aggregate.dll");
|
|
let error = link(
|
|
Target::WindowsX86_64,
|
|
LinkKind::Shared,
|
|
&[object],
|
|
&output,
|
|
None,
|
|
)
|
|
.unwrap_err();
|
|
assert!(error.contains("__chkstk"), "{error}");
|
|
assert!(error.contains("_fltused"), "{error}");
|
|
assert!(!output.exists());
|
|
}
|
|
|
|
// The driver is freestanding and test-owned. Foreign targets above are linked,
|
|
// not executed; the executable checks below only apply to this native host.
|
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
mod native {
|
|
use super::*;
|
|
use std::process::{Command, Stdio};
|
|
use std::time::Duration;
|
|
|
|
fn execute(source: &str, output_type: &str, checks: &str) {
|
|
let mut ir = lower(source);
|
|
ir.push_str(&format!(
|
|
"\ndefine void @_start() noreturn {{\nentry:\n %out = alloca {output_type}\n %fault = alloca %dever.fault\n %status = call i32 @dever_entry(ptr %out, ptr %fault)\n{checks}\n %exit = select i1 %passed, i64 0, i64 1\n call void asm sideeffect \"syscall\", \"{{rax}},{{rdi}},~{{rcx}},~{{r11}},~{{memory}}\"(i64 60, i64 %exit)\n unreachable\n}}\n"
|
|
));
|
|
let directory = temp::TemporaryDirectory::new();
|
|
let object = directory.path().join("entry.o");
|
|
fs::write(&object, emit_object(&ir, Target::LinuxX86_64).unwrap()).unwrap();
|
|
let output = directory.path().join("program");
|
|
link(
|
|
Target::LinuxX86_64,
|
|
LinkKind::Executable,
|
|
&[object],
|
|
&output,
|
|
Some("_start"),
|
|
)
|
|
.unwrap();
|
|
let status = process::status(
|
|
Command::new(output)
|
|
.env_clear()
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null()),
|
|
Duration::from_secs(5),
|
|
)
|
|
.unwrap();
|
|
assert!(status.success(), "native kernel failed: {status}\n{source}");
|
|
}
|
|
|
|
fn integer(source: &str, expected: i64) {
|
|
execute(
|
|
source,
|
|
"{ i64 }",
|
|
&format!(
|
|
" %okay = icmp eq i32 %status, 0\n br i1 %okay, label %success, label %failed\nsuccess:\n %actual = load i64, ptr %out\n %correct = icmp eq i64 %actual, {expected}\n br label %done\nfailed:\n br label %done\ndone:\n %passed = phi i1 [ %correct, %success ], [ false, %failed ]"
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clauses_nullable_choices_and_record_values_execute_without_rustc() {
|
|
integer(
|
|
r#"type Selection {
|
|
Found(value: Int)
|
|
Missing
|
|
}
|
|
type Pair {
|
|
first: Int
|
|
second: Int
|
|
}
|
|
choose(value: Selection.Found(payload)) (answer: Int) { answer = payload }
|
|
choose(value: Selection.Missing) (answer: Int) { answer = -1 }
|
|
choose(value: null) (answer: Int) { answer = -2 }
|
|
public main() (answer: Int) {
|
|
original = Pair {
|
|
second = 2
|
|
first = 1
|
|
}
|
|
changed = original
|
|
changed.first = 99
|
|
answer = original.first + changed.first + choose(Selection.Found(7)) + choose(Selection.Missing) + choose(null)
|
|
}
|
|
"#,
|
|
104,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn short_circuit_does_not_execute_a_faulting_right_operand() {
|
|
integer(
|
|
r#"truth(value: Bool) (answer: Bool) { answer = value }
|
|
probe(left: Int, right: Int) (answer: Bool) { answer = left // right > 0 }
|
|
score(value: true) (answer: Int) { answer = 42 }
|
|
score(value: false) (answer: Int) { answer = 0 }
|
|
public main() (answer: Int) { answer = score(truth(false) and probe(1, 0) or truth(true)) }
|
|
"#,
|
|
42,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn static_handlers_and_nested_aligned_choice_payloads_execute() {
|
|
integer(AGGREGATE_SOURCE, 43);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_outputs_preserve_order_and_ieee_float_semantics() {
|
|
execute(
|
|
r#"divide(left: Float, right: Float) (answer: Float) { answer = left / right }
|
|
public main() (first: Int, second: Float, equal: Bool) {
|
|
first = 7
|
|
second = divide(6.0e0, 2.0e0)
|
|
nan = divide(0.0e0, 0.0e0)
|
|
equal = nan == nan
|
|
}
|
|
"#,
|
|
"{ i64, double, i1 }",
|
|
" %okay = icmp eq i32 %status, 0
|
|
br i1 %okay, label %success, label %failed
|
|
success:
|
|
%first_ptr = getelementptr { i64, double, i1 }, ptr %out, i32 0, i32 0
|
|
%second_ptr = getelementptr { i64, double, i1 }, ptr %out, i32 0, i32 1
|
|
%equal_ptr = getelementptr { i64, double, i1 }, ptr %out, i32 0, i32 2
|
|
%first = load i64, ptr %first_ptr
|
|
%second = load double, ptr %second_ptr
|
|
%equal = load i1, ptr %equal_ptr
|
|
%first_ok = icmp eq i64 %first, 7
|
|
%second_ok = fcmp oeq double %second, 3.0
|
|
%nan_ok = xor i1 %equal, true
|
|
%values_ok = and i1 %first_ok, %second_ok
|
|
%correct = and i1 %values_ok, %nan_ok
|
|
br label %done
|
|
failed:
|
|
br label %done
|
|
done:
|
|
%passed = phi i1 [ %correct, %success ], [ false, %failed ]",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn integer_overflow_and_division_faults_return_checked_statuses() {
|
|
for operation in [
|
|
"left + right",
|
|
"left - right",
|
|
"left * right",
|
|
"left // right",
|
|
"left % right",
|
|
] {
|
|
let (left, right, fault) = match operation {
|
|
"left + right" => (i64::MAX, 1, 1),
|
|
"left - right" => (i64::MIN, 1, 1),
|
|
"left * right" => (i64::MAX, 2, 1),
|
|
_ => (1, 0, 2),
|
|
};
|
|
let source = format!(
|
|
"calculate(left: Int, right: Int) (answer: Int) {{ answer = {operation} }}\npublic main() (answer: Int) {{ answer = calculate({left}, {right}) }}"
|
|
);
|
|
execute(
|
|
&source,
|
|
"{ i64 }",
|
|
&format!(
|
|
" %failed = icmp eq i32 %status, 1\n %code_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 0\n %code = load i32, ptr %code_ptr\n %correct = icmp eq i32 %code, {fault}\n %passed = and i1 %failed, %correct"
|
|
),
|
|
);
|
|
}
|
|
for (operation, parameters, arguments) in [
|
|
("-left", "left: Int", i64::MIN.to_string()),
|
|
(
|
|
"left // right",
|
|
"left: Int, right: Int",
|
|
format!("{}, -1", i64::MIN),
|
|
),
|
|
(
|
|
"left % right",
|
|
"left: Int, right: Int",
|
|
format!("{}, -1", i64::MIN),
|
|
),
|
|
] {
|
|
let source = format!(
|
|
"calculate({parameters}) (answer: Int) {{ answer = {operation} }}\npublic main() (answer: Int) {{ answer = calculate({arguments}) }}"
|
|
);
|
|
execute(
|
|
&source,
|
|
"{ i64 }",
|
|
" %failed = icmp eq i32 %status, 1\n %code_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 0\n %code = load i32, ptr %code_ptr\n %correct = icmp eq i32 %code, 1\n %passed = and i1 %failed, %correct",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_faults_retain_origin_and_every_caller_source_span() {
|
|
let source = "divide(left: Int, right: Int) (answer: Int) { answer = left // right }\nwrap(left: Int, right: Int) (answer: Int) { answer = divide(left, right) }\npublic main() (answer: Int) { answer = wrap(1, 0) }";
|
|
let origin = source.find("left // right").unwrap();
|
|
let inner_call = source.find("divide(left, right)").unwrap();
|
|
let outer_call = source.find("wrap(1, 0)").unwrap();
|
|
execute(source, "{ i64 }", &format!(
|
|
" %failed = icmp eq i32 %status, 1
|
|
%origin_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 1
|
|
%origin_index = load i32, ptr %origin_ptr
|
|
%origin_span = getelementptr {{ i64, i64, i64 }}, ptr @dever_source_spans, i32 %origin_index, i32 1
|
|
%origin = load i64, ptr %origin_span
|
|
%origin_ok = icmp eq i64 %origin, {origin}
|
|
%depth_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 2
|
|
%depth = load i32, ptr %depth_ptr
|
|
%depth_ok = icmp eq i32 %depth, 2
|
|
%frame0_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 3, i32 0
|
|
%frame1_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 3, i32 1
|
|
%frame0 = load i32, ptr %frame0_ptr
|
|
%frame1 = load i32, ptr %frame1_ptr
|
|
%span0 = getelementptr {{ i64, i64, i64 }}, ptr @dever_source_spans, i32 %frame0, i32 1
|
|
%span1 = getelementptr {{ i64, i64, i64 }}, ptr @dever_source_spans, i32 %frame1, i32 1
|
|
%start0 = load i64, ptr %span0
|
|
%start1 = load i64, ptr %span1
|
|
%inner_ok = icmp eq i64 %start0, {inner_call}
|
|
%outer_ok = icmp eq i64 %start1, {outer_call}
|
|
%chain_ok = and i1 %inner_ok, %outer_ok
|
|
%location_ok = and i1 %origin_ok, %depth_ok
|
|
%fault_ok = and i1 %failed, %location_ok
|
|
%passed = and i1 %fault_ok, %chain_ok"
|
|
));
|
|
}
|
|
}
|