use lex_ast::canonicalize_program;
use lex_bytecode::vm::Vm;
use lex_bytecode::Value;
use lex_runtime::{check_program, CapturedSink, DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::collections::BTreeSet;
fn compile(src: &str) -> lex_bytecode::Program {
let prog = parse_source(src).unwrap();
let stages = canonicalize_program(&prog);
lex_bytecode::compile_program(&stages)
}
fn allow(effects: &[&str]) -> Policy {
let mut p = Policy::pure();
p.allow_effects = effects.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>();
p
}
const ECHO: &str = include_str!("../../../examples/c_echo.lex");
#[test]
fn echo_runs_with_io_allowed() {
let prog = compile(ECHO);
let policy = allow(&["io"]);
check_program(&prog, &policy).expect("policy must accept the program");
let sink = Box::new(CapturedSink::default());
let handler = DefaultHandler::new(policy).with_sink(sink);
let mut vm = Vm::with_handler(&prog, Box::new(handler));
let r = vm.call("echo", vec![Value::Str("hi".into())]).unwrap();
assert_eq!(r, Value::Unit);
}
#[test]
fn echo_output_is_captured() {
let prog = compile(ECHO);
let policy = allow(&["io"]);
check_program(&prog, &policy).expect("policy must accept the program");
use std::sync::{Arc, Mutex};
struct SharedSink(Arc<Mutex<Vec<String>>>);
impl lex_runtime::IoSink for SharedSink {
fn print_line(&mut self, s: &str) { self.0.lock().unwrap().push(s.into()); }
}
let captured: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Box::new(SharedSink(Arc::clone(&captured)));
let handler = DefaultHandler::new(policy).with_sink(sink);
let mut vm = Vm::with_handler(&prog, Box::new(handler));
vm.call("echo", vec![Value::Str("hello world".into())]).unwrap();
let lines = captured.lock().unwrap();
assert_eq!(lines.as_slice(), &["hello world".to_string()]);
}
#[test]
fn echo_rejected_when_io_not_allowed() {
let prog = compile(ECHO);
let policy = Policy::pure();
let err = check_program(&prog, &policy).expect_err("policy must reject");
assert_eq!(err.len(), 1);
assert_eq!(err[0].kind, "effect_not_allowed");
assert_eq!(err[0].effect.as_deref(), Some("io"));
assert_eq!(err[0].at.as_deref(), Some("echo"));
}
#[test]
fn budget_is_aggregated_and_enforced() {
let src = r#"
fn step_a() -> [budget(50)] Int { 1 }
fn step_b() -> [budget(50)] Int { 2 }
fn run() -> [budget(50)] Int { step_a() }
"#;
let prog = compile(src);
let mut p = allow(&["budget"]);
p.budget = Some(200);
check_program(&prog, &p).expect("100 ≤ 200");
p.budget = Some(50);
let err = check_program(&prog, &p).expect_err("100 > 50");
assert!(err.iter().any(|v| v.kind == "budget_exceeded"),
"expected budget_exceeded, got {err:#?}");
}
#[test]
fn net_call_blocked_at_policy_time() {
let src = "fn fetch() -> [net] Int { 0 }\n";
let prog = compile(src);
let policy = allow(&["io"]); let err = check_program(&prog, &policy).expect_err("must reject");
assert!(err.iter().any(|v| v.effect.as_deref() == Some("net")
&& v.kind == "effect_not_allowed"),
"expected net violation, got {err:#?}");
}
#[test]
fn net_call_allowed_when_in_allowlist() {
let src = "fn fetch() -> [net] Int { 0 }\n";
let prog = compile(src);
let policy = allow(&["net"]);
check_program(&prog, &policy).expect("net allowed");
}
#[test]
fn fs_read_path_must_be_under_allowlist() {
let src = r#"fn read_data() -> [fs_read("/etc")] Int { 0 }
"#;
let prog = compile(src);
let mut policy = allow(&["fs_read"]);
policy.allow_fs_read = vec!["/var".into()];
let err = check_program(&prog, &policy).expect_err("/etc not under /var");
assert!(err.iter().any(|v| v.kind == "fs_path_not_allowed"
&& v.path.as_deref() == Some("/etc")));
policy.allow_fs_read = vec!["/etc".into()];
check_program(&prog, &policy).expect("now /etc is allowed");
}