use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::collections::BTreeSet;
fn policy_with(effects: &[&str], procs: &[&str]) -> Policy {
let mut p = Policy::pure();
p.allow_effects = effects.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>();
p.allow_proc = procs.iter().map(|s| s.to_string()).collect();
p
}
fn run(src: &str, func: &str, args: Vec<Value>, policy: Policy) -> Result<Value, String> {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors: {errs:#?}");
}
let bc = compile_program(&stages);
let handler = DefaultHandler::new(policy);
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(func, args).map_err(|e| format!("{e}"))
}
fn unwrap_record(v: &Value) -> &indexmap::IndexMap<String, Value> {
match v {
Value::Record(r) => r,
other => panic!("expected Record, got {other:?}"),
}
}
fn variant_args(v: &Value, expected_name: &str) -> Vec<Value> {
match v {
Value::Variant { name, args } if name == expected_name => args.clone(),
other => panic!("expected Variant(`{expected_name}`), got {other:?}"),
}
}
const SRC: &str = r#"
import "std.proc" as proc
fn echo(args :: List[Str]) -> [proc] Result[{ stdout :: Str, stderr :: Str, exit_code :: Int }, Str] {
proc.spawn("echo", args)
}
fn forbidden() -> [proc] Result[{ stdout :: Str, stderr :: Str, exit_code :: Int }, Str] {
proc.spawn("/usr/bin/whoami", [])
}
fn falsy() -> [proc] Result[{ stdout :: Str, stderr :: Str, exit_code :: Int }, Str] {
proc.spawn("false", [])
}
"#;
#[test]
fn proc_spawn_runs_echo_and_returns_stdout() {
let r = run(SRC, "echo",
vec![Value::List(vec![
Value::Str("hello".into()),
Value::Str("world".into()),
].into())],
policy_with(&["proc"], &["echo"])).expect("run");
let inner = variant_args(&r, "Ok");
let rec = unwrap_record(&inner[0]);
assert_eq!(rec.get("stdout"), Some(&Value::Str("hello world\n".into())));
assert_eq!(rec.get("exit_code"), Some(&Value::Int(0)));
}
#[test]
fn proc_spawn_blocks_binary_outside_allow_proc() {
let r = run(SRC, "forbidden",
vec![],
policy_with(&["proc"], &["echo"])).expect("run");
let inner = variant_args(&r, "Err");
let msg = match &inner[0] {
Value::Str(s) => s.clone(),
other => panic!("expected Str err, got {other:?}"),
};
assert!(msg.contains("not in --allow-proc"), "msg: {msg}");
assert!(msg.contains("whoami"), "msg: {msg}");
}
#[test]
fn proc_spawn_with_empty_allow_proc_is_escape_hatch() {
let r = run(SRC, "echo",
vec![Value::List(vec![Value::Str("x".into())].into())],
policy_with(&["proc"], &[])).expect("run");
let inner = variant_args(&r, "Ok");
let rec = unwrap_record(&inner[0]);
assert_eq!(rec.get("stdout"), Some(&Value::Str("x\n".into())));
}
#[test]
fn proc_spawn_propagates_non_zero_exit() {
let r = run(SRC, "falsy",
vec![],
policy_with(&["proc"], &["false"])).expect("run");
let inner = variant_args(&r, "Ok");
let rec = unwrap_record(&inner[0]);
let exit = match rec.get("exit_code") {
Some(Value::Int(n)) => *n,
other => panic!("exit_code: {other:?}"),
};
assert_ne!(exit, 0, "false should exit non-zero");
}
#[test]
fn proc_spawn_without_proc_in_allow_effects_is_runtime_rejected() {
let r = run(SRC, "echo",
vec![Value::List(vec![Value::Str("x".into())].into())],
policy_with(&[], &[])); let err = r.expect_err("proc.spawn without --allow-effects proc must error");
assert!(err.contains("proc"), "err: {err}");
}