use crate::concurrency::marshal::gen_eval;
use crate::interpreter::RuntimeValue;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AcceptanceContract {
pub lo: i64,
pub hi: i64,
}
impl AcceptanceContract {
pub fn new(lo: i64, hi: i64) -> Self {
AcceptanceContract { lo, hi }
}
pub fn apply(&self, function: &RuntimeValue, arg: i64) -> Result<i64, String> {
let closure = match function {
RuntimeValue::Function(c) => c,
other => {
return Err(format!(
"acceptance contract: expected a shipped computation, got {}",
other.type_name()
))
}
};
let gen = closure.generated.as_ref().ok_or_else(|| {
"acceptance contract: an ordinary closure is refused — only a `Send computed` \
shipped pure computation may be run under a contract"
.to_string()
})?;
if closure.param_names.len() != 1 {
return Err(format!(
"acceptance contract: a shipped computation must take exactly one argument, \
this one takes {}",
closure.param_names.len()
));
}
if arg < self.lo || arg > self.hi {
return Err(format!(
"acceptance contract: argument {arg} is outside the accepted range \
{}..={} — refused (the contract is not satisfied; the value is not clamped)",
self.lo, self.hi
));
}
Ok(gen_eval(gen, arg))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::concurrency::marshal::GenExpr;
use crate::interpreter::ClosureValue;
use std::collections::HashMap;
use std::rc::Rc;
fn shipped_3x_plus_1() -> RuntimeValue {
let gen = GenExpr::Add(
Box::new(GenExpr::Mul(Box::new(GenExpr::Index), Box::new(GenExpr::Const(3)))),
Box::new(GenExpr::Const(1)),
);
RuntimeValue::Function(Box::new(ClosureValue {
body_index: usize::MAX,
captured_env: HashMap::default(),
param_names: vec![logicaffeine_base::Symbol::from_index(0)],
generated: Some(Rc::new(gen)),
}))
}
#[test]
fn in_range_argument_runs_in_the_sandbox() {
let contract = AcceptanceContract::new(0, 1000);
assert_eq!(contract.apply(&shipped_3x_plus_1(), 5).unwrap(), 16);
assert_eq!(contract.apply(&shipped_3x_plus_1(), 0).unwrap(), 1);
assert_eq!(contract.apply(&shipped_3x_plus_1(), 1000).unwrap(), 3001);
}
#[test]
fn out_of_range_argument_is_refused_not_clamped() {
let contract = AcceptanceContract::new(0, 1000);
let above = contract.apply(&shipped_3x_plus_1(), 1001);
let below = contract.apply(&shipped_3x_plus_1(), -1);
assert!(above.is_err(), "an argument above the range must be refused");
assert!(below.is_err(), "an argument below the range must be refused");
assert!(above.unwrap_err().contains("1001"));
assert!(below.unwrap_err().contains("refused"));
}
#[test]
fn an_ordinary_closure_is_refused_at_the_signature_check() {
let ordinary = RuntimeValue::Function(Box::new(ClosureValue {
body_index: 7,
captured_env: HashMap::default(),
param_names: vec![logicaffeine_base::Symbol::from_index(0)],
generated: None,
}));
let contract = AcceptanceContract::new(0, 1000);
assert!(contract.apply(&ordinary, 5).is_err(), "an ordinary closure must be refused");
}
#[test]
fn a_wrong_arity_computation_is_refused() {
let gen = GenExpr::Index;
let two_arg = RuntimeValue::Function(Box::new(ClosureValue {
body_index: usize::MAX,
captured_env: HashMap::default(),
param_names: vec![
logicaffeine_base::Symbol::from_index(0),
logicaffeine_base::Symbol::from_index(1),
],
generated: Some(Rc::new(gen)),
}));
let contract = AcceptanceContract::new(0, 1000);
assert!(contract.apply(&two_arg, 5).is_err(), "a 2-argument computation must be refused");
}
#[test]
fn a_non_function_value_is_refused() {
let contract = AcceptanceContract::new(0, 1000);
assert!(contract.apply(&RuntimeValue::Int(5), 5).is_err(), "a non-function must be refused");
}
#[test]
fn a_reversed_range_accepts_nothing() {
let contract = AcceptanceContract::new(1000, 0);
assert!(contract.apply(&shipped_3x_plus_1(), 5).is_err());
assert!(contract.apply(&shipped_3x_plus_1(), 500).is_err());
}
}