Skip to main content

logicaffeine_tv/
symexec.rs

1//! Big-step symbolic execution of the LOGOS Verifiable Core into `VerifyExpr`.
2//!
3//! The executor walks the AST and produces a [`SymSummary`]: the ordered sequence of
4//! `Show` outputs and a sticky "an observable error occurred" condition, each expressed
5//! as a `VerifyExpr` over the program's free inputs. Two summaries can then be compared
6//! for observable equivalence by the SMT backend ([`crate::equiv`]).
7//!
8//! ## Verifiable Core (this phase)
9//!
10//! Straight-line `Int`/`Bool` programs: integer literals, booleans, `+ - *`, signed
11//! comparisons, `== !=`, `and`/`or` (logical on `Bool`, bitwise on `Int`), `not`,
12//! `Let`, `Set`, and `Show … to show`. `Int` is modeled as a 64-bit bitvector so
13//! wrapping/overflow matches the interpreter's native `i64`. Anything outside the
14//! fragment yields [`Unsupported`] — never a silent or wrong result.
15
16use std::collections::{HashMap, VecDeque};
17
18use logicaffeine_compile::ast::stmt::{BinaryOpKind, Block, Expr, Literal, SelectBranch, Stmt};
19use logicaffeine_compile::Interner;
20use logicaffeine_verify::{BitVecOp, VerifyExpr};
21
22/// The non-native functions a program defines, keyed by name → (parameter names, body).
23/// Used to inline a `Launch a task to f(args)` at its spawn point in the determinate model.
24type FuncTable<'a> = HashMap<String, (Vec<String>, Block<'a>)>;
25
26/// SplitMix64 — a byte-for-byte mirror of `logicaffeine_runtime::seed::SeededRng`, so a
27/// `Select` winner the encoder draws matches the winner the interpreter's scheduler drew at
28/// the same seed. (Kept private and re-derived here to honor invariant I6: the TV crate
29/// shares the *spec* of the choice function, not a code path linked into any binary.)
30struct SplitMix64 {
31    state: u64,
32}
33
34impl SplitMix64 {
35    fn new(seed: u64) -> Self {
36        SplitMix64 { state: seed }
37    }
38
39    fn next_u64(&mut self) -> u64 {
40        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
41        let mut z = self.state;
42        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
43        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
44        z ^ (z >> 31)
45    }
46
47    /// A uniform index in `[0, n)`. Returns 0 *without drawing* when `n <= 1` — matching the
48    /// runtime exactly, so the entropy stream stays in lockstep.
49    fn below(&mut self, n: usize) -> usize {
50        if n <= 1 {
51            return 0;
52        }
53        (self.next_u64() % n as u64) as usize
54    }
55}
56
57/// Bit width used to model LOGOS `Int` (native `i64`).
58pub const INT_WIDTH: u32 = 64;
59
60/// A symbolic LOGOS value: an integer (64-bit bitvector), a boolean, or a channel handle.
61#[derive(Debug, Clone)]
62pub enum SymValue {
63    /// An integer value, as a width-64 bitvector `VerifyExpr`.
64    Int(VerifyExpr),
65    /// A boolean value, as a `Bool`-sorted `VerifyExpr`.
66    Bool(VerifyExpr),
67    /// A channel handle — an opaque id into [`State::channels`]. Not an SMT value; it is
68    /// the dataflow conduit the determinate concurrency fragment threads `Send`/`Receive`
69    /// through (and passes as a task argument).
70    Chan(usize),
71}
72
73/// The observable summary of a program: the ordered `Show` outputs plus the condition
74/// under which the program raised an observable error.
75#[derive(Debug, Clone)]
76pub struct SymSummary {
77    /// Ordered `Show` emissions, in execution order.
78    pub outputs: Vec<SymValue>,
79    /// Sticky condition (a `Bool` `VerifyExpr`) that is true exactly when the program
80    /// raised an observable error (e.g. division by zero).
81    pub errored: VerifyExpr,
82}
83
84/// A construct outside the currently-supported Verifiable Core. Carries a human reason
85/// so the validator can report `Unverified(reason)` rather than ever guessing.
86#[derive(Debug, Clone)]
87pub struct Unsupported(pub String);
88
89fn unsupported<T>(reason: impl Into<String>) -> Result<T, Unsupported> {
90    Err(Unsupported(reason.into()))
91}
92
93/// Mutable symbolic-execution state threaded through a straight-line block.
94struct State {
95    env: HashMap<String, SymValue>,
96    outputs: Vec<SymValue>,
97    errored: VerifyExpr,
98    /// Channel buffers, keyed by the opaque id a [`SymValue::Chan`] carries (FIFO histories).
99    channels: HashMap<usize, VecDeque<SymValue>>,
100    /// Allocates the next channel id, so distinct `Pipe`s never alias.
101    chan_counter: usize,
102    /// Present in *seeded* mode: resolves `Select` winners from the same SplitMix64 stream
103    /// the interpreter's scheduler uses. `None` ⇒ `Select` is `Unsupported` (no entropy).
104    rng: Option<SplitMix64>,
105}
106
107impl State {
108    fn new() -> Self {
109        State {
110            env: HashMap::new(),
111            outputs: Vec::new(),
112            errored: VerifyExpr::bool(false),
113            channels: HashMap::new(),
114            chan_counter: 0,
115            rng: None,
116        }
117    }
118}
119
120/// Symbolically execute a program (a sequence of statements) into a [`SymSummary`].
121pub fn execute(stmts: &[Stmt], interner: &Interner) -> Result<SymSummary, Unsupported> {
122    let funcs = collect_funcs(stmts, interner);
123    let mut state = State::new();
124    exec_block(&mut state, stmts, interner, &funcs)?;
125    Ok(SymSummary {
126        outputs: state.outputs,
127        errored: state.errored,
128    })
129}
130
131/// Symbolically execute a *nondeterministic* program under a fixed `seed`, resolving every
132/// `Select` winner from a SplitMix64 mirror of the scheduler's choice function. The result
133/// is cross-checked per-seed against `run_treewalker_concurrent_seeded` at the same seed,
134/// so a misaligned encoding surfaces as a disagreement — never a false proof.
135pub fn execute_seeded(stmts: &[Stmt], interner: &Interner, seed: u64) -> Result<SymSummary, Unsupported> {
136    let funcs = collect_funcs(stmts, interner);
137    let mut state = State::new();
138    state.rng = Some(SplitMix64::new(seed));
139    exec_block(&mut state, stmts, interner, &funcs)?;
140    Ok(SymSummary {
141        outputs: state.outputs,
142        errored: state.errored,
143    })
144}
145
146/// Index the program's non-native function definitions for task inlining.
147fn collect_funcs<'a>(stmts: &'a [Stmt<'a>], interner: &Interner) -> FuncTable<'a> {
148    let mut table = FuncTable::new();
149    for stmt in stmts {
150        if let Stmt::FunctionDef { name, params, body, is_native: false, .. } = stmt {
151            let param_names = params.iter().map(|(p, _)| interner.resolve(*p).to_string()).collect();
152            table.insert(interner.resolve(*name).to_string(), (param_names, *body));
153        }
154    }
155    table
156}
157
158fn exec_block(
159    state: &mut State,
160    stmts: &[Stmt],
161    interner: &Interner,
162    funcs: &FuncTable,
163) -> Result<(), Unsupported> {
164    for stmt in stmts {
165        exec_stmt(state, stmt, interner, funcs)?;
166    }
167    Ok(())
168}
169
170/// Resolve a pipe expression to its channel id, or report why it is not a channel.
171fn resolve_chan(state: &mut State, expr: &Expr, interner: &Interner) -> Result<usize, Unsupported> {
172    match eval(state, expr, interner)? {
173        SymValue::Chan(id) => Ok(id),
174        _ => unsupported("pipe operand is not a channel"),
175    }
176}
177
178fn exec_stmt(
179    state: &mut State,
180    stmt: &Stmt,
181    interner: &Interner,
182    funcs: &FuncTable,
183) -> Result<(), Unsupported> {
184    match stmt {
185        // Definitions are indexed up front by `collect_funcs`; nothing to execute here.
186        Stmt::FunctionDef { .. } => Ok(()),
187        // ---- Determinate concurrency fragment ----
188        Stmt::CreatePipe { var, .. } => {
189            let id = state.chan_counter;
190            state.chan_counter += 1;
191            state.channels.insert(id, VecDeque::new());
192            state.env.insert(interner.resolve(*var).to_string(), SymValue::Chan(id));
193            Ok(())
194        }
195        Stmt::SendPipe { value, pipe } => {
196            let v = eval(state, value, interner)?;
197            let id = resolve_chan(state, pipe, interner)?;
198            state.channels.get_mut(&id).expect("channel id is allocated").push_back(v);
199            Ok(())
200        }
201        Stmt::ReceivePipe { var, pipe } => {
202            let id = resolve_chan(state, pipe, interner)?;
203            let v = match state.channels.get_mut(&id).and_then(|q| q.pop_front()) {
204                Some(v) => v,
205                // A receive on an empty channel cannot be statically resolved under the
206                // single modeled schedule — bail honestly rather than guess.
207                None => return unsupported("receive on an empty channel (not statically resolvable)"),
208            };
209            state.env.insert(interner.resolve(*var).to_string(), v);
210            Ok(())
211        }
212        // A launched task runs to completion at its spawn point: in the determinate fragment
213        // the output is schedule-independent (Kahn), so this single schedule is canonical.
214        // Its `Send`s fill shared channels; a `Receive` it cannot satisfy bails Unsupported.
215        Stmt::LaunchTask { function, args } => {
216            let fname = interner.resolve(*function).to_string();
217            let (params, body) = match funcs.get(&fname) {
218                Some(f) => f.clone(),
219                None => return unsupported(format!("launch of unknown task '{fname}'")),
220            };
221            if params.len() != args.len() {
222                return unsupported(format!("task '{fname}' arity mismatch"));
223            }
224            let arg_vals = args
225                .iter()
226                .map(|a| eval(state, a, interner))
227                .collect::<Result<Vec<_>, _>>()?;
228            let saved = std::mem::take(&mut state.env);
229            for (p, v) in params.iter().zip(arg_vals) {
230                state.env.insert(p.clone(), v);
231            }
232            let r = exec_block(state, body, interner, funcs);
233            state.env = saved;
234            r
235        }
236        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
237            // Sequential in the determinate spec — each task runs without a block scope.
238            exec_block(state, tasks, interner, funcs)
239        }
240        // `Await the first of:` — nondeterministic, so only modeled in seeded mode. Ready
241        // arms are the `Receive`s whose channel already holds a value (readiness is static
242        // once prior tasks have run eagerly); the winner is `below(ready_count)` drawn from
243        // the same SplitMix64 the scheduler uses. With no ready receive, the timeout arm
244        // fires (deterministic, no draw). The per-seed cross-check is the soundness net.
245        Stmt::Select { branches } => {
246            if state.rng.is_none() {
247                return unsupported("Select requires seeded mode (nondeterministic)");
248            }
249            // Indices of receive arms whose modeled channel is non-empty.
250            let mut ready: Vec<usize> = Vec::new();
251            for (i, b) in branches.iter().enumerate() {
252                if let SelectBranch::Receive { pipe, .. } = b {
253                    let id = resolve_chan(state, pipe, interner)?;
254                    if state.channels.get(&id).map(|q| !q.is_empty()).unwrap_or(false) {
255                        ready.push(i);
256                    }
257                }
258            }
259            let winner = if !ready.is_empty() {
260                let k = state.rng.as_mut().unwrap().below(ready.len());
261                ready[k]
262            } else {
263                // No receive ready ⇒ the timeout arm fires. Find the first one.
264                match branches.iter().position(|b| matches!(b, SelectBranch::Timeout { .. })) {
265                    Some(i) => i,
266                    None => return unsupported("Select with no ready arm and no timeout would block"),
267                }
268            };
269            match &branches[winner] {
270                SelectBranch::Receive { var, pipe, body } => {
271                    let id = resolve_chan(state, pipe, interner)?;
272                    let v = state
273                        .channels
274                        .get_mut(&id)
275                        .and_then(|q| q.pop_front())
276                        .ok_or_else(|| Unsupported("Select winner channel empty".into()))?;
277                    state.env.insert(interner.resolve(*var).to_string(), v);
278                    exec_block(state, body, interner, funcs)
279                }
280                SelectBranch::Timeout { body, .. } => exec_block(state, body, interner, funcs),
281            }
282        }
283        Stmt::Let { var, value, .. } => {
284            let v = eval(state, value, interner)?;
285            state.env.insert(interner.resolve(*var).to_string(), v);
286            Ok(())
287        }
288        Stmt::Set { target, value } => {
289            let name = interner.resolve(*target).to_string();
290            if !state.env.contains_key(&name) {
291                return unsupported(format!("Set to variable '{name}' not in scope"));
292            }
293            let v = eval(state, value, interner)?;
294            state.env.insert(name, v);
295            Ok(())
296        }
297        Stmt::Show { object, recipient } => {
298            // Only `Show <expr> to show` (console) is in-fragment; showing to a
299            // function is an effectful call we do not model yet.
300            if let Expr::Identifier(sym) = recipient {
301                if interner.resolve(*sym) == "show" {
302                    let v = eval(state, object, interner)?;
303                    state.outputs.push(v);
304                    return Ok(());
305                }
306            }
307            unsupported("Show to a non-console recipient")
308        }
309        other => unsupported(format!("statement {}", stmt_kind(other))),
310    }
311}
312
313fn eval(state: &mut State, expr: &Expr, interner: &Interner) -> Result<SymValue, Unsupported> {
314    match expr {
315        Expr::Literal(Literal::Number(n)) => {
316            Ok(SymValue::Int(VerifyExpr::bv_const(INT_WIDTH, *n as u64)))
317        }
318        Expr::Literal(Literal::Boolean(b)) => Ok(SymValue::Bool(VerifyExpr::bool(*b))),
319        Expr::Literal(_) => unsupported("non-Int/Bool literal"),
320        Expr::Identifier(sym) => {
321            let name = interner.resolve(*sym);
322            state
323                .env
324                .get(name)
325                .cloned()
326                .ok_or_else(|| Unsupported(format!("reference to unbound variable '{name}'")))
327        }
328        Expr::BinaryOp { op, left, right } => {
329            let l = eval(state, left, interner)?;
330            let r = eval(state, right, interner)?;
331            match op {
332                // Division / modulo: by-zero is an observable error in the interpreter,
333                // so record `divisor == 0` into the sticky error condition. The result
334                // value past that point is irrelevant (observable equivalence only
335                // compares outputs when neither side errored).
336                BinaryOpKind::Divide | BinaryOpKind::Modulo => {
337                    let (a, b) = match (l, r) {
338                        (SymValue::Int(a), SymValue::Int(b)) => (a, b),
339                        _ => return unsupported("division on non-Int operands"),
340                    };
341                    let div_by_zero = VerifyExpr::bv_binary(
342                        BitVecOp::Eq,
343                        b.clone(),
344                        VerifyExpr::bv_const(INT_WIDTH, 0),
345                    );
346                    state.errored = VerifyExpr::or(state.errored.clone(), div_by_zero);
347                    let bvop = if matches!(op, BinaryOpKind::Divide) {
348                        BitVecOp::SDiv
349                    } else {
350                        BitVecOp::SRem
351                    };
352                    Ok(SymValue::Int(VerifyExpr::bv_binary(bvop, a, b)))
353                }
354                _ => apply_binop(*op, l, r),
355            }
356        }
357        Expr::Not { operand } => match eval(state, operand, interner)? {
358            SymValue::Bool(e) => Ok(SymValue::Bool(VerifyExpr::not(e))),
359            // Bitwise NOT on an i64: ~x = x XOR 0xFFFF_FFFF_FFFF_FFFF.
360            SymValue::Int(e) => Ok(SymValue::Int(VerifyExpr::bv_binary(
361                BitVecOp::Xor,
362                e,
363                VerifyExpr::bv_const(INT_WIDTH, u64::MAX),
364            ))),
365            SymValue::Chan(_) => unsupported("`not` on a channel"),
366        },
367        other => unsupported(format!("expression {}", expr_kind(other))),
368    }
369}
370
371fn apply_binop(op: BinaryOpKind, l: SymValue, r: SymValue) -> Result<SymValue, Unsupported> {
372    use BinaryOpKind::*;
373    use SymValue::{Bool, Int};
374    match (op, l, r) {
375        // ---- Integer arithmetic (wrapping, two's complement) ----
376        (Add, Int(a), Int(b)) => Ok(Int(VerifyExpr::bv_binary(BitVecOp::Add, a, b))),
377        (Subtract, Int(a), Int(b)) => Ok(Int(VerifyExpr::bv_binary(BitVecOp::Sub, a, b))),
378        (Multiply, Int(a), Int(b)) => Ok(Int(VerifyExpr::bv_binary(BitVecOp::Mul, a, b))),
379
380        // ---- Signed integer comparison ----
381        (Lt, Int(a), Int(b)) => Ok(Bool(VerifyExpr::bv_binary(BitVecOp::SLt, a, b))),
382        (Gt, Int(a), Int(b)) => Ok(Bool(VerifyExpr::bv_binary(BitVecOp::SLt, b, a))),
383        (LtEq, Int(a), Int(b)) => Ok(Bool(VerifyExpr::bv_binary(BitVecOp::SLe, a, b))),
384        (GtEq, Int(a), Int(b)) => Ok(Bool(VerifyExpr::bv_binary(BitVecOp::SLe, b, a))),
385
386        // ---- Equality (Int via bv, Bool via iff) ----
387        (Eq, Int(a), Int(b)) => Ok(Bool(VerifyExpr::bv_binary(BitVecOp::Eq, a, b))),
388        (Eq, Bool(a), Bool(b)) => Ok(Bool(VerifyExpr::iff(a, b))),
389        (NotEq, Int(a), Int(b)) => Ok(Bool(VerifyExpr::not(VerifyExpr::bv_binary(
390            BitVecOp::Eq,
391            a,
392            b,
393        )))),
394        (NotEq, Bool(a), Bool(b)) => Ok(Bool(VerifyExpr::not(VerifyExpr::iff(a, b)))),
395
396        // ---- And / Or (logical on Bool, bitwise on Int — matches the interpreter) ----
397        (And, Bool(a), Bool(b)) => Ok(Bool(VerifyExpr::and(a, b))),
398        (And, Int(a), Int(b)) => Ok(Int(VerifyExpr::bv_binary(BitVecOp::And, a, b))),
399        (Or, Bool(a), Bool(b)) => Ok(Bool(VerifyExpr::or(a, b))),
400        (Or, Int(a), Int(b)) => Ok(Int(VerifyExpr::bv_binary(BitVecOp::Or, a, b))),
401
402        (op, _, _) => unsupported(format!("operator {op:?} on these operand types")),
403    }
404}
405
406fn stmt_kind(s: &Stmt) -> &'static str {
407    match s {
408        Stmt::If { .. } => "If",
409        Stmt::While { .. } => "While",
410        Stmt::Repeat { .. } => "Repeat",
411        Stmt::Return { .. } => "Return",
412        Stmt::Inspect { .. } => "Inspect",
413        Stmt::FunctionDef { .. } => "FunctionDef",
414        Stmt::Call { .. } => "Call",
415        _ => "<other>",
416    }
417}
418
419fn expr_kind(e: &Expr) -> &'static str {
420    match e {
421        Expr::Call { .. } => "Call",
422        Expr::Index { .. } => "Index",
423        Expr::FieldAccess { .. } => "FieldAccess",
424        Expr::List(_) => "List",
425        Expr::InterpolatedString(_) => "InterpolatedString",
426        _ => "<other>",
427    }
428}