Skip to main content

logicaffeine_tv/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub mod equiv;
4pub mod parse;
5pub mod symexec;
6pub mod verdict;
7
8use logicaffeine_compile::compile::interpret_program;
9use logicaffeine_verify::{BitVecOp, VerifyExpr};
10
11pub use symexec::{SymSummary, SymValue};
12pub use verdict::{SoundnessReport, TvError};
13
14/// Parse `source` and symbolically execute it into a [`SymSummary`] over the LOGOS
15/// semantics. Does not run the optimizer (the source-of-truth side).
16pub fn summarize_logos(source: &str) -> Result<SymSummary, TvError> {
17    match parse::with_program(source, false, symexec::execute) {
18        Ok(Ok(summary)) => Ok(summary),
19        Ok(Err(symexec::Unsupported(reason))) => Err(TvError::Unsupported(reason)),
20        Err(e) => Err(TvError::Parse(e)),
21    }
22}
23
24/// Cross-validate the LOGOS encoder against the tree-walking interpreter on one program.
25///
26/// Runs the program through `interpret_program` (the independent ground truth) and the
27/// symbolic encoder, then proves with Z3 that they agree on the full observable behavior
28/// — the ordered `Show` outputs and whether an error was raised. This is the meta-oracle
29/// that makes the encoder trustworthy: a buggy encoder is caught here, not masked by a
30/// downstream equivalence that "proves" two wrong things equal.
31pub fn check_encoder_sound(source: &str) -> SoundnessReport {
32    // Gate on the determinacy classifier (FINISH_INTERPRETER invariant I4). A *Determinate*
33    // program's output is schedule-independent (Kahn), so a single encoded schedule is
34    // canonical and reuses the existing equivalence with zero new obligations. A
35    // *Nondeterminate* program is validated by seeded replay — encoder(seed) vs
36    // interpreter(seed) over a seed sweep, the `Select` winner drawn from the same SplitMix64.
37    match logicaffeine_compile::classify_source(source) {
38        Err(e) => return SoundnessReport::ParseFailed { detail: format!("{e:?}") },
39        Ok(logicaffeine_compile::concurrency::Determinacy::Nondeterminate { .. }) => {
40            return check_seeded_sweep(source);
41        }
42        Ok(logicaffeine_compile::concurrency::Determinacy::Determinate) => {}
43    }
44
45    let summary = match summarize_logos(source) {
46        Ok(s) => s,
47        Err(TvError::Unsupported(reason)) => return SoundnessReport::Unsupported { reason },
48        Err(TvError::Parse(e)) => {
49            return SoundnessReport::ParseFailed {
50                detail: format!("{e:?}"),
51            }
52        }
53    };
54    check_against_interp(&summary, interpret_program(source).map_err(|e| format!("{e:?}")))
55}
56
57/// Prove the encoder's `summary` matches a single interpreter run (`Ok(output)`, or
58/// `Err(msg)` if the interpreter raised). The shared core of the determinate and
59/// seeded-replay checks. Returns [`SoundnessReport::Agrees`]/[`SoundnessReport::Disagrees`].
60fn check_against_interp(summary: &SymSummary, interp: Result<String, String>) -> SoundnessReport {
61    match interp {
62        Err(e) => {
63            // Interpreter raised → the encoder must prove the error condition holds.
64            if equiv::is_valid(&summary.errored) {
65                SoundnessReport::Agrees
66            } else {
67                SoundnessReport::Disagrees {
68                    detail: format!("interpreter errored ({e}) but encoder did not prove `errored`"),
69                }
70            }
71        }
72        Ok(out) => {
73            // Interpreter succeeded → the encoder must prove it does *not* error, and the
74            // outputs must match position-for-position.
75            if !equiv::is_valid(&VerifyExpr::not(summary.errored.clone())) {
76                return SoundnessReport::Disagrees {
77                    detail: "encoder admits an error on an input where the interpreter succeeded"
78                        .to_string(),
79                };
80            }
81            compare_outputs(&summary.outputs, &out)
82        }
83    }
84}
85
86/// Validate a nondeterministic program by seeded replay over a fixed seed sweep. The encoder
87/// and interpreter are both pinned to each seed; if they provably agree at every seed ⇒
88/// [`SoundnessReport::SeedReplayAgrees`]; any divergence ⇒ [`SoundnessReport::SeedReplayDisagrees`]
89/// (the per-seed cross-check makes a false agreement impossible); a construct outside the
90/// seeded fragment ⇒ [`SoundnessReport::Unsupported`].
91fn check_seeded_sweep(source: &str) -> SoundnessReport {
92    const SEEDS: [u64; 5] = [0, 1, 2, 7, 42];
93    for seed in SEEDS {
94        let summary = match parse::with_program(source, false, |stmts, interner| {
95            symexec::execute_seeded(stmts, interner, seed)
96        }) {
97            Ok(Ok(s)) => s,
98            Ok(Err(symexec::Unsupported(reason))) => return SoundnessReport::Unsupported { reason },
99            Err(e) => return SoundnessReport::ParseFailed { detail: format!("{e:?}") },
100        };
101        let run = logicaffeine_compile::run_treewalker_concurrent_seeded(source, seed);
102        let interp = match run.error {
103            Some(e) => Err(e),
104            None => Ok(run.lines.join("\n")),
105        };
106        match check_against_interp(&summary, interp) {
107            SoundnessReport::Agrees => continue,
108            SoundnessReport::Disagrees { detail } => {
109                return SoundnessReport::SeedReplayDisagrees {
110                    detail: format!("seed {seed}: {detail}"),
111                }
112            }
113            other => return other, // Unsupported / ParseFailed propagate as-is
114        }
115    }
116    SoundnessReport::SeedReplayAgrees
117}
118
119/// What the interpreter's textual output line is expected to be.
120enum Expected {
121    Int(i64),
122    Bool(bool),
123}
124
125fn parse_expected(line: &str) -> Option<Expected> {
126    match line {
127        "true" => Some(Expected::Bool(true)),
128        "false" => Some(Expected::Bool(false)),
129        _ => line.parse::<i64>().ok().map(Expected::Int),
130    }
131}
132
133fn compare_outputs(outputs: &[SymValue], interp_out: &str) -> SoundnessReport {
134    let lines: Vec<&str> = if interp_out.is_empty() {
135        Vec::new()
136    } else {
137        interp_out.split('\n').collect()
138    };
139
140    if outputs.len() != lines.len() {
141        return SoundnessReport::Disagrees {
142            detail: format!(
143                "output count: encoder produced {} line(s), interpreter produced {} ({:?})",
144                outputs.len(),
145                lines.len(),
146                lines
147            ),
148        };
149    }
150
151    for (i, (slot, line)) in outputs.iter().zip(lines.iter()).enumerate() {
152        let expected = match parse_expected(line) {
153            Some(e) => e,
154            None => {
155                return SoundnessReport::Unsupported {
156                    reason: format!("non-Int/Bool output line {i}: {line:?}"),
157                }
158            }
159        };
160        let pred = match (slot, expected) {
161            (SymValue::Int(e), Expected::Int(n)) => {
162                VerifyExpr::bv_binary(BitVecOp::Eq, e.clone(), VerifyExpr::bv_const(64, n as u64))
163            }
164            (SymValue::Bool(e), Expected::Bool(b)) => VerifyExpr::iff(e.clone(), VerifyExpr::bool(b)),
165            (slot, _) => {
166                return SoundnessReport::Disagrees {
167                    detail: format!(
168                        "output {i}: kind mismatch (encoder {} vs interpreter {line:?})",
169                        kind_of(slot)
170                    ),
171                }
172            }
173        };
174        if !equiv::is_valid(&pred) {
175            return SoundnessReport::Disagrees {
176                detail: format!("output {i}: encoder value disagrees with interpreter {line:?}"),
177            };
178        }
179    }
180
181    SoundnessReport::Agrees
182}
183
184fn kind_of(v: &SymValue) -> &'static str {
185    match v {
186        SymValue::Int(_) => "Int",
187        SymValue::Bool(_) => "Bool",
188        SymValue::Chan(_) => "Chan",
189    }
190}