1use 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#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CertInfo {
29 pub pairs: usize,
31 pub classes: usize,
33 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
65pub 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 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 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 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 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 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; 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
299fn 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
335fn 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
355pub 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
365pub 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 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 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 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 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 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}