use lex_ast::canonicalize_program;
use lex_syntax::parse_source;
use lex_types::check_program;
fn checks(src: &str) -> bool {
let Ok(p) = parse_source(src) else {
return false;
};
let stages = canonicalize_program(&p);
check_program(&stages).is_ok()
}
struct Prim {
import: &'static str,
call: &'static str,
effect: &'static str,
ret: &'static str,
int_ret: bool,
}
fn prims() -> Vec<Prim> {
vec![
Prim {
import: "import \"std.time\" as time",
call: "time.now()",
effect: "time",
ret: "Int",
int_ret: true,
},
Prim {
import: "import \"std.time\" as time",
call: "time.now_ms()",
effect: "time",
ret: "Int",
int_ret: true,
},
Prim {
import: "import \"std.rand\" as rand",
call: "rand.int_in(0, 1)",
effect: "random",
ret: "Int",
int_ret: true,
},
Prim {
import: "import \"std.io\" as io",
call: "io.print(\"x\")",
effect: "io",
ret: "Nil",
int_ret: false,
},
Prim {
import: "import \"std.env\" as env",
call: "env.get(\"HOME\")",
effect: "env",
ret: "Option[Str]",
int_ret: false,
},
]
}
fn bodies(p: &Prim) -> Vec<(&'static str, String)> {
let mut v = vec![
("direct", p.call.to_string()),
("let", format!("let x := {}\n x", p.call)),
("match", format!("match 0 {{ _ => {} }}", p.call)),
];
if p.int_ret {
v.push(("if", format!("if 0 == 0 {{ {} }} else {{ 0 }}", p.call)));
}
v
}
fn func(decl_effect: &str, ret: &str, body: &str) -> String {
let row = if decl_effect.is_empty() {
String::new()
} else {
format!("[{decl_effect}] ")
};
format!("fn f() -> {row}{ret} {{\n {body}\n}}\n")
}
#[test]
fn declaring_the_effect_is_accepted() {
for p in prims() {
for (ctx, body) in bodies(&p) {
let src = format!("{}\n{}", p.import, func(p.effect, p.ret, &body));
assert!(
checks(&src),
"honest program should type-check ({} via {ctx}):\n{src}",
p.effect
);
}
}
}
#[test]
fn omitting_a_performed_effect_is_rejected() {
for p in prims() {
for (ctx, body) in bodies(&p) {
let src = format!("{}\n{}", p.import, func("", p.ret, &body));
assert!(
!checks(&src),
"effect `{}` escaped an empty declared row via {ctx} — soundness hole:\n{src}",
p.effect
);
}
}
}
#[test]
fn effect_propagates_through_a_function_call() {
for p in prims() {
let helper = format!(
"fn helper() -> [{}] {} {{\n {}\n}}\n",
p.effect, p.ret, p.call
);
let honest = format!(
"{}\n{}fn f() -> [{}] {} {{\n helper()\n}}\n",
p.import, helper, p.effect, p.ret
);
assert!(checks(&honest), "honest caller should check:\n{honest}");
let lying = format!(
"{}\n{}fn f() -> {} {{\n helper()\n}}\n",
p.import, helper, p.ret
);
assert!(
!checks(&lying),
"effect `{}` escaped through a call boundary — soundness hole:\n{lying}",
p.effect
);
}
}
#[test]
fn every_member_of_a_multi_effect_body_must_be_declared() {
let import = "import \"std.time\" as time\nimport \"std.rand\" as rand";
let body = "let a := time.now()\n let b := rand.int_in(0, 1)\n a + b";
assert!(
checks(&format!("{import}\n{}", func("time, random", "Int", body))),
"declaring both effects should check"
);
for missing in ["time", "random", ""] {
let src = format!("{import}\n{}", func(missing, "Int", body));
assert!(
!checks(&src),
"a two-effect body type-checked under row `[{missing}]` — soundness hole:\n{src}"
);
}
}
const FUZZ_PRIMS: &[(&str, &str)] = &[
("time.now()", "time"),
("time.now_ms()", "time"),
("rand.int_in(0, 1)", "random"), ];
fn wrap_int(kind: usize, expr: &str) -> String {
match kind {
0 => expr.to_string(),
1 => format!("let x := {expr}\n x"),
2 => format!("if 0 == 0 {{ {expr} }} else {{ 0 }}"),
_ => format!("match 0 {{ _ => {expr} }}"),
}
}
#[test]
fn composition_sweep_is_sound() {
let p = FUZZ_PRIMS;
let mut shapes = 0u32;
for n in 1..=3usize {
let combos = p.len().pow(n as u32);
for combo in 0..combos {
let mut idx = combo;
let mut calls = Vec::new();
let (mut used_time, mut used_rand) = (false, false);
for _ in 0..n {
let (call, eff) = p[idx % p.len()];
idx /= p.len();
calls.push(call);
match eff {
"time" => used_time = true,
_ => used_rand = true,
}
}
let expr = calls.join(" + ");
for wrapper in 0..4 {
let body = wrap_int(wrapper, &expr);
let mut imports = String::new();
if used_time {
imports.push_str("import \"std.time\" as time\n");
}
if used_rand {
imports.push_str("import \"std.rand\" as rand\n");
}
let mut used: Vec<&str> = Vec::new();
if used_time {
used.push("time");
}
if used_rand {
used.push("random"); }
let honest = format!(
"{imports}fn f() -> [{}] Int {{\n {body}\n}}\n",
used.join(", ")
);
assert!(checks(&honest), "honest sweep program failed:\n{honest}");
let subsets: Vec<&[&str]> = match used.as_slice() {
["time", "random"] => vec![&[], &["time"], &["random"]],
_ => vec![&[]], };
for sub in subsets {
let lying = format!("{imports}{}", func(&sub.join(", "), "Int", &body));
assert!(
!checks(&lying),
"effect escaped in sweep (performed {used:?}, declared {sub:?}):\n{lying}"
);
}
shapes += 1;
}
}
}
assert!(
shapes >= 150,
"expected to sweep the full shape space, got {shapes}"
);
}
#[test]
fn a_pure_body_needs_no_effects() {
assert!(checks("fn f() -> Int {\n 1 + 2\n}\n"));
}