Skip to main content

rexact_check/
cert.rs

1//! Certificate and witness verification.
2//!
3//! A certificate claims that terms `a` and `b` denote the same language. It
4//! carries the alphabet universe, a partition of it into symbol classes, the
5//! two start terms and a set of derivative pairs. It is valid when:
6//!
7//! 1. the classes partition the universe and every symbol set mentioned by
8//!    any term is a union of classes (so one representative per class
9//!    stands for the whole class);
10//! 2. the start pair is identical or listed;
11//! 3. in every listed pair both terms agree on accepting the empty string;
12//! 4. for every listed pair and every class, the pair of derivatives is
13//!    identical or listed.
14//!
15//! Identical pairs are trivially equivalent (a bisimulation up to equality),
16//! so they need not be listed. Conditions 2-4 make the listed set a
17//! bisimulation, hence `a` and `b` accept the same strings.
18
19use crate::json::{self, J};
20use crate::term::{
21    self, leaf_sets, norm_ranges, parse_spec, ranges_intersect, Ctx, Ranges, World, T,
22};
23use crate::{malformed, on_big_stack, rejected, CheckError};
24use std::collections::HashSet;
25
26/// Summary of a verified certificate.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CertInfo {
29    /// Number of listed (non-identical) pairs.
30    pub pairs: usize,
31    /// Number of symbol classes.
32    pub classes: usize,
33    /// Alphabet mode named by the certificate.
34    pub alphabet: String,
35}
36
37fn ranges_of(j: &J) -> Result<Ranges, CheckError> {
38    let arr = j
39        .as_arr()
40        .ok_or_else(|| malformed("ranges must be an array"))?;
41    let mut out = Vec::with_capacity(arr.len());
42    for r in arr {
43        let p = r
44            .as_arr()
45            .ok_or_else(|| malformed("range must be [lo, hi]"))?;
46        match (
47            p.first().and_then(J::as_num),
48            p.get(1).and_then(J::as_num),
49            p.len(),
50        ) {
51            (Some(lo), Some(hi), 2) if lo <= hi && hi <= 0x10FFFF => {
52                out.push((lo as u32, hi as u32))
53            }
54            _ => return Err(malformed("range must be [lo, hi] with lo <= hi <= 10FFFF")),
55        }
56    }
57    Ok(out)
58}
59
60fn field<'a>(j: &'a J, name: &str) -> Result<&'a J, CheckError> {
61    j.get(name)
62        .ok_or_else(|| malformed(format!("missing field '{name}'")))
63}
64
65/// Verify a certificate document. Returns a summary or the first problem.
66pub fn check_certificate(json_text: &str) -> Result<CertInfo, CheckError> {
67    let text = json_text.to_string();
68    on_big_stack(move || check_certificate_inner(&text))
69}
70
71fn parse_ctx(j: &J, world: &World) -> Result<Option<Ctx>, CheckError> {
72    if matches!(j, J::Null) {
73        return Ok(None);
74    }
75    let start = match field(j, "s")? {
76        J::Bool(b) => *b,
77        _ => return Err(malformed("context 's' must be a boolean")),
78    };
79    let bits = field(j, "b")?
80        .as_num()
81        .ok_or_else(|| malformed("context 'b' must be a number"))?;
82    let mut tracks = Vec::new();
83    for t in field(j, "t")?
84        .as_arr()
85        .ok_or_else(|| malformed("context 't' must be an array"))?
86    {
87        tracks
88            .push(parse_spec(t.as_str().ok_or_else(|| {
89                malformed("tracker state must be a string")
90            })?)?);
91    }
92    if tracks.len() != world.trackers.len()
93        || (bits >> world.aux.len().min(63)) != 0 && world.aux.len() < 64
94    {
95        return Err(malformed(
96            "context does not match the declared auxiliary sets and trackers",
97        ));
98    }
99    Ok(Some(Ctx {
100        start,
101        bits,
102        tracks,
103    }))
104}
105
106fn ctx_key(c: &Option<Ctx>) -> String {
107    c.as_ref().map_or("-".to_string(), Ctx::key)
108}
109
110fn check_certificate_inner(text: &str) -> Result<CertInfo, CheckError> {
111    let doc = json::parse(text)?;
112    if field(&doc, "format")?.as_str() != Some("rexact-cert")
113        || field(&doc, "version")?.as_num() != Some(1)
114    {
115        return Err(malformed("not a rexact-cert version 1 document"));
116    }
117    let alphabet = field(&doc, "alphabet")?
118        .as_str()
119        .ok_or_else(|| malformed("alphabet must be a string"))?
120        .to_string();
121    let universe = norm_ranges(ranges_of(field(&doc, "universe")?)?);
122    let mut classes: Vec<Ranges> = Vec::new();
123    for c in field(&doc, "classes")?
124        .as_arr()
125        .ok_or_else(|| malformed("classes must be an array"))?
126    {
127        let r = ranges_of(c)?;
128        if norm_ranges(r.clone()) != r || r.is_empty() {
129            return Err(malformed("class ranges must be normalised and non-empty"));
130        }
131        classes.push(r);
132    }
133    let mut aux: Vec<Ranges> = Vec::new();
134    for a in field(&doc, "aux")?
135        .as_arr()
136        .ok_or_else(|| malformed("aux must be an array"))?
137    {
138        aux.push(norm_ranges(ranges_of(a)?));
139    }
140    let mut trackers = Vec::new();
141    for t in field(&doc, "trackers")?
142        .as_arr()
143        .ok_or_else(|| malformed("trackers must be an array"))?
144    {
145        trackers.push(parse_spec(
146            t.as_str()
147                .ok_or_else(|| malformed("tracker must be a string"))?,
148        )?);
149    }
150    let world = World {
151        universe: universe.clone(),
152        aux,
153        trackers,
154    };
155
156    // (1a) classes are disjoint and cover the universe exactly.
157    let mut covered: Ranges = Vec::new();
158    for (i, c) in classes.iter().enumerate() {
159        for d in &classes[i + 1..] {
160            if !ranges_intersect(c, d).is_empty() {
161                return Err(rejected("symbol classes overlap"));
162            }
163        }
164        covered.extend(c.iter().copied());
165    }
166    if norm_ranges(covered) != universe {
167        return Err(rejected("symbol classes do not cover the universe exactly"));
168    }
169
170    let a = parse_spec(
171        field(&doc, "a")?
172            .as_str()
173            .ok_or_else(|| malformed("'a' must be a string"))?,
174    )?;
175    let b = parse_spec(
176        field(&doc, "b")?
177            .as_str()
178            .ok_or_else(|| malformed("'b' must be a string"))?,
179    )?;
180    let start_ctx = parse_ctx(field(&doc, "start_ctx")?, &world)?;
181    let mut pairs: Vec<(T, T, Option<Ctx>)> = Vec::new();
182    for p in field(&doc, "pairs")?
183        .as_arr()
184        .ok_or_else(|| malformed("pairs must be an array"))?
185    {
186        let e = p
187            .as_arr()
188            .ok_or_else(|| malformed("pair must be [a, b, ctx]"))?;
189        match (
190            e.first().and_then(J::as_str),
191            e.get(1).and_then(J::as_str),
192            e.get(2),
193            e.len(),
194        ) {
195            (Some(x), Some(y), Some(c), 3) => {
196                pairs.push((parse_spec(x)?, parse_spec(y)?, parse_ctx(c, &world)?))
197            }
198            _ => return Err(malformed("pair must be [a, b, ctx]")),
199        }
200    }
201
202    // (1b) every symbol set anywhere is a union of classes within the universe,
203    // and every assertion set / lookbehind body is declared.
204    let mut sets: Vec<Ranges> = world.aux.clone();
205    leaf_sets(&a, &mut sets);
206    leaf_sets(&b, &mut sets);
207    let mut all_terms: Vec<&T> = vec![&a, &b];
208    for (x, y, c) in &pairs {
209        leaf_sets(x, &mut sets);
210        leaf_sets(y, &mut sets);
211        all_terms.push(x);
212        all_terms.push(y);
213        if let Some(c) = c {
214            c.tracks.iter().for_each(|t| leaf_sets(t, &mut sets));
215        }
216    }
217    for t in &world.trackers {
218        leaf_sets(t, &mut sets);
219    }
220    if !all_terms.iter().all(|t| world.declares(t)) {
221        return Err(rejected(
222            "a term uses an assertion set or lookbehind that the certificate does not declare",
223        ));
224    }
225    for s in &sets {
226        if ranges_intersect(s, &universe) != *s {
227            return Err(rejected("a term mentions symbols outside the universe"));
228        }
229        for c in &classes {
230            let inter = ranges_intersect(s, c);
231            if !inter.is_empty() && inter != *c {
232                return Err(rejected(
233                    "a symbol set splits a class; the partition is too coarse",
234                ));
235            }
236        }
237    }
238
239    // The context only matters when a term is context-dependent; otherwise it is "-".
240    let norm = |x: &T, y: &T, c: &Option<Ctx>| {
241        if x.dep || y.dep {
242            ctx_key(c)
243        } else {
244            "-".to_string()
245        }
246    };
247    let listed: HashSet<(String, String, String)> = pairs
248        .iter()
249        .map(|(x, y, c)| (x.key.clone(), y.key.clone(), norm(x, y, c)))
250        .collect();
251    let ok = |x: &T, y: &T, c: &Option<Ctx>| {
252        x.key == y.key || listed.contains(&(x.key.clone(), y.key.clone(), norm(x, y, c)))
253    };
254
255    // (2) the start pair.
256    if !ok(&a, &b, &start_ctx) {
257        return Err(rejected(format!(
258            "start pair ({}, {}) is not covered",
259            a.key, b.key
260        )));
261    }
262    // (3) acceptance and (4) closure under every class.
263    let blank = Ctx {
264        start: false,
265        bits: 0,
266        tracks: Vec::new(),
267    };
268    for (x, y, c) in &pairs {
269        let here = c.as_ref().unwrap_or(&blank);
270        if world.accepting(x, here) != world.accepting(y, here) {
271            return Err(rejected(format!(
272                "pair ({}, {}) disagrees on the empty string",
273                x.key, y.key
274            )));
275        }
276        for class in &classes {
277            let rep = class[0].0; // smallest member of the class
278            let (dx, dy) = (world.derive(x, rep, here), world.derive(y, rep, here));
279            let next = if dx.dep || dy.dep {
280                Some(world.advance(here, rep))
281            } else {
282                None
283            };
284            if !ok(&dx, &dy, &next) {
285                return Err(rejected(format!(
286                    "pair ({}, {}) has derivative pair ({}, {}) on class starting U+{:04X}, which is not listed",
287                    x.key, y.key, dx.key, dy.key, rep
288                )));
289            }
290        }
291    }
292    Ok(CertInfo {
293        pairs: pairs.len(),
294        classes: classes.len(),
295        alphabet,
296    })
297}
298
299/// Collect assertion sets and lookbehind bodies from specs, to build a world for witnesses.
300fn world_for(specs: &[&T]) -> World {
301    fn walk(t: &T, aux: &mut Vec<Ranges>, trk: &mut Vec<T>) {
302        match &t.k {
303            term::K::Assert(_, w, l) => {
304                for r in [w, l].into_iter().flatten() {
305                    if !aux.contains(r) {
306                        aux.push(r.clone());
307                    }
308                }
309            }
310            term::K::Behind(_, b) => {
311                if !trk.iter().any(|x| x.key == b.key) {
312                    trk.push(b.clone());
313                }
314            }
315            term::K::Ahead(_, x) | term::K::Star(x) | term::K::Not(x) => walk(x, aux, trk),
316            term::K::Cat(a, b) => {
317                walk(a, aux, trk);
318                walk(b, aux, trk);
319            }
320            term::K::Alt(v) | term::K::And(v) => v.iter().for_each(|x| walk(x, aux, trk)),
321            _ => {}
322        }
323    }
324    let (mut aux, mut trk) = (Vec::new(), Vec::new());
325    for t in specs {
326        walk(t, &mut aux, &mut trk);
327    }
328    World {
329        universe: vec![(0, 0x10FFFF)],
330        aux,
331        trackers: trk,
332    }
333}
334
335/// Run a term over `input` from the start of the text; true if it accepts.
336fn run_term(t: T, world: &World, input: &[u32]) -> bool {
337    let sigma = world.sigma_star();
338    let mut ctx = Ctx {
339        start: true,
340        bits: 0,
341        tracks: world
342            .trackers
343            .iter()
344            .map(|b| term::cat(sigma.clone(), b.clone()))
345            .collect(),
346    };
347    let mut t = t;
348    for &c in input {
349        t = world.derive(&t, c, &ctx);
350        ctx = world.advance(&ctx, c);
351    }
352    world.accepting(&t, &ctx)
353}
354
355/// Does `spec` accept `input`? Simple derivative simulation.
356pub fn check_witness(spec: &str, input: &[u32]) -> Result<bool, CheckError> {
357    let (spec, input) = (spec.to_string(), input.to_vec());
358    on_big_stack(move || {
359        let t = parse_spec(&spec)?;
360        let world = world_for(&[&t]);
361        Ok(run_term(t, &world, &input))
362    })
363}
364
365/// Acceptance of `input` by each of two specs: `(by_a, by_b)`.
366pub fn check_counterexample(a: &str, b: &str, input: &[u32]) -> Result<(bool, bool), CheckError> {
367    Ok((check_witness(a, input)?, check_witness(b, input)?))
368}
369
370#[cfg(test)]
371#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
372mod tests {
373    use super::*;
374
375    fn cert(a: &str, b: &str, classes: &str, pairs: &str) -> String {
376        format!(
377            r#"{{"format":"rexact-cert","version":1,"alphabet":"test","universe":[[0,127]],"classes":{classes},"aux":[],"trackers":[],"start_ctx":null,"a":"{a}","b":"{b}","pairs":{pairs}}}"#
378        )
379    }
380
381    // a* and (a|e)* over {a} vs rest: classes [97] and the rest.
382    const CLS: &str = "[[[0,96],[98,127]],[[97,97]]]";
383
384    #[test]
385    fn identical_terms_need_no_pairs() {
386        let c = cert("(* [61])", "(* [61])", CLS, "[]");
387        assert_eq!(check_certificate(&c).unwrap().pairs, 0);
388    }
389
390    #[test]
391    fn a_valid_nontrivial_certificate_is_accepted() {
392        // a* vs (a|e)*: normalisation makes these identical, so use a*  vs  e|aa*
393        let c = cert(
394            "(* [61])",
395            "(| e (. [61] (* [61])))",
396            CLS,
397            r#"[["(* [61])","(| e (. [61] (* [61])))",null]]"#,
398        );
399        let info = check_certificate(&c).unwrap();
400        assert_eq!((info.pairs, info.classes), (1, 2));
401    }
402
403    #[test]
404    fn missing_pair_is_rejected_with_e001() {
405        let c = cert("(* [61])", "(| e (. [61] (* [61])))", CLS, "[]");
406        assert_eq!(check_certificate(&c).unwrap_err().code, "RX-E001");
407    }
408
409    #[test]
410    fn disagreeing_acceptance_is_rejected() {
411        // a+ vs a*: start pair listed but they disagree on the empty string.
412        let c = cert(
413            "(. [61] (* [61]))",
414            "(* [61])",
415            CLS,
416            r#"[["(. [61] (* [61]))","(* [61])",null]]"#,
417        );
418        let e = check_certificate(&c).unwrap_err();
419        assert_eq!(e.code, "RX-E001");
420        assert!(e.message.contains("empty string"));
421    }
422
423    #[test]
424    fn coarse_partition_is_rejected() {
425        let c = cert("[61]", "[61]", "[[[0,127]]]", "[]");
426        assert!(check_certificate(&c)
427            .unwrap_err()
428            .message
429            .contains("splits a class"));
430    }
431
432    #[test]
433    fn partition_must_cover_the_universe() {
434        let c = cert("e", "e", "[[[0,96]]]", "[]");
435        assert!(check_certificate(&c).unwrap_err().message.contains("cover"));
436    }
437
438    #[test]
439    fn malformed_documents_are_e004() {
440        assert_eq!(check_certificate("{").unwrap_err().code, "RX-E004");
441        assert_eq!(
442            check_certificate(r#"{"format":"x"}"#).unwrap_err().code,
443            "RX-E004"
444        );
445        let c = cert("(. [61]", "e", CLS, "[]");
446        assert_eq!(check_certificate(&c).unwrap_err().code, "RX-E004");
447    }
448
449    #[test]
450    fn witness_simulation() {
451        // (a|b)*c
452        let spec = "(. (* [61-62]) [63])";
453        assert!(check_witness(spec, &[97, 98, 99]).unwrap());
454        assert!(!check_witness(spec, &[97, 98]).unwrap());
455        assert!(!check_witness(spec, &[100]).unwrap());
456        assert_eq!(
457            check_counterexample("[61]", "[62]", &[97]).unwrap(),
458            (true, false)
459        );
460    }
461
462    #[test]
463    fn complement_and_intersection() {
464        // !a accepts "" and "b" but not "a".
465        assert!(check_witness("(! [61])", &[]).unwrap());
466        assert!(check_witness("(! [61])", &[98]).unwrap());
467        assert!(!check_witness("(! [61])", &[97]).unwrap());
468        assert!(check_witness("(& (* [61-62]) (! (. [61] (* [61-62]))))", &[98, 97]).unwrap());
469        assert!(!check_witness("(& (* [61-62]) (! (. [61] (* [61-62]))))", &[97, 98]).unwrap());
470    }
471}