1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ExpansionMode {
28 Declared,
31 Hostile,
39}
40
41#[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
55pub 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 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
168fn 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 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 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 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 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 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}