Skip to main content

logicaffeine_compile/extraction/
fol_model.rs

1//! FOL → runnable Rust model-checker / temporal monitor.
2//!
3//! Compiles a first-order (optionally temporal) formula into a self-contained,
4//! reusable Rust module: a `World` you populate with individuals + facts, and a
5//! `holds(...)` that evaluates the rule against it — so a rule/invariant can be
6//! encoded once and *used* against your own state.
7//!
8//! - Quantifiers: ∀→`.all`, ∃→`.any` over `World::domain`.
9//! - Predicates: table lookups (`World::pred`).
10//! - Events (Neo-Davidsonian, e.g. "read"): `∃e. read(e) ∧ Agent(e,x) ∧ Theme(e,y)`
11//!   → an existential over the domain treating events as first-class entities.
12//! - Temporal (`always`/`eventually`/`next`/`until`): a finite-trace **monitor**
13//!   over `&[World]` (Always→all states, Eventually→any, …) plus an incremental
14//!   `Monitor`. Pure O(trace) evaluation — no SMT, no `std::time`, no randomness,
15//!   WASM-safe. (The native automaton path is `logicaffeine_verify::automata`,
16//!   which carries a Z3 crate dep; this mirrors its semantics WASM-safely.)
17
18use logicaffeine_proof::{ProofExpr, ProofTerm};
19use std::collections::BTreeSet;
20
21/// Thematic-role priority — fixes the argument order of an event relation so the
22/// emitted Rust is deterministic and reads naturally (`read(Agent, Theme)`).
23const ROLE_ORDER: &[&str] = &[
24    "Agent", "Patient", "Theme", "Recipient", "Goal", "Source", "Instrument",
25    "Location", "Time", "Manner", "Result", "Depictive",
26];
27
28/// Emit a self-contained Rust rule module for `premises ⊨ goal` (empty `premises`
29/// = check the single formula `goal`). `english`/`fol` are echoed as doc comments.
30pub fn fol_to_model_checker(
31    premises: &[ProofExpr],
32    goal: &ProofExpr,
33    english: &str,
34    fol: &str,
35) -> String {
36    fol_to_model_checker_impl(premises, goal, english, fol, true)
37}
38
39/// Like [`fol_to_model_checker`], but emits NO demo `fn main` — the form bundled
40/// into an imperative program's `mod proven` (which has its own `main`). Produces
41/// only the `World` + `holds` (+ `Monitor`) library items.
42pub fn fol_to_model_checker_module(
43    premises: &[ProofExpr],
44    goal: &ProofExpr,
45    english: &str,
46    fol: &str,
47) -> String {
48    fol_to_model_checker_impl(premises, goal, english, fol, false)
49}
50
51fn fol_to_model_checker_impl(
52    premises: &[ProofExpr],
53    goal: &ProofExpr,
54    english: &str,
55    fol: &str,
56    emit_main: bool,
57) -> String {
58    let temporal = premises.iter().any(contains_temporal) || contains_temporal(goal);
59
60    let mut consts: BTreeSet<String> = BTreeSet::new();
61    let mut facts: BTreeSet<(String, Vec<String>)> = BTreeSet::new();
62    for p in premises {
63        collect(p, &mut consts, &mut facts);
64    }
65    collect(goal, &mut consts, &mut facts);
66
67    let header = doc_header(english, fol);
68    if temporal {
69        emit_temporal_module(premises, goal, &consts, &facts, &header, emit_main)
70    } else {
71        emit_world_module(premises, goal, &consts, &facts, &header, emit_main)
72    }
73}
74
75// --- non-temporal: a single World + holds(&World) ----------------------------
76
77fn emit_world_module(
78    premises: &[ProofExpr],
79    goal: &ProofExpr,
80    consts: &BTreeSet<String>,
81    facts: &BTreeSet<(String, Vec<String>)>,
82    header: &str,
83    emit_main: bool,
84) -> String {
85    let mut body = entailment(premises, goal, &|e| emit_expr(e, "w"));
86    // Universally close any free variable over the domain so the formula is a
87    // closed, evaluable Rust expression (no dangling `v_x`).
88    for f in free_vars_of(premises, goal) {
89        body = format!("w.domain.iter().all(|&{}| {})", sanitize_var(&f), body);
90    }
91    let main = if emit_main {
92        format!(
93            "\nfn main() {{\n    let w = {demo};\n    println!(\"holds = {{}}\", holds(&w));\n}}\n",
94            demo = demo_world(consts, facts),
95        )
96    } else {
97        String::new()
98    };
99    format!(
100        "{header}{world}\n\
101         /// Evaluate the rule against a world.\n\
102         pub fn holds(w: &World) -> bool {{\n    {body}\n}}\n{main}",
103        header = header,
104        world = WORLD_DEF,
105        body = body,
106        main = main,
107    )
108}
109
110// --- temporal: a trace of Worlds + holds(&[World]) + Monitor -----------------
111
112fn emit_temporal_module(
113    premises: &[ProofExpr],
114    goal: &ProofExpr,
115    consts: &BTreeSet<String>,
116    facts: &BTreeSet<(String, Vec<String>)>,
117    header: &str,
118    emit_main: bool,
119) -> String {
120    let mut body = entailment(premises, goal, &|e| emit_expr_trace(e, "0", 0));
121    // Close free variables over the first state's domain.
122    for f in free_vars_of(premises, goal) {
123        body = format!("trace.first().map_or(true, |w| w.domain.iter().all(|&{}| {}))", sanitize_var(&f), body);
124    }
125    let demo = demo_world(consts, facts);
126    let main = if emit_main {
127        format!(
128            "fn main() {{\n    \
129             let trace = vec![{demo}, {demo}];\n    \
130             println!(\"holds = {{}}\", holds(&trace));\n}}\n",
131            demo = demo,
132        )
133    } else {
134        String::new()
135    };
136    format!(
137        "{header}{world}\n\
138         /// Evaluate the temporal rule over a finite trace of worlds.\n\
139         pub fn holds(trace: &[World]) -> bool {{\n    {body}\n}}\n\n\
140         /// Incremental monitor: feed worlds as they happen.\n\
141         #[derive(Default)]\n\
142         pub struct Monitor {{ trace: Vec<World> }}\n\n\
143         impl Monitor {{\n    \
144         pub fn new() -> Self {{ Self::default() }}\n    \
145         /// Append a world and report whether the rule still holds over the trace so far.\n    \
146         pub fn step(&mut self, w: World) -> bool {{ self.trace.push(w); holds(&self.trace) }}\n}}\n\n\
147         {main}",
148        header = header,
149        world = WORLD_DEF,
150        body = body,
151        main = main,
152    )
153}
154
155/// `premises ⊨ goal` as a Rust bool expression, given a per-formula emitter.
156fn entailment(premises: &[ProofExpr], goal: &ProofExpr, emit: &dyn Fn(&ProofExpr) -> String) -> String {
157    let g = emit(goal);
158    if premises.is_empty() {
159        g
160    } else {
161        let prem: Vec<String> = premises.iter().map(|p| emit(p)).collect();
162        format!("(!({}) || {})", prem.join(" && "), g)
163    }
164}
165
166/// The reusable `World` type + ergonomic builder + `pred` lookup.
167const WORLD_DEF: &str = "\
168use std::collections::HashSet;\n\n\
169/// A finite world: named individuals (including events) and the facts that hold.\n\
170#[derive(Default, Clone)]\n\
171pub struct World {\n    \
172pub domain: Vec<&'static str>,\n    \
173pub facts: HashSet<(&'static str, Vec<&'static str>)>,\n}\n\n\
174impl World {\n    \
175pub fn new() -> Self { Self::default() }\n    \
176/// Add a named individual to the domain.\n    \
177pub fn individual(mut self, name: &'static str) -> Self {\n        \
178if !self.domain.contains(&name) { self.domain.push(name); }\n        \
179self\n    }\n    \
180/// Assert `name(args...)`; any new individuals are added to the domain.\n    \
181pub fn fact(mut self, name: &'static str, args: &[&'static str]) -> Self {\n        \
182for &a in args { if !self.domain.contains(&a) { self.domain.push(a); } }\n        \
183self.facts.insert((name, args.to_vec()));\n        \
184self\n    }\n    \
185/// True iff `name(args...)` holds in this world.\n    \
186pub fn pred(&self, name: &'static str, args: &[&'static str]) -> bool {\n        \
187self.facts.contains(&(name, args.to_vec()))\n    }\n}\n";
188
189fn doc_header(english: &str, fol: &str) -> String {
190    let one_line = |s: &str| {
191        s.lines()
192            .map(|l| l.trim())
193            .filter(|l| !l.is_empty())
194            .collect::<Vec<_>>()
195            .join(" | ")
196    };
197    let mut h = String::from("// Auto-generated rule from logic — a reusable World + `holds`.\n");
198    let e = one_line(english);
199    if !e.is_empty() {
200        h.push_str(&format!("// English: {}\n", e));
201    }
202    let f = one_line(fol);
203    if !f.is_empty() {
204        h.push_str(&format!("// FOL:     {}\n", f));
205    }
206    h.push('\n');
207    h
208}
209
210/// A demo `World` (builder chain) seeded with the formula's ground facts.
211fn demo_world(consts: &BTreeSet<String>, facts: &BTreeSet<(String, Vec<String>)>) -> String {
212    let mut chain = String::from("World::new()");
213    for (name, args) in facts {
214        let a: Vec<String> = args.iter().map(|x| format!("{:?}", x)).collect();
215        chain.push_str(&format!(".fact({:?}, &[{}])", name, a.join(", ")));
216    }
217    // Individuals that never appear in a fact still belong to the domain.
218    let in_fact: BTreeSet<&String> = facts.iter().flat_map(|(_, a)| a.iter()).collect();
219    for c in consts {
220        if !in_fact.contains(c) {
221            chain.push_str(&format!(".individual({:?})", c));
222        }
223    }
224    chain
225}
226
227// --- single-world FOL evaluator (predicates read from `world`) ----------------
228
229fn emit_expr(e: &ProofExpr, world: &str) -> String {
230    match e {
231        ProofExpr::Predicate { name, args, .. } => {
232            let a: Vec<String> = args.iter().map(emit_term).collect();
233            format!("{}.pred({:?}, &[{}])", world, name, a.join(", "))
234        }
235        ProofExpr::Atom(s) => format!("{}.pred({:?}, &[])", world, s),
236        ProofExpr::Identity(a, b) => format!("({} == {})", emit_term(a), emit_term(b)),
237        ProofExpr::And(l, r) => format!("({} && {})", emit_expr(l, world), emit_expr(r, world)),
238        ProofExpr::Or(l, r) => format!("({} || {})", emit_expr(l, world), emit_expr(r, world)),
239        ProofExpr::Implies(l, r) => format!("(!{} || {})", emit_expr(l, world), emit_expr(r, world)),
240        ProofExpr::Iff(l, r) => format!("({} == {})", emit_expr(l, world), emit_expr(r, world)),
241        ProofExpr::Not(x) => format!("(!{})", emit_expr(x, world)),
242        ProofExpr::ForAll { variable, body } => format!(
243            "{}.domain.iter().all(|&{}| {})",
244            world,
245            sanitize_var(variable),
246            emit_expr(body, world)
247        ),
248        ProofExpr::Exists { variable, body } => format!(
249            "{}.domain.iter().any(|&{}| {})",
250            world,
251            sanitize_var(variable),
252            emit_expr(body, world)
253        ),
254        // Neo-Davidsonian event: ∃e. verb(e) ∧ Role(e, arg) ∧ … — events are
255        // domain entities, so this is an existential over the world.
256        ProofExpr::NeoEvent { event_var, verb, roles } => {
257            let e = sanitize_var(event_var);
258            let mut sorted = roles.clone();
259            sorted.sort_by_key(|(r, _)| {
260                ROLE_ORDER.iter().position(|x| x == r).unwrap_or(ROLE_ORDER.len())
261            });
262            let mut parts = vec![format!("{}.pred({:?}, &[{}])", world, verb, e)];
263            for (role, term) in &sorted {
264                parts.push(format!(
265                    "{}.pred({:?}, &[{}, {}])",
266                    world,
267                    role,
268                    e,
269                    emit_term(term)
270                ));
271            }
272            format!("{}.domain.iter().any(|&{}| {})", world, e, parts.join(" && "))
273        }
274        other => format!("true /* unsupported: {} */", variant_name(other)),
275    }
276}
277
278// --- temporal skeleton over a trace (delegates per-state to emit_expr) --------
279
280fn emit_expr_trace(e: &ProofExpr, idx: &str, depth: usize) -> String {
281    // A subformula with no temporal operator is evaluated at the current state.
282    if !contains_temporal(e) {
283        return emit_expr(e, &format!("(&trace[{}])", idx));
284    }
285    match e {
286        ProofExpr::Temporal { operator, body } => {
287            let v = format!("i{}", depth);
288            let inner = emit_expr_trace(body, &v, depth + 1);
289            match operator.as_str() {
290                "Always" => format!("({idx}..trace.len()).all(|{v}| {inner})", idx = idx, v = v, inner = inner),
291                "Eventually" | "Future" => {
292                    format!("({idx}..trace.len()).any(|{v}| {inner})", idx = idx, v = v, inner = inner)
293                }
294                "Past" => format!("(0..={idx}).any(|{v}| {inner})", idx = idx, v = v, inner = inner),
295                "Next" => format!(
296                    "{{ let {v} = {idx} + 1; {v} < trace.len() && {inner} }}",
297                    v = v,
298                    idx = idx,
299                    inner = inner
300                ),
301                _ => format!("({idx}..trace.len()).all(|{v}| {inner})", idx = idx, v = v, inner = inner),
302            }
303        }
304        ProofExpr::TemporalBinary { operator, left, right } => {
305            let k = format!("k{}", depth);
306            let j = format!("j{}", depth);
307            let r_at_k = emit_expr_trace(right, &k, depth + 1);
308            let l_at_j = emit_expr_trace(left, &j, depth + 1);
309            match operator.as_str() {
310                "Release" => format!(
311                    "({idx}..trace.len()).all(|{k}| {r} || ({idx}..{k}).any(|{j}| {l}))",
312                    idx = idx, k = k, j = j, r = r_at_k, l = l_at_j
313                ),
314                "WeakUntil" => {
315                    let l_all = emit_expr_trace(left, &j, depth + 1);
316                    format!(
317                        "(({idx}..trace.len()).all(|{j}| {l_all})) || (({idx}..trace.len()).any(|{k}| {r} && ({idx}..{k}).all(|{j}| {l})))",
318                        idx = idx, k = k, j = j, l_all = l_all, r = r_at_k, l = l_at_j
319                    )
320                }
321                // Until (and default): ∃k≥idx. right@k ∧ ∀idx≤j<k. left@j
322                _ => format!(
323                    "({idx}..trace.len()).any(|{k}| {r} && ({idx}..{k}).all(|{j}| {l}))",
324                    idx = idx, k = k, j = j, r = r_at_k, l = l_at_j
325                ),
326            }
327        }
328        ProofExpr::And(l, r) => {
329            format!("({} && {})", emit_expr_trace(l, idx, depth), emit_expr_trace(r, idx, depth))
330        }
331        ProofExpr::Or(l, r) => {
332            format!("({} || {})", emit_expr_trace(l, idx, depth), emit_expr_trace(r, idx, depth))
333        }
334        ProofExpr::Implies(l, r) => {
335            format!("(!{} || {})", emit_expr_trace(l, idx, depth), emit_expr_trace(r, idx, depth))
336        }
337        ProofExpr::Iff(l, r) => {
338            format!("({} == {})", emit_expr_trace(l, idx, depth), emit_expr_trace(r, idx, depth))
339        }
340        ProofExpr::Not(x) => format!("(!{})", emit_expr_trace(x, idx, depth)),
341        // Temporal nested deeper than the skeleton handles (e.g. under a
342        // quantifier) → evaluate at the current state (deep operators degrade).
343        _ => emit_expr(e, &format!("(&trace[{}])", idx)),
344    }
345}
346
347/// Free variables of `premises ⊨ goal` — variables referenced but not bound by an
348/// enclosing quantifier or event. Sorted for deterministic closure order.
349fn free_vars_of(premises: &[ProofExpr], goal: &ProofExpr) -> BTreeSet<String> {
350    let mut out = BTreeSet::new();
351    let mut bound: Vec<String> = Vec::new();
352    for p in premises {
353        free_vars(p, &mut bound, &mut out);
354    }
355    free_vars(goal, &mut bound, &mut out);
356    out
357}
358
359fn free_vars(e: &ProofExpr, bound: &mut Vec<String>, out: &mut BTreeSet<String>) {
360    match e {
361        ProofExpr::Predicate { args, .. } => {
362            for a in args {
363                free_term(a, bound, out);
364            }
365        }
366        ProofExpr::Identity(a, b) => {
367            free_term(a, bound, out);
368            free_term(b, bound, out);
369        }
370        ProofExpr::And(l, r)
371        | ProofExpr::Or(l, r)
372        | ProofExpr::Implies(l, r)
373        | ProofExpr::Iff(l, r) => {
374            free_vars(l, bound, out);
375            free_vars(r, bound, out);
376        }
377        ProofExpr::Not(x) => free_vars(x, bound, out),
378        ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body } => {
379            bound.push(variable.clone());
380            free_vars(body, bound, out);
381            bound.pop();
382        }
383        ProofExpr::Temporal { body, .. } => free_vars(body, bound, out),
384        ProofExpr::TemporalBinary { left, right, .. } => {
385            free_vars(left, bound, out);
386            free_vars(right, bound, out);
387        }
388        ProofExpr::NeoEvent { event_var, roles, .. } => {
389            bound.push(event_var.clone());
390            for (_, t) in roles {
391                free_term(t, bound, out);
392            }
393            bound.pop();
394        }
395        _ => {}
396    }
397}
398
399fn free_term(t: &ProofTerm, bound: &[String], out: &mut BTreeSet<String>) {
400    match t {
401        ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) => {
402            if !bound.contains(v) {
403                out.insert(v.clone());
404            }
405        }
406        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
407            for a in args {
408                free_term(a, bound, out);
409            }
410        }
411        ProofTerm::Constant(_) => {}
412    }
413}
414
415fn contains_temporal(e: &ProofExpr) -> bool {
416    match e {
417        ProofExpr::Temporal { .. } | ProofExpr::TemporalBinary { .. } => true,
418        ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) | ProofExpr::Iff(l, r) => {
419            contains_temporal(l) || contains_temporal(r)
420        }
421        ProofExpr::Not(x) => contains_temporal(x),
422        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => contains_temporal(body),
423        _ => false,
424    }
425}
426
427fn emit_term(t: &ProofTerm) -> String {
428    match t {
429        ProofTerm::Constant(c) => format!("{:?}", c),
430        ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) => sanitize_var(v),
431        ProofTerm::Function(name, _) => format!("{:?}", name),
432        ProofTerm::Group(_) => "\"?\"".to_string(),
433    }
434}
435
436fn sanitize_var(v: &str) -> String {
437    let cleaned: String = v
438        .chars()
439        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
440        .collect();
441    format!("v_{}", cleaned)
442}
443
444fn collect(e: &ProofExpr, consts: &mut BTreeSet<String>, facts: &mut BTreeSet<(String, Vec<String>)>) {
445    match e {
446        ProofExpr::Predicate { name, args, .. } => {
447            for a in args {
448                collect_term(a, consts);
449            }
450            let mut ground = Vec::new();
451            let mut all_const = true;
452            for a in args {
453                match a {
454                    ProofTerm::Constant(c) => ground.push(c.clone()),
455                    _ => {
456                        all_const = false;
457                        break;
458                    }
459                }
460            }
461            if all_const {
462                facts.insert((name.clone(), ground));
463            }
464        }
465        ProofExpr::Atom(s) => {
466            facts.insert((s.clone(), vec![]));
467        }
468        ProofExpr::Identity(a, b) => {
469            collect_term(a, consts);
470            collect_term(b, consts);
471        }
472        ProofExpr::And(l, r)
473        | ProofExpr::Or(l, r)
474        | ProofExpr::Implies(l, r)
475        | ProofExpr::Iff(l, r) => {
476            collect(l, consts, facts);
477            collect(r, consts, facts);
478        }
479        ProofExpr::Not(x) => collect(x, consts, facts),
480        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => collect(body, consts, facts),
481        ProofExpr::Temporal { body, .. } => collect(body, consts, facts),
482        ProofExpr::TemporalBinary { left, right, .. } => {
483            collect(left, consts, facts);
484            collect(right, consts, facts);
485        }
486        ProofExpr::NeoEvent { roles, .. } => {
487            for (_, term) in roles {
488                collect_term(term, consts);
489            }
490        }
491        _ => {}
492    }
493}
494
495fn collect_term(t: &ProofTerm, consts: &mut BTreeSet<String>) {
496    match t {
497        ProofTerm::Constant(c) => {
498            consts.insert(c.clone());
499        }
500        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
501            for a in args {
502                collect_term(a, consts);
503            }
504        }
505        _ => {}
506    }
507}
508
509fn variant_name(e: &ProofExpr) -> &'static str {
510    match e {
511        ProofExpr::Modal { .. } => "modal",
512        ProofExpr::Counterfactual { .. } => "counterfactual",
513        ProofExpr::Lambda { .. } => "lambda",
514        ProofExpr::App(..) => "application",
515        ProofExpr::Ctor { .. } => "constructor",
516        ProofExpr::Match { .. } => "match",
517        ProofExpr::Fixpoint { .. } => "fixpoint",
518        ProofExpr::Temporal { .. } | ProofExpr::TemporalBinary { .. } => "temporal",
519        _ => "construct",
520    }
521}