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::sync::Arc;
fn compile_and_run(
src: &str,
fn_name: &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 = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, args).map_err(|e| format!("{e:?}"))
}
fn permissive_with_ceiling(ceiling: u64) -> Policy {
let mut p = Policy::permissive();
p.budget = Some(ceiling);
p
}
const SRC: &str = r#"
fn step() -> [budget(10)] Int { 1 }
# Calls step twice. Static walk sees only one declaration of
# `[budget(10)]` at step's signature; runtime should deduct 10
# per invocation, totalling 20.
fn run_twice() -> [budget(10)] Int {
let _a := step()
let _b := step()
42
}
fn run_once() -> [budget(10)] Int {
step()
}
"#;
#[test]
fn single_call_under_ceiling_succeeds() {
let v = compile_and_run(SRC, "run_once", vec![],
permissive_with_ceiling(15)).unwrap();
assert_eq!(v, Value::Int(1));
}
#[test]
fn repeated_calls_exceeding_ceiling_are_refused() {
let err = compile_and_run(SRC, "run_twice", vec![],
permissive_with_ceiling(15)).unwrap_err();
assert!(err.contains("budget exceeded"),
"expected budget-exceeded error, got: {err}");
}
#[test]
fn repeated_calls_under_ceiling_succeed() {
let v = compile_and_run(SRC, "run_twice", vec![],
permissive_with_ceiling(30)).unwrap();
assert_eq!(v, Value::Int(42));
}
#[test]
fn no_ceiling_means_no_runtime_check() {
let p = Policy::permissive();
let v = compile_and_run(SRC, "run_twice", vec![], p).unwrap();
assert_eq!(v, Value::Int(42));
}
#[test]
fn pure_function_calls_are_not_charged() {
let pure_src = r#"
fn helper() -> Int { 1 }
fn main_fn() -> Int {
let _a := helper()
let _b := helper()
let _c := helper()
helper()
}
"#;
let v = compile_and_run(pure_src, "main_fn", vec![],
permissive_with_ceiling(0)).unwrap();
assert_eq!(v, Value::Int(1));
}
#[test]
fn budget_exceeded_error_names_the_ceiling() {
let err = compile_and_run(SRC, "run_twice", vec![],
permissive_with_ceiling(15)).unwrap_err();
assert!(err.contains("ceiling 15"),
"error should name the ceiling; got: {err}");
assert!(err.contains("requested 10"),
"error should name the offending request; got: {err}");
}