1use crate::ast::{Spec, SpecExpr, SpecOp};
23use indexmap::IndexMap;
24use lex_bytecode::{compile_program, vm::Vm, Value};
25use lex_runtime::{DefaultHandler, Policy};
26use lex_syntax::parse_source;
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31#[serde(tag = "verdict", rename_all = "snake_case")]
32pub enum GateVerdict {
33 Allow,
34 Deny { spec_name: String, reason: String },
39 Inconclusive { spec_name: String, reason: String },
44}
45
46pub fn evaluate_gate(
55 specs: &[Spec],
56 bindings: &IndexMap<String, Value>,
57 lex_source: &str,
58) -> GateVerdict {
59 let prog = match parse_source(lex_source) {
60 Ok(p) => p,
61 Err(e) => return GateVerdict::Inconclusive {
62 spec_name: "<parse>".into(),
63 reason: format!("parse: {e}"),
64 },
65 };
66 let stages = lex_ast::canonicalize_program(&prog);
67 if let Err(errs) = lex_types::check_program(&stages) {
68 return GateVerdict::Inconclusive {
69 spec_name: "<typecheck>".into(),
70 reason: format!("typecheck: {errs:?}"),
71 };
72 }
73 let bc = compile_program(&stages);
74 evaluate_gate_compiled(specs, bindings, &bc)
75}
76
77pub fn evaluate_gate_compiled(
81 specs: &[Spec],
82 bindings: &IndexMap<String, Value>,
83 bc: &lex_bytecode::Program,
84) -> GateVerdict {
85 evaluate_gate_compiled_inner(specs, bindings, bc, None)
86}
87
88pub fn evaluate_gate_compiled_traced<F>(
103 specs: &[Spec],
104 bindings: &IndexMap<String, Value>,
105 bc: &lex_bytecode::Program,
106 new_tracer: F,
107) -> GateVerdict
108where
109 F: Fn() -> Box<dyn lex_bytecode::vm::Tracer>,
110{
111 evaluate_gate_compiled_inner(specs, bindings, bc, Some(&new_tracer))
112}
113
114fn evaluate_gate_compiled_inner(
115 specs: &[Spec],
116 bindings: &IndexMap<String, Value>,
117 bc: &lex_bytecode::Program,
118 new_tracer: Option<&dyn Fn() -> Box<dyn lex_bytecode::vm::Tracer>>,
119) -> GateVerdict {
120 let policy = Policy::permissive();
121 for spec in specs {
122 match eval_body(&spec.body, bindings, bc, &policy, new_tracer) {
123 Ok(Value::Bool(true)) => continue,
124 Ok(Value::Bool(false)) => {
125 return GateVerdict::Deny {
126 spec_name: spec.name.clone(),
127 reason: format!(
128 "spec `{}` returned false; bindings: {}",
129 spec.name,
130 format_bindings(bindings),
131 ),
132 };
133 }
134 Ok(other) => return GateVerdict::Inconclusive {
135 spec_name: spec.name.clone(),
136 reason: format!("spec body returned non-bool: {other:?}"),
137 },
138 Err(e) => return GateVerdict::Inconclusive {
139 spec_name: spec.name.clone(),
140 reason: e,
141 },
142 }
143 }
144 GateVerdict::Allow
145}
146
147fn format_bindings(b: &IndexMap<String, Value>) -> String {
148 let mut parts: Vec<String> = Vec::with_capacity(b.len());
149 for (k, v) in b {
150 parts.push(format!("{k}={}", short_value(v)));
151 }
152 parts.join(", ")
153}
154
155fn short_value(v: &Value) -> String {
156 match v {
157 Value::Int(i) => format!("{i}"),
158 Value::Float(f) => format!("{f}"),
159 Value::Bool(b) => format!("{b}"),
160 Value::Str(s) => format!("\"{}\"", s.chars().take(40).collect::<String>()),
161 other => format!("{other:?}"),
162 }
163}
164
165fn eval_body(
176 e: &SpecExpr,
177 bindings: &IndexMap<String, Value>,
178 bc: &lex_bytecode::Program,
179 policy: &Policy,
180 new_tracer: Option<&dyn Fn() -> Box<dyn lex_bytecode::vm::Tracer>>,
181) -> Result<Value, String> {
182 match e {
183 SpecExpr::IntLit { value } => Ok(Value::Int(*value)),
184 SpecExpr::FloatLit { value } => Ok(Value::Float(*value)),
185 SpecExpr::BoolLit { value } => Ok(Value::Bool(*value)),
186 SpecExpr::StrLit { value } => Ok(Value::Str(value.clone())),
187 SpecExpr::Var { name } => bindings.get(name).cloned()
188 .ok_or_else(|| format!("unbound spec var `{name}` (provide via gate bindings)")),
189 SpecExpr::Let { name, value, body } => {
190 let v = eval_body(value, bindings, bc, policy, new_tracer)?;
191 let mut next = bindings.clone();
192 next.insert(name.clone(), v);
193 eval_body(body, &next, bc, policy, new_tracer)
194 }
195 SpecExpr::Not { expr } => match eval_body(expr, bindings, bc, policy, new_tracer)? {
196 Value::Bool(b) => Ok(Value::Bool(!b)),
197 other => Err(format!("not on non-bool: {other:?}")),
198 },
199 SpecExpr::BinOp { op, lhs, rhs } => {
200 let a = eval_body(lhs, bindings, bc, policy, new_tracer)?;
201 let b = eval_body(rhs, bindings, bc, policy, new_tracer)?;
202 apply_binop(*op, a, b)
203 }
204 SpecExpr::Call { func, args } => {
205 let mut argv = Vec::new();
206 for a in args { argv.push(eval_body(a, bindings, bc, policy, new_tracer)?); }
207 let handler = DefaultHandler::new(policy.clone());
208 let mut vm = Vm::with_handler(bc, Box::new(handler));
209 if let Some(make_tracer) = new_tracer {
210 vm.set_tracer(make_tracer());
211 }
212 vm.call(func, argv).map_err(|e| format!("call `{func}`: {e}"))
213 }
214 }
215}
216
217fn apply_binop(op: SpecOp, a: Value, b: Value) -> Result<Value, String> {
218 use SpecOp::*;
219 match (op, &a, &b) {
220 (Add, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x + y)),
221 (Sub, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x - y)),
222 (Mul, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x * y)),
223 (Div, Value::Int(x), Value::Int(y)) if *y != 0 => Ok(Value::Int(x / y)),
224 (Mod, Value::Int(x), Value::Int(y)) if *y != 0 => Ok(Value::Int(x % y)),
225 (Add, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x + y)),
226 (Sub, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x - y)),
227 (Mul, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x * y)),
228 (Div, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x / y)),
229 (Eq, x, y) => Ok(Value::Bool(x == y)),
230 (Neq, x, y) => Ok(Value::Bool(x != y)),
231 (Lt, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x < y)),
232 (Le, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x <= y)),
233 (Gt, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x > y)),
234 (Ge, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x >= y)),
235 (Lt, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x < y)),
236 (Le, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x <= y)),
237 (Gt, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x > y)),
238 (Ge, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x >= y)),
239 (And, Value::Bool(x), Value::Bool(y)) => Ok(Value::Bool(*x && *y)),
240 (Or, Value::Bool(x), Value::Bool(y)) => Ok(Value::Bool(*x || *y)),
241 _ => Err(format!("invalid binop {op:?} on {a:?}, {b:?}")),
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::parser::parse_spec;
249
250 fn b<I: IntoIterator<Item = (&'static str, Value)>>(items: I) -> IndexMap<String, Value> {
251 items.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
252 }
253
254 #[test]
255 fn allow_when_spec_returns_true() {
256 let spec = parse_spec("spec ok { forall x :: Int : x + 1 > x }").unwrap();
257 let v = evaluate_gate(&[spec], &b([("x", Value::Int(5))]), "");
258 assert_eq!(v, GateVerdict::Allow);
259 }
260
261 #[test]
262 fn deny_when_spec_returns_false() {
263 let spec = parse_spec("spec budget { forall used :: Int, delta :: Int : (used + delta) <= 100 }").unwrap();
264 let v = evaluate_gate(
265 &[spec],
266 &b([("used", Value::Int(80)), ("delta", Value::Int(30))]),
267 "",
268 );
269 match v {
270 GateVerdict::Deny { spec_name, reason } => {
271 assert_eq!(spec_name, "budget");
272 assert!(reason.contains("used=80"), "reason should include bindings: {reason}");
273 }
274 other => panic!("expected Deny, got {other:?}"),
275 }
276 }
277
278 #[test]
279 fn first_failing_spec_is_reported() {
280 let s1 = parse_spec("spec always { forall x :: Int : x == x }").unwrap();
283 let s2 = parse_spec("spec never { forall x :: Int : x != x }").unwrap();
284 let v = evaluate_gate(&[s1, s2], &b([("x", Value::Int(0))]), "");
285 match v {
286 GateVerdict::Deny { spec_name, .. } => assert_eq!(spec_name, "never"),
287 other => panic!("expected Deny on `never`, got {other:?}"),
288 }
289 }
290
291 #[test]
292 fn missing_binding_is_inconclusive_not_panic() {
293 let spec = parse_spec("spec needs_x { forall x :: Int : x > 0 }").unwrap();
297 let v = evaluate_gate(&[spec], &b([]), "");
298 match v {
299 GateVerdict::Inconclusive { reason, .. } => {
300 assert!(reason.contains("unbound spec var"),
301 "expected unbound-var error, got: {reason}");
302 }
303 other => panic!("expected Inconclusive, got {other:?}"),
304 }
305 }
306
307 #[test]
308 fn grid_budget_phase1_spec() {
309 let spec = parse_spec(r#"
312 spec grid_budget {
313 forall active :: Int, scheduled :: Int, delta :: Int, budget :: Int :
314 (active + scheduled + delta) <= budget
315 }
316 "#).unwrap();
317 let allow = evaluate_gate(std::slice::from_ref(&spec), &b([
318 ("active", Value::Int(40)),
319 ("scheduled", Value::Int(20)),
320 ("delta", Value::Int(15)),
321 ("budget", Value::Int(100)),
322 ]), "");
323 assert_eq!(allow, GateVerdict::Allow);
324 let deny = evaluate_gate(&[spec], &b([
325 ("active", Value::Int(40)),
326 ("scheduled", Value::Int(20)),
327 ("delta", Value::Int(60)),
328 ("budget", Value::Int(100)),
329 ]), "");
330 assert!(matches!(deny, GateVerdict::Deny { .. }));
331 }
332
333 #[test]
334 fn soc_reserve_phase1_spec() {
335 let spec = parse_spec(r#"
338 spec soc_reserve {
339 forall soc :: Int, draw :: Int, reserve :: Int :
340 (soc - draw) >= reserve
341 }
342 "#).unwrap();
343 let allow = evaluate_gate(std::slice::from_ref(&spec), &b([
344 ("soc", Value::Int(80)),
345 ("draw", Value::Int(20)),
346 ("reserve", Value::Int(40)),
347 ]), "");
348 assert_eq!(allow, GateVerdict::Allow);
349 let deny = evaluate_gate(&[spec], &b([
350 ("soc", Value::Int(50)),
351 ("draw", Value::Int(20)),
352 ("reserve", Value::Int(40)),
353 ]), "");
354 assert!(matches!(deny, GateVerdict::Deny { .. }));
355 }
356
357 #[test]
358 fn gate_is_fast_enough_for_synchronous_use() {
359 let s1 = parse_spec(r#"
364 spec grid_budget {
365 forall active :: Int, scheduled :: Int, delta :: Int, budget :: Int :
366 (active + scheduled + delta) <= budget
367 }
368 "#).unwrap();
369 let s2 = parse_spec(r#"
370 spec soc_reserve {
371 forall soc :: Int, draw :: Int, reserve :: Int :
372 (soc - draw) >= reserve
373 }
374 "#).unwrap();
375 let bindings = b([
376 ("active", Value::Int(40)),
377 ("scheduled", Value::Int(20)),
378 ("delta", Value::Int(15)),
379 ("budget", Value::Int(100)),
380 ("soc", Value::Int(80)),
381 ("draw", Value::Int(20)),
382 ("reserve", Value::Int(40)),
383 ]);
384 let prog = parse_source("").unwrap();
385 let stages = lex_ast::canonicalize_program(&prog);
386 let bc = compile_program(&stages);
387
388 let n = 1000;
389 let start = std::time::Instant::now();
390 for _ in 0..n {
391 let v = evaluate_gate_compiled(&[s1.clone(), s2.clone()], &bindings, &bc);
392 assert_eq!(v, GateVerdict::Allow);
393 }
394 let elapsed = start.elapsed();
395 let per_call_us = elapsed.as_micros() / n as u128;
396 assert!(per_call_us < 5_000,
397 "per-gate verdict should be under 5ms; got {per_call_us}μs");
398 }
399
400 struct CallRecorder {
406 captured: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
407 }
408 impl lex_bytecode::vm::Tracer for CallRecorder {
409 fn enter_call(&mut self, _node_id: &str, name: &str, _args: &[Value]) {
410 self.captured.lock().unwrap().push(name.to_string());
411 }
412 fn enter_effect(&mut self, _: &str, _: &str, _: &str, _: &[Value]) {}
413 fn exit_ok(&mut self, _: &Value) {}
414 fn exit_err(&mut self, _: &str) {}
415 fn exit_call_tail(&mut self) {}
416 fn override_effect(&mut self, _: &str) -> Option<Value> { None }
417 }
418
419 #[test]
420 fn traced_gate_captures_nested_call_events() {
421 let host_src = r#"
426 fn projected_load(active :: Int, delta :: Int) -> Int {
427 active + delta
428 }
429 fn budget_total(budget :: Int, headroom :: Int) -> Int {
430 budget + headroom
431 }
432 fn under_budget(active :: Int, delta :: Int, budget :: Int, headroom :: Int) -> Bool {
433 projected_load(active, delta) <= budget_total(budget, headroom)
434 }
435 "#;
436 let prog = parse_source(host_src).unwrap();
437 let stages = lex_ast::canonicalize_program(&prog);
438 let bc = compile_program(&stages);
439
440 let spec = parse_spec(r#"
441 spec gated_budget {
442 forall active :: Int, delta :: Int, budget :: Int, headroom :: Int :
443 under_budget(active, delta, budget, headroom)
444 }
445 "#).unwrap();
446 let bindings = b([
447 ("active", Value::Int(40)),
448 ("delta", Value::Int(15)),
449 ("budget", Value::Int(60)),
450 ("headroom", Value::Int(0)),
451 ]);
452
453 let captured = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
454 let captured_for_factory = std::sync::Arc::clone(&captured);
455 let v = evaluate_gate_compiled_traced(
456 std::slice::from_ref(&spec),
457 &bindings,
458 &bc,
459 move || Box::new(CallRecorder {
460 captured: std::sync::Arc::clone(&captured_for_factory),
461 }),
462 );
463 assert_eq!(v, GateVerdict::Allow);
464
465 let calls = captured.lock().unwrap();
466 for expected in ["projected_load", "budget_total"] {
473 assert!(calls.iter().any(|c| c == expected),
474 "expected `{expected}` in captured calls; got {:?}", *calls);
475 }
476 }
477
478 #[test]
479 fn untraced_gate_path_unchanged() {
480 let spec = parse_spec("spec ok { forall x :: Int : x + 1 > x }").unwrap();
485 let v = evaluate_gate_compiled(
486 std::slice::from_ref(&spec),
487 &b([("x", Value::Int(5))]),
488 &compile_program(&lex_ast::canonicalize_program(&parse_source("").unwrap())),
489 );
490 assert_eq!(v, GateVerdict::Allow);
491 }
492}