1use crate::{is_num, is_structurally_same, key_of, parse_lino, parse_one, tokenize_one, Node};
10use std::collections::HashSet;
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct CheckOk {
14 pub rule: String,
15 pub expr: String,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct CheckError {
20 pub path: Vec<String>,
21 pub message: String,
22}
23
24#[derive(Debug, Clone, Default, PartialEq)]
25pub struct CheckResult {
26 pub ok: Vec<CheckOk>,
27 pub errors: Vec<CheckError>,
28}
29
30impl CheckResult {
31 pub fn is_ok(&self) -> bool {
32 self.errors.is_empty()
33 }
34}
35
36fn collect_operators(forms: &[Node]) -> HashSet<String> {
40 let mut ops: HashSet<String> = ["not", "and", "or", "=", "!=", "+", "-", "*", "/"]
41 .iter()
42 .map(|s| s.to_string())
43 .collect();
44 for f in forms {
45 if let Node::List(c) = f {
46 if let Some(Node::Leaf(s)) = c.first() {
47 if let Some(name) = s.strip_suffix(':') {
48 if !name.is_empty() {
49 ops.insert(name.to_string());
50 }
51 }
52 }
53 }
54 }
55 ops
56}
57
58fn collect_assignments(forms: &[Node]) -> HashSet<String> {
62 let mut out = HashSet::new();
63 for f in forms {
64 if let Node::List(c) = f {
65 if c.len() == 4 {
66 if let (Node::Leaf(w1), Node::Leaf(w2), Node::Leaf(w3)) = (&c[1], &c[2], &c[3]) {
67 if w1 == "has" && w2 == "probability" && is_num(w3) {
68 if let Node::List(inner) = &c[0] {
69 out.insert(key_of(&c[0]));
70 if inner.len() == 3 {
71 if let Node::Leaf(op) = &inner[1] {
72 if op == "=" {
73 out.insert(key_of(&Node::List(vec![
74 Node::Leaf("=".into()),
75 inner[0].clone(),
76 inner[2].clone(),
77 ])));
78 }
79 }
80 }
81 }
82 }
83 }
84 }
85 }
86 }
87 out
88}
89
90fn parse_forms(src: &str) -> Vec<Node> {
92 parse_lino(src)
93 .into_iter()
94 .filter(|s| !s.trim_start().starts_with("(#"))
95 .filter_map(|s| parse_one(&tokenize_one(&s)).ok())
96 .collect()
97}
98
99fn query_target(n: &Node) -> Option<Node> {
101 if let Node::List(c) = n {
102 if let Some(Node::Leaf(h)) = c.first() {
103 if h == "?" {
104 let parts: Vec<Node> = c[1..].to_vec();
105 let parts = if parts.len() >= 2 {
106 if let (Node::Leaf(a), Node::Leaf(b)) =
107 (&parts[parts.len() - 2], &parts[parts.len() - 1])
108 {
109 if a == "with" && b == "proof" {
110 parts[..parts.len() - 2].to_vec()
111 } else {
112 parts
113 }
114 } else {
115 parts
116 }
117 } else {
118 parts
119 };
120 return Some(if parts.len() == 1 {
121 parts[0].clone()
122 } else {
123 Node::List(parts)
124 });
125 }
126 }
127 }
128 None
129}
130
131fn decode<'a>(p: &'a Node, path: &[String]) -> Result<(String, &'a [Node]), CheckError> {
133 if let Node::List(c) = p {
134 if c.len() >= 2 {
135 if let (Node::Leaf(by), Node::Leaf(rule)) = (&c[0], &c[1]) {
136 if by == "by" {
137 return Ok((rule.clone(), &c[2..]));
138 }
139 }
140 }
141 }
142 Err(CheckError {
143 path: path.to_vec(),
144 message: format!("expected `(by <rule> ...)`, got `{}`", key_of(p)),
145 })
146}
147
148fn expected_rule(expr: &Node, ops: &HashSet<String>, assigned: &HashSet<String>) -> &'static str {
152 match expr {
153 Node::Leaf(s) => {
154 if is_num(s) {
155 "literal"
156 } else {
157 "symbol"
158 }
159 }
160 Node::List(c) => {
161 if let Some(Node::Leaf(h)) = c.first() {
162 if h.ends_with(':') {
163 return "definition";
164 }
165 match h.as_str() {
166 "Type" if c.len() == 2 => return "type-universe",
167 "Prop" if c.len() == 1 => return "prop",
168 "Pi" if c.len() == 3 => return "pi-formation",
169 "lambda" if c.len() == 3 => return "lambda-formation",
170 "apply" if c.len() == 3 => return "beta-reduction",
171 "subst" if c.len() == 4 => return "substitution",
172 "fresh" if c.len() == 4 => {
173 if let Node::Leaf(in_kw) = &c[2] {
174 if in_kw == "in" {
175 return "fresh";
176 }
177 }
178 }
179 "type" if c.len() == 3 => {
180 if let Node::Leaf(of) = &c[1] {
181 if of == "of" {
182 return "type-query";
183 }
184 }
185 }
186 _ => {}
187 }
188 }
189 if c.len() == 4 {
191 if let (Node::Leaf(w1), Node::Leaf(w2), Node::Leaf(w3)) = (&c[1], &c[2], &c[3]) {
192 if w1 == "has" && w2 == "probability" && is_num(w3) {
193 return "assigned-probability";
194 }
195 }
196 }
197 if c.len() == 3 {
199 if let (Node::Leaf(h), Node::Leaf(a), Node::Leaf(b)) = (&c[0], &c[1], &c[2]) {
200 if h == "range" && is_num(a) && is_num(b) {
201 return "configuration";
202 }
203 }
204 }
205 if c.len() == 2 {
206 if let (Node::Leaf(h), Node::Leaf(v)) = (&c[0], &c[1]) {
207 if h == "valence" && is_num(v) {
208 return "configuration";
209 }
210 }
211 }
212 if c.len() == 3 {
214 if let Node::Leaf(op) = &c[1] {
215 return match op.as_str() {
216 "+" => "sum",
217 "-" => "difference",
218 "*" => "product",
219 "/" => "quotient",
220 "and" => "and",
221 "or" => "or",
222 "both" => "both",
223 "neither" => "neither",
224 "of" => "type-check",
225 "=" | "!=" => {
226 let l = &c[0];
227 let r = &c[2];
228 let kp = key_of(&Node::List(vec![
229 Node::Leaf("=".into()),
230 l.clone(),
231 r.clone(),
232 ]));
233 let ki = key_of(&Node::List(vec![
234 l.clone(),
235 Node::Leaf("=".into()),
236 r.clone(),
237 ]));
238 let assigned = assigned.contains(&kp) || assigned.contains(&ki);
239 if op == "!=" {
240 if assigned {
241 "assigned-inequality"
242 } else if is_structurally_same(l, r) {
243 "structural-inequality"
244 } else {
245 "numeric-inequality"
246 }
247 } else if assigned {
248 "assigned-equality"
249 } else if is_structurally_same(l, r) {
250 "structural-equality"
251 } else {
252 "numeric-equality"
253 }
254 }
255 _ => fallback_prefix(c, ops),
256 };
257 }
258 }
259 if c.len() >= 4 && c.len() % 2 == 0 {
261 if let Node::Leaf(h) = &c[0] {
262 if h == "both" {
263 return "both";
264 }
265 if h == "neither" {
266 return "neither";
267 }
268 }
269 }
270 fallback_prefix(c, ops)
271 }
272 }
273}
274
275fn fallback_prefix(c: &[Node], ops: &HashSet<String>) -> &'static str {
276 if let Some(Node::Leaf(h)) = c.first() {
277 if ops.contains(h) {
278 return prefix_marker(h);
279 }
280 }
281 "reduce"
282}
283
284fn prefix_marker(name: &str) -> &'static str {
288 match name {
289 "not" => "not",
290 "and" => "and",
291 "or" => "or",
292 "+" => "sum",
293 "-" => "difference",
294 "*" => "product",
295 "/" => "quotient",
296 _ => "reduce",
299 }
300}
301
302fn check_node(
304 expr: &Node,
305 proof: &Node,
306 ops: &HashSet<String>,
307 assigned: &HashSet<String>,
308 path: &[String],
309) -> Result<String, CheckError> {
310 let (rule, subs) = decode(proof, path)?;
311 if rule == "smt-trusted" {
312 return check_smt_trusted(&rule, subs, path);
313 }
314 let exp = expected_rule(expr, ops, assigned);
315 let rule_ok = rule == exp || prefix_match(&rule, expr, ops);
319 if !rule_ok {
320 return Err(err(
321 path,
322 format!(
323 "rule `{}` does not justify `{}` (expected `{}`)",
324 rule,
325 key_of(expr),
326 exp
327 ),
328 ));
329 }
330 let mut next_path = path.to_vec();
331 next_path.push(format!("{}", rule));
332 match rule.as_str() {
333 "literal" => arity(&rule, subs, 1, path).and_then(|_| match (&subs[0], expr) {
335 (Node::Leaf(n), Node::Leaf(en)) if is_num(n) && n == en => Ok(rule),
336 _ => Err(err(
337 path,
338 format!("literal `{}` ≠ `{}`", key_of(&subs[0]), key_of(expr)),
339 )),
340 }),
341 "symbol" => arity(&rule, subs, 1, path).and_then(|_| match (&subs[0], expr) {
342 (Node::Leaf(s), Node::Leaf(es)) if s == es => Ok(rule),
343 _ => Err(err(
344 path,
345 format!("symbol `{}` ≠ `{}`", key_of(&subs[0]), key_of(expr)),
346 )),
347 }),
348 "definition" => check_payload(&rule, subs, &[expr.clone()], path),
351 "configuration" => check_configuration(expr, &rule, subs, path),
352 "assigned-probability" => check_assigned_probability(expr, &rule, subs, path),
353 "query" => Err(err(path, "stray `query` rule (stripped by checker)".into())),
354 "reduce" => check_payload(&rule, subs, &[expr.clone()], path),
355 "sum" | "difference" | "product" | "quotient" => {
357 let op = match rule.as_str() {
358 "sum" => "+",
359 "difference" => "-",
360 "product" => "*",
361 "quotient" => "/",
362 _ => unreachable!(),
363 };
364 check_infix(expr, &rule, subs, op, ops, assigned, &next_path, path)
365 }
366 "and" | "or" | "both" | "neither" => {
368 check_logic(expr, &rule, subs, ops, assigned, &next_path, path)
369 }
370 "not" => arity(&rule, subs, 1, path).and_then(|_| match expr {
372 Node::List(c) if c.len() == 2 => match &c[0] {
373 Node::Leaf(h) if h == "not" => {
374 check_node(&c[1], &subs[0], ops, assigned, &next_path).map(|_| rule.clone())
375 }
376 _ => Err(err(path, format!("`not` ≠ `{}`", key_of(expr)))),
377 },
378 _ => Err(err(path, format!("`not` ≠ `{}`", key_of(expr)))),
379 }),
380 "structural-equality"
382 | "numeric-equality"
383 | "assigned-equality"
384 | "structural-inequality"
385 | "numeric-inequality"
386 | "assigned-inequality" => check_eq(expr, &rule, subs, ops, assigned, path),
387 "type-universe" | "prop" | "pi-formation" | "lambda-formation" | "type-query"
389 | "type-check" | "substitution" | "fresh" => check_typesys(expr, &rule, subs, path),
390 "beta-reduction" => arity(&rule, subs, 2, path).and_then(|_| match expr {
391 Node::List(c) if c.len() == 3 => match &c[0] {
392 Node::Leaf(h) if h == "apply" => {
393 check_node(&c[1], &subs[0], ops, assigned, &next_path)?;
394 check_node(&c[2], &subs[1], ops, assigned, &next_path)?;
395 Ok(rule.clone())
396 }
397 _ => Err(err(path, format!("`beta-reduction` ≠ `{}`", key_of(expr)))),
398 },
399 _ => Err(err(path, format!("`beta-reduction` ≠ `{}`", key_of(expr)))),
400 }),
401 _ => check_prefix(expr, &rule, subs, ops, assigned, &next_path, path),
403 }
404}
405
406fn check_smt_trusted(
407 rule: &str,
408 subs: &[Node],
409 path: &[String],
410) -> Result<String, CheckError> {
411 arity(rule, subs, 1, path)?;
412 match &subs[0] {
413 Node::Leaf(solver) if !solver.is_empty() => Ok(rule.to_string()),
414 _ => Err(err(
415 path,
416 "`smt-trusted` expects a solver name payload".to_string(),
417 )),
418 }
419}
420
421fn err(path: &[String], message: String) -> CheckError {
422 CheckError {
423 path: path.to_vec(),
424 message,
425 }
426}
427
428fn arity(rule: &str, subs: &[Node], n: usize, path: &[String]) -> Result<(), CheckError> {
429 if subs.len() == n {
430 Ok(())
431 } else {
432 Err(err(
433 path,
434 format!("rule `{}` expects {} sub(s), got {}", rule, n, subs.len()),
435 ))
436 }
437}
438
439fn check_payload(
440 rule: &str,
441 subs: &[Node],
442 expected: &[Node],
443 path: &[String],
444) -> Result<String, CheckError> {
445 arity(rule, subs, expected.len(), path)?;
446 for (i, want) in expected.iter().enumerate() {
447 if !is_structurally_same(&subs[i], want) {
448 return Err(err(
449 path,
450 format!("payload {} `{}` ≠ `{}`", i, key_of(&subs[i]), key_of(want)),
451 ));
452 }
453 }
454 Ok(rule.into())
455}
456
457fn check_configuration(
458 expr: &Node,
459 rule: &str,
460 subs: &[Node],
461 path: &[String],
462) -> Result<String, CheckError> {
463 match expr {
464 Node::List(c) if c.len() == 3 => match (&c[0], &c[1], &c[2]) {
465 (Node::Leaf(h), lo, hi) if h == "range" => check_payload(
466 rule,
467 subs,
468 &[Node::Leaf("range".into()), lo.clone(), hi.clone()],
469 path,
470 ),
471 _ => Err(err(path, format!("`{}` ≠ `{}`", rule, key_of(expr)))),
472 },
473 Node::List(c) if c.len() == 2 => match (&c[0], &c[1]) {
474 (Node::Leaf(h), v) if h == "valence" => {
475 check_payload(rule, subs, &[Node::Leaf("valence".into()), v.clone()], path)
476 }
477 _ => Err(err(path, format!("`{}` ≠ `{}`", rule, key_of(expr)))),
478 },
479 _ => Err(err(path, format!("`{}` ≠ `{}`", rule, key_of(expr)))),
480 }
481}
482
483fn check_assigned_probability(
484 expr: &Node,
485 rule: &str,
486 subs: &[Node],
487 path: &[String],
488) -> Result<String, CheckError> {
489 if let Node::List(c) = expr {
490 if c.len() == 4 {
491 if let (Node::Leaf(w1), Node::Leaf(w2)) = (&c[1], &c[2]) {
492 if w1 == "has" && w2 == "probability" {
493 return check_payload(rule, subs, &[c[0].clone(), c[3].clone()], path);
494 }
495 }
496 }
497 }
498 Err(err(path, format!("`{}` ≠ `{}`", rule, key_of(expr))))
499}
500
501fn prefix_match(rule: &str, expr: &Node, ops: &HashSet<String>) -> bool {
503 if !ops.contains(rule) {
504 return false;
505 }
506 if let Node::List(c) = expr {
507 if let Some(Node::Leaf(h)) = c.first() {
508 return h == rule;
509 }
510 }
511 false
512}
513
514fn check_infix(
515 expr: &Node,
516 rule: &str,
517 subs: &[Node],
518 op: &str,
519 ops: &HashSet<String>,
520 assigned: &HashSet<String>,
521 next_path: &[String],
522 path: &[String],
523) -> Result<String, CheckError> {
524 arity(rule, subs, 2, path)?;
525 if let Node::List(c) = expr {
526 if c.len() == 3 {
527 if let Node::Leaf(o) = &c[1] {
528 if o == op {
529 check_node(&c[0], &subs[0], ops, assigned, next_path)?;
530 check_node(&c[2], &subs[1], ops, assigned, next_path)?;
531 return Ok(rule.into());
532 }
533 }
534 }
535 }
536 Err(err(path, format!("rule `{}` ≠ `{}`", rule, key_of(expr))))
537}
538
539fn check_logic(
540 expr: &Node,
541 rule: &str,
542 subs: &[Node],
543 ops: &HashSet<String>,
544 assigned: &HashSet<String>,
545 next_path: &[String],
546 path: &[String],
547) -> Result<String, CheckError> {
548 if let Node::List(c) = expr {
549 if c.len() == 3 {
551 if let Node::Leaf(o) = &c[1] {
552 if o == rule {
553 arity(rule, subs, 2, path)?;
554 check_node(&c[0], &subs[0], ops, assigned, next_path)?;
555 check_node(&c[2], &subs[1], ops, assigned, next_path)?;
556 return Ok(rule.into());
557 }
558 }
559 }
560 if (rule == "both" || rule == "neither") && c.len() >= 4 && c.len() % 2 == 0 {
562 if let Node::Leaf(h) = &c[0] {
563 if h == rule {
564 let sep = if rule == "both" { "and" } else { "nor" };
565 for i in (2..c.len()).step_by(2) {
566 if let Node::Leaf(s) = &c[i] {
567 if s != sep {
568 return Err(err(
569 path,
570 format!("composite `{}` separator must be `{}`", rule, sep),
571 ));
572 }
573 } else {
574 return Err(err(
575 path,
576 format!("composite `{}` separator must be `{}`", rule, sep),
577 ));
578 }
579 }
580 let n = c.len() / 2;
581 arity(rule, subs, n, path)?;
582 for (j, sub) in subs.iter().enumerate() {
583 check_node(&c[1 + j * 2], sub, ops, assigned, next_path)?;
584 }
585 return Ok(rule.into());
586 }
587 }
588 }
589 }
590 Err(err(path, format!("rule `{}` ≠ `{}`", rule, key_of(expr))))
591}
592
593fn check_eq(
594 expr: &Node,
595 rule: &str,
596 subs: &[Node],
597 ops: &HashSet<String>,
598 assigned: &HashSet<String>,
599 path: &[String],
600) -> Result<String, CheckError> {
601 arity(rule, subs, 1, path)?;
602 let pair = match &subs[0] {
603 Node::List(p) if p.len() == 2 => p,
604 _ => return Err(err(path, format!("`{}` expects `(L R)` sub", rule))),
605 };
606 if let Node::List(c) = expr {
607 if c.len() == 3 {
608 if let Node::Leaf(op) = &c[1] {
609 if op == "=" || op == "!=" {
610 if !is_structurally_same(&c[0], &pair[0])
611 || !is_structurally_same(&c[2], &pair[1])
612 {
613 return Err(err(
614 path,
615 format!(
616 "operands `{} {}` ≠ `{} {}`",
617 key_of(&pair[0]),
618 key_of(&pair[1]),
619 key_of(&c[0]),
620 key_of(&c[2])
621 ),
622 ));
623 }
624 let exp = expected_rule(expr, ops, assigned);
625 if exp == rule {
626 return Ok(rule.into());
627 }
628 return Err(err(path, format!("rule `{}` ≠ expected `{}`", rule, exp)));
629 }
630 }
631 }
632 }
633 Err(err(path, format!("rule `{}` ≠ `{}`", rule, key_of(expr))))
634}
635
636fn check_typesys(
637 expr: &Node,
638 rule: &str,
639 subs: &[Node],
640 path: &[String],
641) -> Result<String, CheckError> {
642 let bad = |reason: &str| {
643 Err(err(
644 path,
645 format!("`{}` ≠ `{}` ({})", rule, key_of(expr), reason),
646 ))
647 };
648 if let Node::List(c) = expr {
649 match (rule, c.len()) {
650 ("type-universe", 2) => {
651 arity(rule, subs, 1, path)?;
652 if let Node::Leaf(h) = &c[0] {
653 if h == "Type" && is_structurally_same(&c[1], &subs[0]) {
654 return Ok(rule.into());
655 }
656 }
657 return bad("Type level mismatch");
658 }
659 ("prop", 1) => {
660 arity(rule, subs, 0, path)?;
661 if let Node::Leaf(h) = &c[0] {
662 if h == "Prop" {
663 return Ok(rule.into());
664 }
665 }
666 return bad("Prop head mismatch");
667 }
668 ("pi-formation", 3) | ("lambda-formation", 3) => {
669 arity(rule, subs, 2, path)?;
670 let head = if rule == "pi-formation" {
671 "Pi"
672 } else {
673 "lambda"
674 };
675 if let Node::Leaf(h) = &c[0] {
676 if h == head
677 && is_structurally_same(&c[1], &subs[0])
678 && is_structurally_same(&c[2], &subs[1])
679 {
680 return Ok(rule.into());
681 }
682 }
683 return bad("binder mismatch");
684 }
685 ("type-query", 3) => {
686 arity(rule, subs, 1, path)?;
687 if let (Node::Leaf(h), Node::Leaf(of)) = (&c[0], &c[1]) {
688 if h == "type" && of == "of" && is_structurally_same(&c[2], &subs[0]) {
689 return Ok(rule.into());
690 }
691 }
692 return bad("type-query mismatch");
693 }
694 ("type-check", 3) => {
695 arity(rule, subs, 2, path)?;
696 if let Node::Leaf(of) = &c[1] {
697 if of == "of"
698 && is_structurally_same(&c[0], &subs[0])
699 && is_structurally_same(&c[2], &subs[1])
700 {
701 return Ok(rule.into());
702 }
703 }
704 return bad("type-check mismatch");
705 }
706 ("substitution", 4) => {
707 arity(rule, subs, 3, path)?;
708 if let Node::Leaf(h) = &c[0] {
709 if h == "subst"
710 && is_structurally_same(&c[1], &subs[0])
711 && is_structurally_same(&c[2], &subs[1])
712 && is_structurally_same(&c[3], &subs[2])
713 {
714 return Ok(rule.into());
715 }
716 }
717 return bad("substitution mismatch");
718 }
719 ("fresh", 4) => {
720 arity(rule, subs, 2, path)?;
721 if let (Node::Leaf(h), Node::Leaf(in_kw)) = (&c[0], &c[2]) {
722 if h == "fresh"
723 && in_kw == "in"
724 && is_structurally_same(&c[1], &subs[0])
725 && is_structurally_same(&c[3], &subs[1])
726 {
727 return Ok(rule.into());
728 }
729 }
730 return bad("fresh mismatch");
731 }
732 _ => {}
733 }
734 }
735 bad("shape mismatch")
736}
737
738fn check_prefix(
739 expr: &Node,
740 rule: &str,
741 subs: &[Node],
742 ops: &HashSet<String>,
743 assigned: &HashSet<String>,
744 next_path: &[String],
745 path: &[String],
746) -> Result<String, CheckError> {
747 if let Node::List(c) = expr {
748 if let Some(Node::Leaf(h)) = c.first() {
749 if h == rule && ops.contains(rule) {
750 arity(rule, subs, c.len() - 1, path)?;
751 for (i, sub) in subs.iter().enumerate() {
752 check_node(&c[1 + i], sub, ops, assigned, next_path)?;
753 }
754 return Ok(rule.into());
755 }
756 }
757 }
758 Err(err(
759 path,
760 format!("unknown rule `{}` for `{}`", rule, key_of(expr)),
761 ))
762}
763
764pub fn check_program(program_src: &str, proofs_src: &str) -> CheckResult {
769 let program_forms = parse_forms(program_src);
770 let proof_forms = parse_forms(proofs_src);
771 let queries: Vec<Node> = program_forms.iter().filter_map(query_target).collect();
772 let ops = collect_operators(&program_forms);
773 let assigned = collect_assignments(&program_forms);
774 let mut result = CheckResult::default();
775 if queries.len() != proof_forms.len() {
776 result.errors.push(CheckError {
777 path: vec![],
778 message: format!(
779 "expected {} derivation(s), got {}",
780 queries.len(),
781 proof_forms.len()
782 ),
783 });
784 return result;
785 }
786 for (i, (q, p)) in queries.iter().zip(proof_forms.iter()).enumerate() {
787 let path = vec![format!("query[{}]", i)];
788 match check_node(q, p, &ops, &assigned, &path) {
789 Ok(rule) => result.ok.push(CheckOk {
790 rule,
791 expr: key_of(q),
792 }),
793 Err(e) => result.errors.push(e),
794 }
795 }
796 result
797}
798
799#[cfg(test)]
800mod sanity {
801 use super::*;
804
805 #[test]
806 fn smoke_structural_equality() {
807 assert!(
808 check_program("(a: a is a)\n(? (a = a))", "(by structural-equality (a a))").is_ok()
809 );
810 }
811
812 #[test]
813 fn smoke_mutated_rule_fails() {
814 assert!(!check_program("(a: a is a)\n(? (a = a))", "(by numeric-equality (a a))").is_ok());
815 }
816}