Skip to main content

omena_smt/
lib.rs

1//! SMT-encoded cascade verification contracts.
2//!
3//! The crate keeps the M4-gamma three-layer split explicit:
4//! L1 cascade proof primitives remain in `omena-cascade`, L2 refinement
5//! delegates live beside the cascade crate, and this crate owns L3 encoding,
6//! proof contracts, backend selection, and unsat-core audit metadata.
7//!
8//! Most obligations are propositional (`require:name=bool`) conjunctions the
9//! Rust code already decided; the [`layer_inversion`] obligation is the
10//! exception, emitting a QF_LIA cascade-ordering search the opt-in `smt-z3`
11//! backend genuinely solves and the propositional stub cannot.
12//!
13//! claim_level: default stub plus opt-in solver-backed checking, where the
14//! opt-in z3 backend genuinely solves one non-trivial QF_LIA cascade-ordering
15//! obligation the stub cannot, while the remaining obligations stay
16//! propositional and not default build SMT completeness.
17
18pub mod backend;
19pub mod encoder;
20pub mod fuzz;
21pub mod layer_inversion;
22pub mod obligations;
23pub mod proof;
24pub mod theory;
25pub mod unsat_core;
26
27#[cfg(feature = "smt-z3")]
28pub use backend::z3::Z3SmtBackendV0;
29pub use backend::{
30    SmtBackendCheckV0, SmtBackendKindV0, SmtBackendSatResultV0, SmtBackendV0, StubSmtBackendV0,
31};
32pub use encoder::{
33    CanonicalSmtInputV0, canonical_smt_input_v0, canonical_smt_input_with_script_v0,
34    canonical_smtlib2_script_v0,
35};
36pub use fuzz::{
37    SmtBisimulationFuzzCaseV0, SmtBisimulationFuzzReportV0, run_smt_bisimulation_fuzz_case_v0,
38    run_smt_bisimulation_fuzz_seed_corpus_v0, smt_bisimulation_fuzz_case_v0,
39};
40pub use layer_inversion::{
41    LayerFlattenInversionVerdictV0, LayerInversionDeclarationV0,
42    canonical_layer_flatten_inversion_input_v0, layer_inversion_declaration_v0,
43    smt_check_layer_flatten_inversion_v0,
44};
45pub use obligations::{
46    smt_evaluate_static_supports_condition_v0, smt_prove_box_shorthand_combination_v0,
47    smt_prove_layer_flatten_candidate_v0, smt_prove_scope_flatten_candidate_v0,
48};
49pub use proof::{
50    CascadeSMTProofAuditLogV0, CascadeSMTProofV0, SmtVerdictV0, cascade_spec_digest_v0,
51};
52pub use theory::{CascadeTheorySignatureV0, cascade_theory_signature_v0};
53pub use unsat_core::{CascadeUnsatCoreLabelV0, cascade_unsat_core_label_v0};
54
55pub const SMT_SCHEMA_VERSION_V0: &str = "0";
56pub const SMT_LAYER_MARKER_V0: &str = "smt-cascade-verification";
57pub const SMT_FEATURE_GATE_V0: &str = "smt-stub";
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use omena_cascade::{
63        BoxLonghandInputV0, LayerFlattenInputV0, ScopeFlattenInputV0, StaticSupportsAssumptionV0,
64        StaticSupportsEvalVerdictV0, evaluate_static_supports_condition,
65        prove_box_shorthand_combination, prove_layer_flatten_candidate,
66        prove_scope_flatten_candidate,
67    };
68
69    fn accepted_verdict(accepted: bool) -> SmtVerdictV0 {
70        if accepted {
71            SmtVerdictV0::Accepted
72        } else {
73            SmtVerdictV0::Rejected
74        }
75    }
76
77    #[test]
78    fn default_stub_backend_matches_l1_proof_verdict() {
79        let backend = StubSmtBackendV0::default();
80        let proof = smt_prove_box_shorthand_combination_v0(
81            "margin",
82            &[
83                BoxLonghandInputV0 {
84                    property: "margin-top".to_string(),
85                    value: "1px".to_string(),
86                    important: false,
87                    source_order: 1,
88                },
89                BoxLonghandInputV0 {
90                    property: "margin-right".to_string(),
91                    value: "1px".to_string(),
92                    important: false,
93                    source_order: 2,
94                },
95                BoxLonghandInputV0 {
96                    property: "margin-bottom".to_string(),
97                    value: "1px".to_string(),
98                    important: false,
99                    source_order: 3,
100                },
101                BoxLonghandInputV0 {
102                    property: "margin-left".to_string(),
103                    value: "1px".to_string(),
104                    important: false,
105                    source_order: 4,
106                },
107            ],
108            &backend,
109        );
110        assert_eq!(proof.schema_version, "0");
111        assert_eq!(proof.verdict, SmtVerdictV0::Accepted);
112        assert_eq!(proof.backend, SmtBackendKindV0::Stub);
113        assert!(
114            proof
115                .canonical_input
116                .smtlib2_script
117                .contains("(set-logic QF_UF)")
118        );
119    }
120
121    #[test]
122    fn cascade_spec_digest_is_hash_of_canonical_spec_material() {
123        let digest = cascade_spec_digest_v0();
124        assert_eq!(
125            digest,
126            *blake3::hash(crate::proof::CASCADE_SMT_SPEC_MATERIAL_V0.as_bytes()).as_bytes()
127        );
128        assert_ne!(digest, *b"omena-cascade-smt-spec-v0-------");
129    }
130
131    #[test]
132    fn proof_style_bisimulation_invariant_holds_for_all_l1_primitives() {
133        let backend = StubSmtBackendV0::default();
134        let longhands = vec![
135            BoxLonghandInputV0 {
136                property: "margin-top".to_string(),
137                value: "1px".to_string(),
138                important: false,
139                source_order: 1,
140            },
141            BoxLonghandInputV0 {
142                property: "margin-right".to_string(),
143                value: "1px".to_string(),
144                important: false,
145                source_order: 2,
146            },
147            BoxLonghandInputV0 {
148                property: "margin-bottom".to_string(),
149                value: "1px".to_string(),
150                important: false,
151                source_order: 3,
152            },
153            BoxLonghandInputV0 {
154                property: "margin-left".to_string(),
155                value: "1px".to_string(),
156                important: false,
157                source_order: 4,
158            },
159        ];
160        let l1_box = prove_box_shorthand_combination("margin", &longhands);
161        let l3_box = smt_prove_box_shorthand_combination_v0("margin", &longhands, &backend);
162        assert_eq!(l3_box.verdict, accepted_verdict(l1_box.accepted));
163
164        let scope_input = ScopeFlattenInputV0 {
165            root_selector: ":root".to_string(),
166            limit_selector: None,
167            scoped_rule_count: 1,
168            peer_scope_count: 0,
169            competing_unscoped_rule_count: 0,
170            inside_layer: false,
171        };
172        let l1_scope = prove_scope_flatten_candidate(scope_input.clone());
173        let l3_scope = smt_prove_scope_flatten_candidate_v0(scope_input, &backend);
174        assert_eq!(l3_scope.verdict, accepted_verdict(l1_scope.accepted));
175
176        let layer_input = LayerFlattenInputV0 {
177            layer_name: Some("components".to_string()),
178            layer_rule_count: 1,
179            peer_layer_count: 0,
180            unlayered_rule_count: 0,
181            important_declaration_count: 0,
182            closed_bundle: true,
183        };
184        let l1_layer = prove_layer_flatten_candidate(layer_input.clone());
185        let l3_layer = smt_prove_layer_flatten_candidate_v0(layer_input, &backend);
186        assert_eq!(l3_layer.verdict, accepted_verdict(l1_layer.accepted));
187    }
188
189    #[test]
190    fn static_supports_smt_equivalence_tracks_l1_verdict_shape() {
191        let backend = StubSmtBackendV0::default();
192        let l1 = evaluate_static_supports_condition(
193            "(display: grid)",
194            StaticSupportsAssumptionV0::ModernBrowser,
195        );
196        let l3 = smt_evaluate_static_supports_condition_v0(
197            "(display: grid)",
198            StaticSupportsAssumptionV0::ModernBrowser,
199            &backend,
200        );
201
202        assert_eq!(l1.verdict, StaticSupportsEvalVerdictV0::AlwaysTrue);
203        assert_eq!(l3.verdict, SmtVerdictV0::Accepted);
204        assert_eq!(l3.l1_primitive, "evaluate_static_supports_condition");
205    }
206
207    #[test]
208    fn smt_bisimulation_fuzz_seed_corpus_covers_m3_fixture_shapes() {
209        let report = run_smt_bisimulation_fuzz_seed_corpus_v0(128);
210        assert_eq!(report.schema_version, "0");
211        assert_eq!(report.fixture_suite, "m3-cascade-proof-fixtures");
212        assert_eq!(report.checked_obligation_count, 128 * 4);
213        assert_eq!(report.l1_l3_mismatch_count, 0);
214        assert!(report.passed);
215    }
216
217    #[test]
218    fn smt_bisimulation_fuzz_case_is_a_schema_zero_contract() {
219        let case = smt_bisimulation_fuzz_case_v0(42);
220        assert_eq!(case.schema_version, "0");
221        assert_eq!(case.layer_marker, "smt-cascade-verification");
222        assert_eq!(case.feature_gate, "smt-stub");
223        assert_eq!(case.seed, 42);
224    }
225
226    fn three_layer_declarations() -> [LayerInversionDeclarationV0; 3] {
227        [
228            layer_inversion_declaration_v0("base", 0, 0),
229            layer_inversion_declaration_v0("components", 1, 1),
230            layer_inversion_declaration_v0("utilities", 2, 2),
231        ]
232    }
233
234    #[test]
235    fn layer_inversion_encoder_emits_real_qf_lia_search() {
236        // The body is an arithmetic search, not a conjunction of pre-decided
237        // booleans: the encoder never evaluates whether an inversion exists.
238        let input = canonical_layer_flatten_inversion_input_v0(&three_layer_declarations());
239        assert!(input.smtlib2_script.contains("(set-logic QF_LIA)"));
240        assert!(input.smtlib2_script.contains("(declare-const rank_0 Int)"));
241        assert!(
242            input
243                .smtlib2_script
244                .contains(":named cascade_layer_flatten_inversion")
245        );
246        assert!(input.smtlib2_script.contains("(> rank_"));
247        assert!(input.smtlib2_script.contains("(> source_"));
248        // No `require:name=bool` literal terms: the propositional stub has
249        // nothing to evaluate and degenerates to Sat.
250        assert!(
251            input
252                .canonical_terms
253                .iter()
254                .all(|term| !term.starts_with("require:"))
255        );
256    }
257
258    #[test]
259    fn stub_backend_cannot_reason_about_layer_inversion() {
260        // The propositional stub does not model integer ordering, so it returns
261        // Sat for *every* layer-inversion formula regardless of whether an
262        // inversion actually exists. Both a real inversion and a safe ordering
263        // collapse to the same stub verdict, which is exactly why z3 is needed.
264        let backend = StubSmtBackendV0::default();
265
266        let mut inverted = three_layer_declarations();
267        // Make `utilities` (highest layer rank) lose in source order -> inversion.
268        inverted[2].source_order = -1;
269        let stub_inverted = smt_check_layer_flatten_inversion_v0(&inverted, &backend);
270
271        let safe = three_layer_declarations();
272        let stub_safe = smt_check_layer_flatten_inversion_v0(&safe, &backend);
273
274        assert_eq!(stub_inverted.backend, SmtBackendKindV0::Stub);
275        assert_eq!(stub_inverted.sat_result, SmtBackendSatResultV0::Sat);
276        assert_eq!(stub_safe.sat_result, SmtBackendSatResultV0::Sat);
277        // The stub gives the identical verdict for an unsafe and a safe ordering.
278        assert_eq!(stub_inverted.verdict, stub_safe.verdict);
279    }
280
281    #[cfg(feature = "smt-z3")]
282    #[test]
283    fn z3_decides_layer_inversion_and_disagrees_with_stub() {
284        let z3 = Z3SmtBackendV0::default();
285        let stub = StubSmtBackendV0::default();
286
287        // Emit case: `utilities` has the highest layer rank but is moved before
288        // `base`/`components` in source order, so flattening inverts the winner.
289        // Only `source_order` of one declaration differs from the clear case.
290        let mut inverted = three_layer_declarations();
291        inverted[2].source_order = -1;
292        let z3_inverted = smt_check_layer_flatten_inversion_v0(&inverted, &z3);
293
294        // Clear case: same declarations, restored source order -> layered and
295        // flattened winners coincide, so no inversion exists.
296        let safe = three_layer_declarations();
297        let z3_safe = smt_check_layer_flatten_inversion_v0(&safe, &z3);
298
299        // z3 actually solves the QF_LIA search and the verdict flips on the one
300        // changed source_order field.
301        assert_eq!(z3_inverted.backend, SmtBackendKindV0::Z3);
302        assert_eq!(z3_inverted.sat_result, SmtBackendSatResultV0::Sat);
303        assert!(z3_inverted.inversion_exists);
304        assert_eq!(z3_inverted.verdict, SmtVerdictV0::Rejected);
305
306        assert_eq!(z3_safe.sat_result, SmtBackendSatResultV0::Unsat);
307        assert!(!z3_safe.inversion_exists);
308        assert_eq!(z3_safe.verdict, SmtVerdictV0::Accepted);
309
310        // z3 adds reasoning: on the safe ordering z3 proves Unsat while the
311        // propositional stub degenerates to Sat. They disagree, which is only
312        // possible because z3 solves the formula the stub cannot.
313        let stub_safe = smt_check_layer_flatten_inversion_v0(&safe, &stub);
314        assert_eq!(stub_safe.sat_result, SmtBackendSatResultV0::Sat);
315        assert_ne!(z3_safe.sat_result, stub_safe.sat_result);
316        assert_ne!(z3_safe.verdict, stub_safe.verdict);
317    }
318
319    #[cfg(feature = "smt-z3")]
320    #[test]
321    fn z3_backend_solves_canonical_box_shorthand_obligation() {
322        let backend = Z3SmtBackendV0::default();
323        let accepted = smt_prove_box_shorthand_combination_v0(
324            "margin",
325            &[
326                BoxLonghandInputV0 {
327                    property: "margin-top".to_string(),
328                    value: "1px".to_string(),
329                    important: false,
330                    source_order: 1,
331                },
332                BoxLonghandInputV0 {
333                    property: "margin-right".to_string(),
334                    value: "1px".to_string(),
335                    important: false,
336                    source_order: 2,
337                },
338                BoxLonghandInputV0 {
339                    property: "margin-bottom".to_string(),
340                    value: "1px".to_string(),
341                    important: false,
342                    source_order: 3,
343                },
344                BoxLonghandInputV0 {
345                    property: "margin-left".to_string(),
346                    value: "1px".to_string(),
347                    important: false,
348                    source_order: 4,
349                },
350            ],
351            &backend,
352        );
353        let rejected = smt_prove_box_shorthand_combination_v0(
354            "margin",
355            &[
356                BoxLonghandInputV0 {
357                    property: "margin-top".to_string(),
358                    value: "1px".to_string(),
359                    important: false,
360                    source_order: 1,
361                },
362                BoxLonghandInputV0 {
363                    property: "margin-bottom".to_string(),
364                    value: "1px".to_string(),
365                    important: false,
366                    source_order: 2,
367                },
368                BoxLonghandInputV0 {
369                    property: "margin-right".to_string(),
370                    value: "1px".to_string(),
371                    important: false,
372                    source_order: 3,
373                },
374                BoxLonghandInputV0 {
375                    property: "margin-left".to_string(),
376                    value: "1px".to_string(),
377                    important: false,
378                    source_order: 4,
379                },
380            ],
381            &backend,
382        );
383
384        assert_eq!(accepted.backend, SmtBackendKindV0::Z3);
385        assert_eq!(accepted.solver_check.sat_result, SmtBackendSatResultV0::Sat);
386        assert_eq!(accepted.verdict, SmtVerdictV0::Accepted);
387        assert!(
388            accepted
389                .canonical_input
390                .smtlib2_script
391                .contains("(assert (! true :named req_canonical_longhand_quartet))")
392        );
393        assert_eq!(
394            rejected.solver_check.sat_result,
395            SmtBackendSatResultV0::Unsat
396        );
397        assert_eq!(rejected.verdict, SmtVerdictV0::Rejected);
398        assert!(
399            rejected
400                .canonical_input
401                .smtlib2_script
402                .contains("(assert (! false :named req_canonical_longhand_quartet))")
403        );
404        assert_ne!(
405            accepted.canonical_input.canonical_terms,
406            rejected.canonical_input.canonical_terms
407        );
408    }
409}