1use crate::ast::*;
4use indexmap::IndexMap;
5use lex_ast::canonicalize_program;
6use lex_bytecode::{compile_program, vm::Vm, Value};
7use lex_runtime::{DefaultHandler, Policy};
8use lex_syntax::parse_source;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "snake_case")]
13pub enum ProofStatus { Proved, Counterexample, Inconclusive }
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Evidence {
17 pub method: String,
18 pub trials: u32,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub counterexample: Option<IndexMap<String, serde_json::Value>>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub note: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct CheckResult {
29 pub spec_id: String,
30 pub status: ProofStatus,
31 pub evidence: Evidence,
32}
33
34pub fn check_spec(spec: &Spec, lex_source: &str, trials: u32) -> CheckResult {
41 let spec_id = spec_id(spec);
42 let prog = match parse_source(lex_source) {
43 Ok(p) => p,
44 Err(e) => return inconclusive(spec_id, format!("parse: {e}")),
45 };
46 let stages = canonicalize_program(&prog);
47 if let Err(errs) = lex_types::check_program(&stages) {
48 return inconclusive(spec_id, format!("typecheck: {errs:?}"));
49 }
50 let bc = compile_program(&stages);
51
52 if spec.quantifiers.iter().any(|q| q.ty == SpecType::Float) {
56 return CheckResult {
57 spec_id,
58 status: ProofStatus::Inconclusive,
59 evidence: Evidence {
60 method: "randomized".into(),
61 trials: 0,
62 counterexample: None,
63 note: Some("randomized search inconclusive on Float quantifiers; use SMT (see to_smtlib)".into()),
64 },
65 };
66 }
67
68 let mut rng = DetRng::new(seed_from_spec(spec));
69 let policy = Policy::permissive();
70
71 for trial in 0..trials {
72 let mut bindings = IndexMap::new();
73 let mut skip = false;
74 for q in &spec.quantifiers {
75 let v = sample(&q.ty, &mut rng);
76 bindings.insert(q.name.clone(), v);
77 if let Some(c) = &q.constraint {
79 match eval(c, &bindings, &bc, &policy) {
80 Ok(Value::Bool(true)) => {}
81 Ok(Value::Bool(false)) => { skip = true; break; }
82 Ok(_) => return inconclusive(spec_id, format!("constraint did not return Bool (trial {trial})")),
83 Err(e) => return inconclusive(spec_id, format!("constraint eval failed: {e}")),
84 }
85 }
86 }
87 if skip { continue; }
88
89 match eval(&spec.body, &bindings, &bc, &policy) {
90 Ok(Value::Bool(true)) => continue,
91 Ok(Value::Bool(false)) => {
92 return CheckResult {
93 spec_id,
94 status: ProofStatus::Counterexample,
95 evidence: Evidence {
96 method: "randomized".into(),
97 trials: trial + 1,
98 counterexample: Some(bindings_to_json(&bindings)),
99 note: None,
100 },
101 };
102 }
103 Ok(other) => return inconclusive(spec_id, format!("body returned non-bool: {other:?}")),
104 Err(e) => return inconclusive(spec_id, format!("body eval failed: {e}")),
105 }
106 }
107
108 CheckResult {
109 spec_id,
110 status: ProofStatus::Proved,
111 evidence: Evidence {
112 method: "randomized".into(),
113 trials,
114 counterexample: None,
115 note: Some(format!("survived {trials} random trials; not a deductive proof")),
116 },
117 }
118}
119
120fn inconclusive(spec_id: String, note: impl Into<String>) -> CheckResult {
121 CheckResult {
122 spec_id,
123 status: ProofStatus::Inconclusive,
124 evidence: Evidence {
125 method: "randomized".into(),
126 trials: 0,
127 counterexample: None,
128 note: Some(note.into()),
129 },
130 }
131}
132
133fn spec_id(spec: &Spec) -> String {
134 use sha2::{Digest, Sha256};
135 let v = serde_json::to_value(spec).unwrap();
136 let s = lex_ast::canon_json::to_canonical_string(&v);
137 let mut h = Sha256::new();
138 h.update(s.as_bytes());
139 let r = h.finalize();
140 let mut hex = String::with_capacity(64);
141 for b in r { hex.push_str(&format!("{:02x}", b)); }
142 hex
143}
144
145fn seed_from_spec(spec: &Spec) -> u64 {
146 use sha2::{Digest, Sha256};
147 let mut h = Sha256::new();
148 h.update(spec.name.as_bytes());
149 let r = h.finalize();
150 u64::from_le_bytes(r[..8].try_into().unwrap())
151}
152
153struct DetRng { state: u64 }
155impl DetRng {
156 fn new(seed: u64) -> Self { Self { state: seed.max(1) } }
157 fn next_u64(&mut self) -> u64 {
158 let mut x = self.state;
159 x ^= x << 13;
160 x ^= x >> 7;
161 x ^= x << 17;
162 self.state = x;
163 x
164 }
165 fn next_i64_in(&mut self, lo: i64, hi: i64) -> i64 {
166 if hi <= lo { return lo; }
167 let span = (hi - lo) as u64;
168 lo + (self.next_u64() % span) as i64
169 }
170}
171
172fn sample(ty: &SpecType, rng: &mut DetRng) -> Value {
173 match ty {
174 SpecType::Int => Value::Int(rng.next_i64_in(-1000, 1001)),
175 SpecType::Float => {
176 let n = rng.next_u64() as f64;
177 Value::Float((n / u64::MAX as f64) * 2000.0 - 1000.0)
178 }
179 SpecType::Bool => Value::Bool(rng.next_u64() & 1 == 0),
180 SpecType::Str => {
181 let n = rng.next_u64() % 6;
182 Value::Str(("ab".repeat(n as usize)).chars().take(n as usize).collect())
183 }
184 SpecType::Record { fields } => {
189 let mut out = indexmap::IndexMap::new();
190 for (name, fty) in fields {
191 out.insert(name.clone(), sample(fty, rng));
192 }
193 Value::Record(out)
194 }
195 SpecType::List { element } => {
201 let len = (rng.next_u64() % 9) as usize;
202 let mut out = Vec::with_capacity(len);
203 for _ in 0..len {
204 out.push(sample(element, rng));
205 }
206 Value::List(out)
207 }
208 SpecType::Named { name } => Value::Variant {
217 name: format!("__unsampled_{name}"),
218 args: Vec::new(),
219 },
220 }
221}
222
223fn eval(
224 e: &SpecExpr,
225 bindings: &IndexMap<String, Value>,
226 bc: &lex_bytecode::Program,
227 policy: &Policy,
228) -> Result<Value, String> {
229 match e {
230 SpecExpr::IntLit { value } => Ok(Value::Int(*value)),
231 SpecExpr::FloatLit { value } => Ok(Value::Float(*value)),
232 SpecExpr::BoolLit { value } => Ok(Value::Bool(*value)),
233 SpecExpr::StrLit { value } => Ok(Value::Str(value.clone())),
234 SpecExpr::Var { name } => bindings.get(name).cloned()
235 .ok_or_else(|| format!("unbound spec var `{name}`")),
236 SpecExpr::Let { name, value, body } => {
237 let v = eval(value, bindings, bc, policy)?;
238 let mut next = bindings.clone();
239 next.insert(name.clone(), v);
240 eval(body, &next, bc, policy)
241 }
242 SpecExpr::Not { expr } => match eval(expr, bindings, bc, policy)? {
243 Value::Bool(b) => Ok(Value::Bool(!b)),
244 other => Err(format!("not on non-bool: {other:?}")),
245 },
246 SpecExpr::BinOp { op, lhs, rhs } => {
247 let a = eval(lhs, bindings, bc, policy)?;
248 let b = eval(rhs, bindings, bc, policy)?;
249 apply_binop(*op, a, b)
250 }
251 SpecExpr::Call { func, args } => {
252 let mut argv = Vec::new();
254 for a in args { argv.push(eval(a, bindings, bc, policy)?); }
255 if let Some(v) = crate::gate::list_builtin(func, &argv) { return v; }
259 let handler = DefaultHandler::new(policy.clone());
261 let mut vm = Vm::with_handler(bc, Box::new(handler));
262 vm.call(func, argv).map_err(|e| format!("call `{func}`: {e}"))
263 }
264 SpecExpr::FieldAccess { value, field } => {
265 let v = eval(value, bindings, bc, policy)?;
270 match v {
271 Value::Record(fields) => fields.get(field).cloned().ok_or_else(|| {
272 let known: Vec<&str> = fields.keys().map(String::as_str).collect();
273 format!("field `{field}` missing on record (have: {})", known.join(", "))
274 }),
275 other => Err(format!("field access `.{field}` on non-record: {other:?}")),
276 }
277 }
278 SpecExpr::Index { list, index } => {
279 let xs = eval(list, bindings, bc, policy)?;
281 let i = eval(index, bindings, bc, policy)?;
282 let xs = match xs {
283 Value::List(xs) => xs,
284 other => return Err(format!("index on non-list: {other:?}")),
285 };
286 let i = match i {
287 Value::Int(n) => n,
288 other => return Err(format!("list index must be Int: {other:?}")),
289 };
290 if i < 0 || (i as usize) >= xs.len() {
291 return Err(format!("list index {i} out of bounds (length {})", xs.len()));
292 }
293 Ok(xs[i as usize].clone())
294 }
295 SpecExpr::Match { scrutinee, arms } => {
296 let v = eval(scrutinee, bindings, bc, policy)?;
298 for arm in arms {
299 if let Some(extra) = crate::gate::pattern_match(&arm.pattern, &v) {
300 let mut next = bindings.clone();
301 for (k, vv) in extra { next.insert(k, vv); }
302 return eval(&arm.body, &next, bc, policy);
303 }
304 }
305 Err(format!("non-exhaustive match: no arm matched value {v:?}"))
306 }
307 }
308}
309
310fn apply_binop(op: SpecOp, a: Value, b: Value) -> Result<Value, String> {
311 use SpecOp::*;
312 match (op, &a, &b) {
313 (Add, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x + y)),
314 (Sub, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x - y)),
315 (Mul, Value::Int(x), Value::Int(y)) => Ok(Value::Int(x * y)),
316 (Div, Value::Int(x), Value::Int(y)) if *y != 0 => Ok(Value::Int(x / y)),
317 (Mod, Value::Int(x), Value::Int(y)) if *y != 0 => Ok(Value::Int(x % y)),
318 (Add, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x + y)),
319 (Sub, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x - y)),
320 (Mul, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x * y)),
321 (Div, Value::Float(x), Value::Float(y)) => Ok(Value::Float(x / y)),
322 (Eq, x, y) => Ok(Value::Bool(x == y)),
323 (Neq, x, y) => Ok(Value::Bool(x != y)),
324 (Lt, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x < y)),
325 (Le, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x <= y)),
326 (Gt, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x > y)),
327 (Ge, Value::Int(x), Value::Int(y)) => Ok(Value::Bool(x >= y)),
328 (Lt, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x < y)),
329 (Le, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x <= y)),
330 (Gt, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x > y)),
331 (Ge, Value::Float(x), Value::Float(y)) => Ok(Value::Bool(x >= y)),
332 (And, Value::Bool(x), Value::Bool(y)) => Ok(Value::Bool(*x && *y)),
333 (Or, Value::Bool(x), Value::Bool(y)) => Ok(Value::Bool(*x || *y)),
334 _ => Err(format!("invalid binop {op:?} on {a:?}, {b:?}")),
335 }
336}
337
338fn bindings_to_json(b: &IndexMap<String, Value>) -> IndexMap<String, serde_json::Value> {
339 let mut out = IndexMap::new();
340 for (k, v) in b { out.insert(k.clone(), value_to_json(v)); }
341 out
342}
343
344fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }