logicaffeine_compile/semantics/
acceptance.rs1use crate::concurrency::marshal::gen_eval;
20use crate::interpreter::RuntimeValue;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct AcceptanceContract {
27 pub lo: i64,
28 pub hi: i64,
29}
30
31impl AcceptanceContract {
32 pub fn new(lo: i64, hi: i64) -> Self {
33 AcceptanceContract { lo, hi }
36 }
37
38 pub fn apply(&self, function: &RuntimeValue, arg: i64) -> Result<i64, String> {
48 let closure = match function {
49 RuntimeValue::Function(c) => c,
50 other => {
51 return Err(format!(
52 "acceptance contract: expected a shipped computation, got {}",
53 other.type_name()
54 ))
55 }
56 };
57 let gen = closure.generated.as_ref().ok_or_else(|| {
58 "acceptance contract: an ordinary closure is refused — only a `Send computed` \
59 shipped pure computation may be run under a contract"
60 .to_string()
61 })?;
62 if closure.param_names.len() != 1 {
63 return Err(format!(
64 "acceptance contract: a shipped computation must take exactly one argument, \
65 this one takes {}",
66 closure.param_names.len()
67 ));
68 }
69 if arg < self.lo || arg > self.hi {
70 return Err(format!(
71 "acceptance contract: argument {arg} is outside the accepted range \
72 {}..={} — refused (the contract is not satisfied; the value is not clamped)",
73 self.lo, self.hi
74 ));
75 }
76 Ok(gen_eval(gen, arg))
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::concurrency::marshal::GenExpr;
84 use crate::interpreter::ClosureValue;
85 use std::collections::HashMap;
86 use std::rc::Rc;
87
88 fn shipped_3x_plus_1() -> RuntimeValue {
91 let gen = GenExpr::Add(
92 Box::new(GenExpr::Mul(Box::new(GenExpr::Index), Box::new(GenExpr::Const(3)))),
93 Box::new(GenExpr::Const(1)),
94 );
95 RuntimeValue::Function(Box::new(ClosureValue {
96 body_index: usize::MAX,
97 captured_env: HashMap::default(),
98 param_names: vec![logicaffeine_base::Symbol::from_index(0)],
99 generated: Some(Rc::new(gen)),
100 }))
101 }
102
103 #[test]
104 fn in_range_argument_runs_in_the_sandbox() {
105 let contract = AcceptanceContract::new(0, 1000);
106 assert_eq!(contract.apply(&shipped_3x_plus_1(), 5).unwrap(), 16);
108 assert_eq!(contract.apply(&shipped_3x_plus_1(), 0).unwrap(), 1);
110 assert_eq!(contract.apply(&shipped_3x_plus_1(), 1000).unwrap(), 3001);
111 }
112
113 #[test]
114 fn out_of_range_argument_is_refused_not_clamped() {
115 let contract = AcceptanceContract::new(0, 1000);
116 let above = contract.apply(&shipped_3x_plus_1(), 1001);
117 let below = contract.apply(&shipped_3x_plus_1(), -1);
118 assert!(above.is_err(), "an argument above the range must be refused");
119 assert!(below.is_err(), "an argument below the range must be refused");
120 assert!(above.unwrap_err().contains("1001"));
122 assert!(below.unwrap_err().contains("refused"));
123 }
124
125 #[test]
126 fn an_ordinary_closure_is_refused_at_the_signature_check() {
127 let ordinary = RuntimeValue::Function(Box::new(ClosureValue {
130 body_index: 7,
131 captured_env: HashMap::default(),
132 param_names: vec![logicaffeine_base::Symbol::from_index(0)],
133 generated: None,
134 }));
135 let contract = AcceptanceContract::new(0, 1000);
136 assert!(contract.apply(&ordinary, 5).is_err(), "an ordinary closure must be refused");
137 }
138
139 #[test]
140 fn a_wrong_arity_computation_is_refused() {
141 let gen = GenExpr::Index;
142 let two_arg = RuntimeValue::Function(Box::new(ClosureValue {
143 body_index: usize::MAX,
144 captured_env: HashMap::default(),
145 param_names: vec![
146 logicaffeine_base::Symbol::from_index(0),
147 logicaffeine_base::Symbol::from_index(1),
148 ],
149 generated: Some(Rc::new(gen)),
150 }));
151 let contract = AcceptanceContract::new(0, 1000);
152 assert!(contract.apply(&two_arg, 5).is_err(), "a 2-argument computation must be refused");
153 }
154
155 #[test]
156 fn a_non_function_value_is_refused() {
157 let contract = AcceptanceContract::new(0, 1000);
158 assert!(contract.apply(&RuntimeValue::Int(5), 5).is_err(), "a non-function must be refused");
159 }
160
161 #[test]
162 fn a_reversed_range_accepts_nothing() {
163 let contract = AcceptanceContract::new(1000, 0);
165 assert!(contract.apply(&shipped_3x_plus_1(), 5).is_err());
166 assert!(contract.apply(&shipped_3x_plus_1(), 500).is_err());
167 }
168}