Skip to main content

aver/verify_law/
expand.rs

1//! Mode-parametrized expansion of `verify ... law` cases.
2//!
3//! A law is parameterized by `given` clauses; running it means picking
4//! values for each given, substituting them into the template lhs / rhs /
5//! when, and producing a flat list of cases. The parser already does this
6//! with the values the user wrote (`Declared` mode). Under
7//! `aver verify --hostile` we run the same expansion with the same
8//! template, but the value source for typed givens is augmented with
9//! the per-type boundary set (`Hostile` mode) — so the same code path
10//! produces the dual-run pair (declared cases vs hostile cases) without
11//! a second implementation.
12//!
13//! The parser keeps its own copy of declared-mode expansion for now to
14//! avoid a wider refactor in this commit. When the hostile pipeline is
15//! wired into vm_verify, both call sites will move here and the parser
16//! will delegate.
17
18use std::collections::HashMap;
19
20use crate::ast::{Expr, Literal, Spanned, VerifyGiven, VerifyGivenDomain, VerifyLaw};
21use crate::ast_rewrite::rewrite_idents_scoped;
22use crate::types::checker::hostile_values::boundary_exprs;
23use crate::types::parse_type_str_strict;
24
25/// Which values the expansion should draw from for each `given` clause.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ExpansionMode {
28    /// Use only the values written by the user (literal list or `IntRange`).
29    /// Mirrors the parser's expansion exactly.
30    Declared,
31    /// Use the declared values *plus* the boundary set for the parsed
32    /// type — `i64::MIN`, `i64::MAX`, `0`, `1`, `-1` for `Int`; empty,
33    /// long, NUL, multi-byte for `Str`, etc. Failures here that would
34    /// not arise under `Declared` reveal one-sided assumptions: either
35    /// the law silently relies on a nice-world / narrow-range
36    /// precondition (encode it as `when`), or the law is genuinely
37    /// universal and the failure is a bug.
38    Hostile,
39}
40
41/// One expanded case: lhs, rhs, the per-given binding map that produced
42/// it, and the substituted `when` guard if the law had one. The vm-verify
43/// runner takes ownership and produces VM helper fns from these. Origin
44/// flag distinguishes cases that came from the user's own values from
45/// cases the hostile expansion added.
46#[derive(Debug, Clone)]
47pub struct ExpandedCase {
48    pub lhs: Spanned<Expr>,
49    pub rhs: Spanned<Expr>,
50    pub guard: Option<Spanned<Expr>>,
51    pub bindings: Vec<(String, Spanned<Expr>)>,
52    pub from_hostile: bool,
53}
54
55/// Expand a `verify ... law` template under the chosen mode. Returns the
56/// flat list of cases ready for vm-verify plan construction. Cases from
57/// the declared values come first (in the same order the parser would
58/// produce them); when the mode is `Hostile`, additional cases follow
59/// with `from_hostile = true` and any combination already covered by the
60/// declared run is suppressed (no duplicate work).
61pub fn expand_law_cases(law: &VerifyLaw, mode: ExpansionMode) -> Vec<ExpandedCase> {
62    let declared_combos = collect_combinations(law, ExpansionMode::Declared);
63    match mode {
64        ExpansionMode::Declared => declared_combos
65            .into_iter()
66            .map(|combo| materialize_case(law, &combo, false))
67            .collect(),
68        ExpansionMode::Hostile => {
69            let mut out: Vec<ExpandedCase> = declared_combos
70                .iter()
71                .map(|combo| materialize_case(law, combo, false))
72                .collect();
73            let declared_set: std::collections::HashSet<Vec<String>> = declared_combos
74                .iter()
75                .map(|c| combination_signature(c))
76                .collect();
77            for combo in collect_combinations(law, ExpansionMode::Hostile) {
78                if declared_set.contains(&combination_signature(&combo)) {
79                    continue;
80                }
81                out.push(materialize_case(law, &combo, true));
82            }
83            out
84        }
85    }
86}
87
88fn collect_combinations(law: &VerifyLaw, mode: ExpansionMode) -> Vec<Vec<(String, Spanned<Expr>)>> {
89    let per_given: Vec<Vec<Spanned<Expr>>> = law
90        .givens
91        .iter()
92        .map(|g| values_for_given(g, mode))
93        .collect();
94    cartesian(&law.givens, &per_given)
95}
96
97fn values_for_given(g: &VerifyGiven, mode: ExpansionMode) -> Vec<Spanned<Expr>> {
98    let mut values = declared_values(&g.domain);
99    if mode == ExpansionMode::Hostile
100        && let Ok(ty) = parse_type_str_strict(&g.type_name)
101    {
102        // Only append boundary values that aren't already covered by
103        // the declared set — keeps the cartesian product tight and
104        // avoids re-running the same combination twice.
105        // `boundary_exprs` covers scalars *and* compound types (Option,
106        // Result, List, Tuple) through one recursive walk. Map/Vector
107        // and user-defined types return empty — hostile is a no-op for
108        // them. Dedup against declared set so a value the user already
109        // wrote is not re-run.
110        let existing: std::collections::HashSet<String> = values.iter().map(render_expr).collect();
111        for candidate in boundary_exprs(&ty) {
112            if !existing.contains(&render_expr(&candidate)) {
113                values.push(candidate);
114            }
115        }
116    }
117    values
118}
119
120fn declared_values(domain: &VerifyGivenDomain) -> Vec<Spanned<Expr>> {
121    match domain {
122        VerifyGivenDomain::Explicit(values) => values.clone(),
123        VerifyGivenDomain::IntRange { start, end } => (*start..=*end)
124            .map(|n| Spanned::bare(Expr::Literal(Literal::Int(n))))
125            .collect(),
126    }
127}
128
129fn cartesian(
130    givens: &[VerifyGiven],
131    per_given: &[Vec<Spanned<Expr>>],
132) -> Vec<Vec<(String, Spanned<Expr>)>> {
133    let mut out: Vec<Vec<(String, Spanned<Expr>)>> = vec![Vec::new()];
134    for (g, choices) in givens.iter().zip(per_given) {
135        let mut next = Vec::with_capacity(out.len() * choices.len().max(1));
136        for partial in &out {
137            for choice in choices {
138                let mut extended = partial.clone();
139                extended.push((g.name.clone(), choice.clone()));
140                next.push(extended);
141            }
142        }
143        out = next;
144    }
145    out
146}
147
148fn materialize_case(
149    law: &VerifyLaw,
150    bindings: &[(String, Spanned<Expr>)],
151    from_hostile: bool,
152) -> ExpandedCase {
153    let map: HashMap<&str, &Spanned<Expr>> =
154        bindings.iter().map(|(n, v)| (n.as_str(), v)).collect();
155    ExpandedCase {
156        lhs: substitute(&law.lhs, &map),
157        rhs: substitute(&law.rhs, &map),
158        guard: law.when.as_ref().map(|w| substitute(w, &map)),
159        bindings: bindings.to_vec(),
160        from_hostile,
161    }
162}
163
164fn substitute(expr: &Spanned<Expr>, bindings: &HashMap<&str, &Spanned<Expr>>) -> Spanned<Expr> {
165    rewrite_idents_scoped(expr, |name| bindings.get(name).map(|v| (*v).clone()))
166}
167
168/// Stable signature for a binding combination, used to suppress
169/// duplicates when the hostile set happens to contain a value the user
170/// already wrote.
171fn combination_signature(combo: &[(String, Spanned<Expr>)]) -> Vec<String> {
172    combo
173        .iter()
174        .map(|(n, v)| format!("{}={}", n, render_expr(v)))
175        .collect()
176}
177
178fn render_expr(expr: &Spanned<Expr>) -> String {
179    match &expr.node {
180        Expr::Literal(Literal::Int(i)) => i.to_string(),
181        Expr::Literal(Literal::Float(f)) => format!("{:?}", f),
182        Expr::Literal(Literal::Str(s)) => format!("{:?}", s),
183        Expr::Literal(Literal::Bool(b)) => b.to_string(),
184        Expr::Literal(Literal::Unit) => "Unit".to_string(),
185        // Non-literal declared values are rare (programmer wrote a
186        // constructor like `Option.None`); render through Debug so the
187        // signature is at least unique. Hostile mode never produces
188        // these, so dedup against declared still works.
189        other => format!("{:?}", other),
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn lit_int(n: i64) -> Spanned<Expr> {
198        Spanned::bare(Expr::Literal(Literal::Int(n)))
199    }
200
201    fn law_one_int(name: &str, ty: &str, declared: Vec<i64>) -> VerifyLaw {
202        VerifyLaw {
203            name: "test".to_string(),
204            givens: vec![VerifyGiven {
205                name: name.to_string(),
206                type_name: ty.to_string(),
207                domain: VerifyGivenDomain::Explicit(declared.into_iter().map(lit_int).collect()),
208            }],
209            when: None,
210            lhs: Spanned::bare(Expr::Ident(name.to_string())),
211            rhs: Spanned::bare(Expr::Ident(name.to_string())),
212            sample_guards: vec![],
213        }
214    }
215
216    #[test]
217    fn declared_mode_returns_only_user_values() {
218        let law = law_one_int("x", "Int", vec![1, 2]);
219        let cases = expand_law_cases(&law, ExpansionMode::Declared);
220        assert_eq!(cases.len(), 2);
221        assert!(cases.iter().all(|c| !c.from_hostile));
222    }
223
224    #[test]
225    fn hostile_mode_appends_boundary_set() {
226        let law = law_one_int("x", "Int", vec![5]);
227        let declared = expand_law_cases(&law, ExpansionMode::Declared);
228        let hostile = expand_law_cases(&law, ExpansionMode::Hostile);
229        assert_eq!(declared.len(), 1);
230        // Boundary set for Int is 5 values; user provided 5, which isn't
231        // in the boundary set, so all 5 boundary values are added.
232        assert_eq!(hostile.len(), 1 + 5);
233        assert!(!hostile[0].from_hostile);
234        assert!(hostile[1..].iter().all(|c| c.from_hostile));
235    }
236
237    #[test]
238    fn hostile_dedupes_against_declared() {
239        // User declared `0` — boundary set for Int also contains `0`,
240        // so it must not appear twice.
241        let law = law_one_int("x", "Int", vec![0]);
242        let hostile = expand_law_cases(&law, ExpansionMode::Hostile);
243        let zero_count = hostile
244            .iter()
245            .filter(|c| matches!(&c.lhs.node, Expr::Literal(Literal::Int(0))))
246            .count();
247        assert_eq!(zero_count, 1, "zero should appear exactly once");
248    }
249
250    #[test]
251    fn cartesian_two_givens() {
252        let law = VerifyLaw {
253            name: "two".to_string(),
254            givens: vec![
255                VerifyGiven {
256                    name: "x".to_string(),
257                    type_name: "Bool".to_string(),
258                    domain: VerifyGivenDomain::Explicit(vec![Spanned::bare(Expr::Literal(
259                        Literal::Bool(true),
260                    ))]),
261                },
262                VerifyGiven {
263                    name: "y".to_string(),
264                    type_name: "Int".to_string(),
265                    domain: VerifyGivenDomain::Explicit(vec![lit_int(1)]),
266                },
267            ],
268            when: None,
269            lhs: Spanned::bare(Expr::Ident("x".to_string())),
270            rhs: Spanned::bare(Expr::Ident("y".to_string())),
271            sample_guards: vec![],
272        };
273        let declared = expand_law_cases(&law, ExpansionMode::Declared);
274        assert_eq!(declared.len(), 1);
275        let hostile = expand_law_cases(&law, ExpansionMode::Hostile);
276        // Bool boundary = 2 values, Int boundary = 5 values.
277        // Cartesian: 2 × 5 = 10. Declared (true, 1) is in the cartesian,
278        // so total unique = 10 (1 declared + 9 hostile-only).
279        assert_eq!(hostile.iter().filter(|c| !c.from_hostile).count(), 1);
280        assert_eq!(hostile.len(), 10);
281    }
282
283    #[test]
284    fn unknown_type_falls_back_to_declared_only() {
285        // For a user-defined type the boundary set is empty; hostile
286        // mode degenerates to declared — no extra cases injected, no
287        // crash on unknown type names.
288        let law = law_one_int("x", "Shape", vec![1]);
289        let hostile = expand_law_cases(&law, ExpansionMode::Hostile);
290        assert_eq!(hostile.len(), 1);
291        assert!(!hostile[0].from_hostile);
292    }
293}