1use serde_json::Value;
22
23use crate::ast::{BinOp, Expr, Literal, Spanned, TopLevel, VerifyKind};
24
25pub const AVER_DUMP_GOAL_ELAB: &str = r#"import Lean
32open Lean Elab Tactic Meta in
33private def averJsonEsc (s : String) : String := Id.run do
34 let mut out := ""
35 for c in s.toList do
36 out := out ++ (match c with
37 | '"' => "\\\""
38 | '\\' => "\\\\"
39 | '\n' => "\\n"
40 | '\r' => "\\r"
41 | '\t' => "\\t"
42 | c => toString c)
43 return out
44open Lean in
45private partial def averExprJson (names : List String) (e : Expr) : String :=
46 match e with
47 | .forallE n t b _ =>
48 let nm := toString n
49 "{\"forall\":{\"name\":\"" ++ averJsonEsc nm ++ "\",\"ty\":" ++ averExprJson names t
50 ++ ",\"body\":" ++ averExprJson (nm :: names) b ++ "}}"
51 | .const n _ => "{\"const\":\"" ++ averJsonEsc (toString n) ++ "\"}"
52 | .bvar i =>
53 match names[i]? with
54 | some nm => "{\"var\":\"" ++ averJsonEsc nm ++ "\"}"
55 | none => "{\"opaque\":\"bvar\"}"
56 | .lit (.natVal v) => "{\"nat\":\"" ++ toString v ++ "\"}"
57 | .lit (.strVal s) => "{\"str\":\"" ++ averJsonEsc s ++ "\"}"
58 | .mdata _ e => averExprJson names e
59 | .app .. =>
60 let fn := e.getAppFn
61 let args := e.getAppArgs
62 let argsJson := String.intercalate "," (args.toList.map (averExprJson names))
63 "{\"app\":{\"fn\":" ++ averExprJson names fn ++ ",\"args\":[" ++ argsJson ++ "]}}"
64 | .proj s i e =>
65 "{\"proj\":{\"struct\":\"" ++ averJsonEsc (toString s) ++ "\",\"idx\":" ++ toString i
66 ++ ",\"e\":" ++ averExprJson names e ++ "}}"
67 | _ => "{\"opaque\":\"other\"}"
68open Lean Elab Tactic Meta in
69elab "aver_dump_goal " s:str : tactic => do
70 match (← getUnsolvedGoals) with
71 | [] => pure ()
72 | g :: _ =>
73 g.withContext do
74 let ty ← instantiateMVars (← g.getType)
75 let fvars ← (← getLCtx).foldlM (init := (#[] : Array Expr)) fun acc d =>
76 pure (if d.isImplementationDetail then acc else acc.push d.toExpr)
77 let closed ← instantiateMVars (← mkForallFVars fvars ty)
78 logInfo m!"AVER_GOAL_JSON:{s.getString}:{averExprJson [] closed}"
79"#;
80
81pub const GOAL_JSON_MARKER: &str = "AVER_GOAL_JSON:";
84
85#[derive(Debug, Clone, PartialEq)]
89pub struct EngineGap {
90 pub reason: String,
91}
92
93impl EngineGap {
94 fn new(reason: impl Into<String>) -> Self {
95 EngineGap {
96 reason: reason.into(),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
106pub struct UntranslatedGoal {
107 pub givens: Vec<(String, String)>,
110 pub premises: Vec<Spanned<Expr>>,
113 pub claim: (Spanned<Expr>, Spanned<Expr>),
115}
116
117fn sp(e: Expr) -> Spanned<Expr> {
118 Spanned::new(e, 0)
119}
120
121#[derive(Debug, Clone, Default)]
132pub struct UntranslateCtx {
133 pub peano: Option<PeanoCtx>,
134}
135
136#[derive(Debug, Clone)]
140pub struct PeanoCtx {
141 pub type_name: String,
142 pub zero_ctor: String,
143 pub succ_ctor: String,
144}
145
146pub fn peano_ctx_for_law(items: &[TopLevel], fn_name: &str, law_name: &str) -> UntranslateCtx {
152 let peanos: Vec<_> = items
153 .iter()
154 .filter_map(|it| match it {
155 TopLevel::TypeDef(td) => crate::codegen::proof_recognize::detect_canonical_peano(td),
156 _ => None,
157 })
158 .collect();
159 if peanos.is_empty() {
160 return UntranslateCtx::default();
161 }
162 for it in items {
163 if let TopLevel::Verify(vb) = it
164 && vb.fn_name == fn_name
165 && let VerifyKind::Law(law) = &vb.kind
166 && law.name == law_name
167 {
168 for g in &law.givens {
169 for tok in type_name_tokens(&g.type_name) {
170 if let Some(p) = peanos.iter().find(|p| p.type_name == tok) {
171 return UntranslateCtx {
172 peano: Some(PeanoCtx {
173 type_name: p.type_name.clone(),
174 zero_ctor: p.base_ctor.clone(),
175 succ_ctor: p.succ_ctor.clone(),
176 }),
177 };
178 }
179 }
180 }
181 }
182 }
183 UntranslateCtx::default()
184}
185
186fn type_name_tokens(ty: &str) -> Vec<String> {
189 ty.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
190 .filter(|s| !s.is_empty())
191 .map(str::to_string)
192 .collect()
193}
194
195pub fn untranslate_goal(json: &str) -> Result<UntranslatedGoal, EngineGap> {
199 untranslate_goal_ctx(json, &UntranslateCtx::default())
200}
201
202pub fn untranslate_goal_ctx(
205 json: &str,
206 ctx: &UntranslateCtx,
207) -> Result<UntranslatedGoal, EngineGap> {
208 let v: Value = serde_json::from_str(json)
209 .map_err(|e| EngineGap::new(format!("malformed goal JSON: {e}")))?;
210 let mut givens: Vec<(String, String)> = Vec::new();
211 let mut premises: Vec<Spanned<Expr>> = Vec::new();
212 let mut cur = &v;
213 while let Some(fa) = cur.get("forall") {
216 let name = fa
217 .get("name")
218 .and_then(Value::as_str)
219 .ok_or_else(|| EngineGap::new("forall binder without a name"))?;
220 let ty = fa
221 .get("ty")
222 .ok_or_else(|| EngineGap::new("forall binder without a type"))?;
223 let body = fa
224 .get("body")
225 .ok_or_else(|| EngineGap::new("forall binder without a body"))?;
226 if is_vacuous_prop(ty) {
227 } else if is_prop_type(ty) {
232 premises.push(untranslate_bool(ty, ctx)?);
234 } else {
235 let tn = aver_type_name(ty, ctx).ok_or_else(|| {
236 EngineGap::new(format!(
237 "binder `{name}` has a type outside the grammar (cannot name it in Aver)"
238 ))
239 })?;
240 givens.push((name.to_string(), tn));
241 }
242 cur = body;
243 }
244 let (lhs, rhs) = untranslate_eq(cur, ctx)
246 .ok_or_else(|| EngineGap::new("goal claim is not an equality our grammar renders"))?;
247 Ok(UntranslatedGoal {
248 givens,
249 premises,
250 claim: (lhs?, rhs?),
251 })
252}
253
254fn untranslate_bool(v: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
257 if let Some((l, r)) = untranslate_eq(v, ctx) {
258 return Ok(sp(Expr::BinOp(BinOp::Eq, Box::new(l?), Box::new(r?))));
259 }
260 if let Some(inner) = negated_eq_inner(v)
264 && let Some((l, r)) = untranslate_eq(inner, ctx)
265 {
266 return Ok(sp(Expr::BinOp(BinOp::Neq, Box::new(l?), Box::new(r?))));
267 }
268 if let Some(app) = v.get("app")
269 && let Some(op) = comparison_binop(head_const(app))
270 {
271 require_int_carrier(app)?;
272 let args = app_operands(app, 2)?;
273 return Ok(sp(Expr::BinOp(
274 op,
275 Box::new(untranslate_expr(&args[0], ctx)?),
276 Box::new(untranslate_expr(&args[1], ctx)?),
277 )));
278 }
279 Err(EngineGap::new(
280 "premise hypothesis is not an equality/comparison our grammar renders",
281 ))
282}
283
284#[allow(clippy::type_complexity)]
286fn untranslate_eq(
287 v: &Value,
288 ctx: &UntranslateCtx,
289) -> Option<(
290 Result<Spanned<Expr>, EngineGap>,
291 Result<Spanned<Expr>, EngineGap>,
292)> {
293 let app = v.get("app")?;
294 if head_const(app) != Some("Eq") {
295 return None;
296 }
297 let args = app.get("args")?.as_array()?;
298 let n = args.len();
299 if n < 2 {
300 return None;
301 }
302 Some((
303 untranslate_expr(&args[n - 2], ctx),
304 untranslate_expr(&args[n - 1], ctx),
305 ))
306}
307
308fn untranslate_expr(v: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
311 if let Some(nat) = v.get("nat").and_then(Value::as_str) {
312 if let Some(p) = &ctx.peano {
313 return peano_numeral_from_str(p, nat).map(sp);
314 }
315 return Ok(sp(int_literal(nat)));
316 }
317 if let Some(s) = v.get("str").and_then(Value::as_str) {
318 return Ok(sp(Expr::Literal(Literal::Str(s.to_string()))));
319 }
320 if let Some(name) = v.get("var").and_then(Value::as_str) {
321 return Ok(sp(Expr::Ident(name.to_string())));
322 }
323 if let Some(name) = v.get("const").and_then(Value::as_str) {
324 return Ok(sp(const_to_expr(name)));
325 }
326 if let Some(app) = v.get("app") {
327 return untranslate_app(app, ctx);
328 }
329 if let Some(o) = v.get("opaque").and_then(Value::as_str) {
330 return Err(EngineGap::new(format!("goal contains a `{o}` node")));
331 }
332 if v.get("forall").is_some() {
333 return Err(EngineGap::new("nested quantifier in the goal claim"));
334 }
335 if v.get("proj").is_some() {
336 return Err(EngineGap::new("field projection outside the grammar"));
337 }
338 Err(EngineGap::new("unrecognised goal node"))
339}
340
341fn untranslate_app(app: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
342 let head = head_const(app);
343 if head == Some("OfNat.ofNat") {
345 let nat = app
346 .get("args")
347 .and_then(Value::as_array)
348 .and_then(|args| {
349 args.iter()
350 .find_map(|a| a.get("nat").and_then(Value::as_str))
351 })
352 .ok_or_else(|| EngineGap::new("OfNat literal without a nat operand"))?;
353 if let Some(p) = &ctx.peano
356 && carrier_const(app) == Some("Nat")
357 {
358 return peano_numeral_from_str(p, nat).map(sp);
359 }
360 return Ok(sp(int_literal(nat)));
361 }
362 if head == Some("Nat.succ")
364 && let Some(p) = &ctx.peano
365 {
366 let ops = app_operands(app, 1)?;
367 return Ok(sp(peano_succ(p, untranslate_expr(&ops[0], ctx)?)));
368 }
369 if head == Some("Neg.neg") {
371 require_int_carrier(app)?;
372 let args = app_operands(app, 1)?;
373 return Ok(sp(Expr::Neg(Box::new(untranslate_expr(&args[0], ctx)?))));
374 }
375 if is_numeric_coercion(head) {
379 return Err(EngineGap::new("numeric coercion outside the grammar"));
380 }
381 if let Some(op) = arith_binop(head) {
385 if let Some(p) = &ctx.peano
390 && matches!(head, Some("HAdd.hAdd") | Some("Add.add"))
391 && carrier_const(app) == Some("Nat")
392 {
393 let ops = app_operands(app, 2)?;
394 if nat_lit_value(&ops[1]) == Some(1) {
395 return Ok(sp(peano_succ(p, untranslate_expr(&ops[0], ctx)?)));
396 }
397 }
398 require_int_carrier(app)?;
399 let args = app_operands(app, 2)?;
400 return Ok(sp(Expr::BinOp(
401 op,
402 Box::new(untranslate_expr(&args[0], ctx)?),
403 Box::new(untranslate_expr(&args[1], ctx)?),
404 )));
405 }
406 if let Some(op) = comparison_binop(head) {
407 require_int_carrier(app)?;
408 let args = app_operands(app, 2)?;
409 return Ok(sp(Expr::BinOp(
410 op,
411 Box::new(untranslate_expr(&args[0], ctx)?),
412 Box::new(untranslate_expr(&args[1], ctx)?),
413 )));
414 }
415 if head == Some("Eq") {
416 return Err(EngineGap::new("equality nested inside the claim"));
417 }
418 if head == Some("List.nil") {
421 return Ok(sp(Expr::List(vec![])));
422 }
423 if head == Some("List.cons") {
424 let ops = app_operands(app, 2)?;
426 let a = untranslate_expr(&ops[0], ctx)?;
427 let b = untranslate_expr(&ops[1], ctx)?;
428 return Ok(sp(Expr::FnCall(
429 Box::new(sp(Expr::Ident("List.concat".to_string()))),
430 vec![sp(Expr::List(vec![a])), b],
431 )));
432 }
433 if matches!(
434 head,
435 Some("HAppend.hAppend") | Some("Append.append") | Some("List.append")
436 ) {
437 let ops = app_operands(app, 2)?;
439 return Ok(sp(Expr::FnCall(
440 Box::new(sp(Expr::Ident("List.concat".to_string()))),
441 vec![
442 untranslate_expr(&ops[0], ctx)?,
443 untranslate_expr(&ops[1], ctx)?,
444 ],
445 )));
446 }
447 let Some(name) = head else {
450 return Err(EngineGap::new(
451 "application head is not a named function (higher-order)",
452 ));
453 };
454 let all = app
455 .get("args")
456 .and_then(Value::as_array)
457 .ok_or_else(|| EngineGap::new("application without args"))?;
458 let mut args = Vec::with_capacity(all.len());
459 for a in all {
460 args.push(untranslate_expr(a, ctx)?);
461 }
462 Ok(sp(Expr::FnCall(Box::new(sp(const_to_expr(name))), args)))
463}
464
465fn head_const(app: &Value) -> Option<&str> {
467 app.get("fn")?.get("const")?.as_str()
468}
469
470fn app_operands(app: &Value, n: usize) -> Result<Vec<Value>, EngineGap> {
472 let args = app
473 .get("args")
474 .and_then(Value::as_array)
475 .ok_or_else(|| EngineGap::new("application without args"))?;
476 if args.len() < n {
477 return Err(EngineGap::new(
478 "application has fewer operands than expected",
479 ));
480 }
481 Ok(args[args.len() - n..].to_vec())
482}
483
484fn arith_binop(head: Option<&str>) -> Option<BinOp> {
485 match head? {
486 "HAdd.hAdd" | "Add.add" => Some(BinOp::Add),
487 "HSub.hSub" | "Sub.sub" => Some(BinOp::Sub),
488 "HMul.hMul" | "Mul.mul" => Some(BinOp::Mul),
489 "HDiv.hDiv" | "Div.div" => Some(BinOp::Div),
490 _ => None,
491 }
492}
493
494fn comparison_binop(head: Option<&str>) -> Option<BinOp> {
495 match head? {
496 "LT.lt" => Some(BinOp::Lt),
497 "LE.le" => Some(BinOp::Lte),
498 "GT.gt" => Some(BinOp::Gt),
499 "GE.ge" => Some(BinOp::Gte),
500 "Ne" => Some(BinOp::Neq),
501 _ => None,
502 }
503}
504
505fn is_numeric_coercion(head: Option<&str>) -> bool {
509 matches!(
510 head,
511 Some(
512 "Nat.cast"
513 | "NatCast.natCast"
514 | "IntCast.intCast"
515 | "Int.ofNat"
516 | "Int.cast"
517 | "Int.toNat"
518 )
519 )
520}
521
522fn require_int_carrier(app: &Value) -> Result<(), EngineGap> {
529 match app
530 .get("args")
531 .and_then(Value::as_array)
532 .and_then(|a| a.first())
533 .and_then(|t| t.get("const"))
534 .and_then(Value::as_str)
535 {
536 Some("Int") => Ok(()),
537 Some("Nat") => Err(EngineGap::new(
538 "natural-number arithmetic (truncated subtraction differs from Aver's Int)",
539 )),
540 _ => Err(EngineGap::new(
541 "arithmetic over a non-Int carrier type outside the grammar",
542 )),
543 }
544}
545
546fn const_to_expr(name: &str) -> Expr {
551 match name {
552 "Bool.true" | "True" => return Expr::Literal(Literal::Bool(true)),
553 "Bool.false" | "False" => return Expr::Literal(Literal::Bool(false)),
554 "List.nil" => return Expr::List(vec![]),
555 _ => {}
556 }
557 Expr::Ident(lean_dotted_to_aver(name))
558}
559
560fn lean_dotted_to_aver(name: &str) -> String {
563 match name.rsplit_once('.') {
564 Some((prefix, last)) => format!("{prefix}.{}", super::expr::lean_name_to_aver(last)),
565 None => super::expr::lean_name_to_aver(name),
566 }
567}
568
569fn aver_type_name(v: &Value, ctx: &UntranslateCtx) -> Option<String> {
574 if let Some(name) = v.get("const").and_then(Value::as_str) {
575 return Some(match name {
576 "Int" => "Int".to_string(),
577 "Bool" => "Bool".to_string(),
578 "String" => "Str".to_string(),
579 "Nat" => match &ctx.peano {
580 Some(p) => p.type_name.clone(),
581 None => lean_dotted_to_aver("Nat"),
582 },
583 other => lean_dotted_to_aver(other),
584 });
585 }
586 if let Some(app) = v.get("app") {
587 if head_const(app) == Some("List") {
589 let args = app.get("args")?.as_array()?;
590 let inner = aver_type_name(args.last()?, ctx)?;
591 return Some(format!("List<{inner}>"));
592 }
593 }
594 None
595}
596
597fn is_prop_type(v: &Value) -> bool {
600 let Some(app) = v.get("app") else {
601 return false;
602 };
603 let head = head_const(app);
604 head == Some("Eq") || comparison_binop(head).is_some() || negated_eq_inner(v).is_some()
605}
606
607fn negated_eq_inner(v: &Value) -> Option<&Value> {
609 let app = v.get("app")?;
610 if head_const(app) != Some("Not") {
611 return None;
612 }
613 let inner = app.get("args")?.as_array()?.last()?;
614 if inner.get("app").map(head_const)? == Some("Eq") {
615 Some(inner)
616 } else {
617 None
618 }
619}
620
621fn int_literal(nat: &str) -> Expr {
622 match nat.parse::<i64>() {
623 Ok(n) => Expr::Literal(Literal::Int(n)),
624 Err(_) => Expr::Literal(Literal::BigInt(nat.to_string())),
626 }
627}
628
629fn is_vacuous_prop(v: &Value) -> bool {
635 if v.get("const").and_then(Value::as_str) == Some("True") {
636 return true;
637 }
638 let Some(app) = v.get("app") else {
639 return false;
640 };
641 match head_const(app) {
642 Some("Eq") => eq_operands_identical(app),
643 Some("Not") => app
644 .get("args")
645 .and_then(Value::as_array)
646 .and_then(|a| a.last())
647 .is_some_and(is_vacuous_prop),
648 _ => false,
649 }
650}
651
652fn eq_operands_identical(app: &Value) -> bool {
654 match app.get("args").and_then(Value::as_array) {
655 Some(args) if args.len() >= 2 => args[args.len() - 2] == args[args.len() - 1],
656 _ => false,
657 }
658}
659
660fn nat_lit_value(v: &Value) -> Option<u64> {
663 if let Some(n) = v.get("nat").and_then(Value::as_str) {
664 return n.parse().ok();
665 }
666 let app = v.get("app")?;
667 if head_const(app) != Some("OfNat.ofNat") {
668 return None;
669 }
670 app.get("args")?
671 .as_array()?
672 .iter()
673 .find_map(|a| a.get("nat").and_then(Value::as_str))
674 .and_then(|n| n.parse().ok())
675}
676
677fn carrier_const(app: &Value) -> Option<&str> {
679 app.get("args")?.as_array()?.first()?.get("const")?.as_str()
680}
681
682fn peano_zero(p: &PeanoCtx) -> Expr {
684 Expr::Ident(format!("{}.{}", p.type_name, p.zero_ctor))
685}
686
687fn peano_succ(p: &PeanoCtx, x: Spanned<Expr>) -> Expr {
689 Expr::FnCall(
690 Box::new(sp(Expr::Ident(format!("{}.{}", p.type_name, p.succ_ctor)))),
691 vec![x],
692 )
693}
694
695fn peano_numeral(p: &PeanoCtx, n: u64) -> Result<Expr, EngineGap> {
697 if n > 8 {
698 return Err(EngineGap::new(
699 "Peano numeral larger than 8 — declines rather than nesting Succ",
700 ));
701 }
702 let mut e = peano_zero(p);
703 for _ in 0..n {
704 e = peano_succ(p, sp(e));
705 }
706 Ok(e)
707}
708
709fn peano_numeral_from_str(p: &PeanoCtx, nat: &str) -> Result<Expr, EngineGap> {
711 let n: u64 = nat
712 .parse()
713 .map_err(|_| EngineGap::new("Peano numeral is not a natural number"))?;
714 peano_numeral(p, n)
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720 use crate::ast::unparse;
721
722 fn render(g: &UntranslatedGoal) -> String {
724 let mut out = String::new();
725 for (n, t) in &g.givens {
726 out.push_str(&format!("given {n}: {t}; "));
727 }
728 for p in &g.premises {
729 let mut buf = String::new();
730 unparse::write_expr_public(&mut buf, p, 0).unwrap();
731 out.push_str(&format!("when {buf}; "));
732 }
733 let mut l = String::new();
734 let mut r = String::new();
735 unparse::write_expr_public(&mut l, &g.claim.0, 0).unwrap();
736 unparse::write_expr_public(&mut r, &g.claim.1, 0).unwrap();
737 out.push_str(&format!("{l} => {r}"));
738 out
739 }
740
741 fn eq_json(lhs: &str, rhs: &str) -> String {
744 format!(r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{lhs},{rhs}]}}}}"#)
745 }
746 fn hadd(a: &str, b: &str) -> String {
747 format!(
748 r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Int"}},{{"const":"Int"}},{{"const":"Int"}},{{"opaque":"other"}},{a},{b}]}}}}"#
749 )
750 }
751 fn var(n: &str) -> String {
752 format!(r#"{{"var":"{n}"}}"#)
753 }
754 fn forall(name: &str, ty: &str, body: &str) -> String {
755 format!(r#"{{"forall":{{"name":"{name}","ty":{ty},"body":{body}}}}}"#)
756 }
757
758 #[test]
759 fn round_trips_int_commutativity() {
760 let claim = eq_json(&hadd(&var("a"), &var("b")), &hadd(&var("b"), &var("a")));
762 let json = forall(
763 "a",
764 r#"{"const":"Int"}"#,
765 &forall("b", r#"{"const":"Int"}"#, &claim),
766 );
767 let g = untranslate_goal(&json).expect("in grammar");
768 assert_eq!(
769 g.givens,
770 vec![
771 ("a".to_string(), "Int".to_string()),
772 ("b".to_string(), "Int".to_string()),
773 ]
774 );
775 assert!(g.premises.is_empty());
776 assert_eq!(render(&g), "given a: Int; given b: Int; (a + b) => (b + a)");
777 }
778
779 #[test]
780 fn premise_becomes_when() {
781 let le = format!(
783 r#"{{"app":{{"fn":{{"const":"LE.le"}},"args":[{{"const":"Int"}},{{"opaque":"other"}},{},{}]}}}}"#,
784 r#"{"nat":"0"}"#,
785 var("a")
786 );
787 let claim = eq_json(&hadd(&var("a"), r#"{"nat":"0"}"#), &var("a"));
788 let json = forall("a", r#"{"const":"Int"}"#, &forall("h", &le, &claim));
789 let g = untranslate_goal(&json).expect("in grammar");
790 assert_eq!(g.givens, vec![("a".to_string(), "Int".to_string())]);
791 assert_eq!(render(&g), "given a: Int; when (0 <= a); (a + 0) => a");
792 }
793
794 #[test]
795 fn user_fn_call_round_trips() {
796 let call = |arg: &str| format!(r#"{{"app":{{"fn":{{"const":"length"}},"args":[{arg}]}}}}"#);
798 let list_ty = r#"{"app":{"fn":{"const":"List"},"args":[{"const":"Int"}]}}"#;
799 let claim = format!(
800 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
801 call(&var("x")),
802 call(&var("x"))
803 );
804 let json = forall("x", list_ty, &claim);
805 let g = untranslate_goal(&json).expect("in grammar");
806 assert_eq!(g.givens, vec![("x".to_string(), "List<Int>".to_string())]);
807 assert_eq!(render(&g), "given x: List<Int>; length(x) => length(x)");
808 }
809
810 #[test]
811 fn residual_maps_list_builtins_and_ih_premise() {
812 let list_int = r#"{"app":{"fn":{"const":"List"},"args":[{"const":"Int"}]}}"#;
819 let inst = r#"{"opaque":"inst"}"#;
820 let length = |a: &str| format!(r#"{{"app":{{"fn":{{"const":"length"}},"args":[{a}]}}}}"#);
821 let qrev =
822 |a: &str, b: &str| format!(r#"{{"app":{{"fn":{{"const":"qrev"}},"args":[{a},{b}]}}}}"#);
823 let plus =
824 |a: &str, b: &str| format!(r#"{{"app":{{"fn":{{"const":"plus"}},"args":[{a},{b}]}}}}"#);
825 let nil =
826 format!(r#"{{"app":{{"fn":{{"const":"List.nil"}},"args":[{{"const":"Int"}}]}}}}"#);
827 let append = format!(
829 r#"{{"app":{{"fn":{{"const":"HAppend.hAppend"}},"args":[{list_int},{list_int},{list_int},{inst},{nil},{}]}}}}"#,
830 var("y")
831 );
832 let cons = format!(
834 r#"{{"app":{{"fn":{{"const":"List.cons"}},"args":[{{"const":"Int"}},{},{append}]}}}}"#,
835 var("head")
836 );
837 let one = format!(
839 r#"{{"app":{{"fn":{{"const":"OfNat.ofNat"}},"args":[{{"const":"Nat"}},{{"nat":"1"}},{inst}]}}}}"#
840 );
841 let rhs = format!(
842 r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Int"}},{{"const":"Int"}},{{"const":"Int"}},{inst},{},{one}]}}}}"#,
843 plus(&length(&var("tail")), &length(&var("y")))
844 );
845 let claim = format!(
846 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{},{rhs}]}}}}"#,
847 length(&qrev(&var("tail"), &cons))
848 );
849 let ih_ty = format!(
850 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{},{}]}}}}"#,
851 length(&qrev(&var("tail"), &var("y"))),
852 plus(&length(&var("tail")), &length(&var("y")))
853 );
854 let json = forall(
855 "y",
856 list_int,
857 &forall(
858 "head",
859 r#"{"const":"Int"}"#,
860 &forall("tail", list_int, &forall("ih", &ih_ty, &claim)),
861 ),
862 );
863 let g = untranslate_goal(&json).expect("in grammar");
864 assert_eq!(
865 g.givens,
866 vec![
867 ("y".to_string(), "List<Int>".to_string()),
868 ("head".to_string(), "Int".to_string()),
869 ("tail".to_string(), "List<Int>".to_string()),
870 ]
871 );
872 assert_eq!(g.premises.len(), 1);
873 let r = render(&g);
874 assert!(
875 r.contains("when (length(qrev(tail, y)) == plus(length(tail), length(y)))"),
876 "{r}"
877 );
878 assert!(r.contains("List.concat([head], List.concat([], y))"), "{r}");
879 assert!(r.ends_with("=> (plus(length(tail), length(y)) + 1)"), "{r}");
880 assert!(!r.contains("List.cons") && !r.contains("hAppend"), "{r}");
881 }
882
883 #[test]
884 fn declines_out_of_grammar_lambda() {
885 let claim = eq_json(r#"{"opaque":"other"}"#, &var("a"));
887 let json = forall("a", r#"{"const":"Int"}"#, &claim);
888 let err = untranslate_goal(&json).expect_err("out of grammar");
889 assert!(err.reason.contains("other"), "{}", err.reason);
890 }
891
892 #[test]
893 fn declines_non_equality_claim() {
894 let claim = format!(
896 r#"{{"app":{{"fn":{{"const":"LE.le"}},"args":[{{"const":"Int"}},{{"opaque":"other"}},{},{}]}}}}"#,
897 var("a"),
898 var("a")
899 );
900 let json = forall("a", r#"{"const":"Int"}"#, &claim);
901 let err = untranslate_goal(&json).expect_err("not an equality");
902 assert!(err.reason.contains("equality"), "{}", err.reason);
903 }
904
905 #[test]
906 fn declines_nat_subtraction() {
907 let hsub = format!(
912 r#"{{"app":{{"fn":{{"const":"HSub.hSub"}},"args":[{{"const":"Nat"}},{{"const":"Nat"}},{{"const":"Nat"}},{{"opaque":"inst"}},{},{}]}}}}"#,
913 var("a"),
914 var("b")
915 );
916 let lhs = format!(
917 r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Nat"}},{{"const":"Nat"}},{{"const":"Nat"}},{{"opaque":"inst"}},{hsub},{}]}}}}"#,
918 var("b")
919 );
920 let claim = format!(
921 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{lhs},{}]}}}}"#,
922 var("a")
923 );
924 let json = forall(
925 "a",
926 r#"{"const":"Nat"}"#,
927 &forall("b", r#"{"const":"Nat"}"#, &claim),
928 );
929 let err = untranslate_goal(&json).expect_err("Nat arithmetic must decline");
930 assert!(err.reason.contains("natural-number"), "{}", err.reason);
931 }
932
933 const DUMP_P66_0: &str = include_str!("testdata/lemma_calc_krok0/p66_0.json");
942 const DUMP_P66_1: &str = include_str!("testdata/lemma_calc_krok0/p66_1.json");
943 const DUMP_P66_2: &str = include_str!("testdata/lemma_calc_krok0/p66_2.json");
944 const DUMP_P73_0: &str = include_str!("testdata/lemma_calc_krok0/p73_0.json");
945 const DUMP_P73_1: &str = include_str!("testdata/lemma_calc_krok0/p73_1.json");
946
947 fn peano_nat() -> UntranslateCtx {
949 UntranslateCtx {
950 peano: Some(PeanoCtx {
951 type_name: "Nat".to_string(),
952 zero_ctor: "Z".to_string(),
953 succ_ctor: "S".to_string(),
954 }),
955 }
956 }
957
958 #[test]
959 fn peano_66_1_forces_successor_lemma() {
960 let g = untranslate_goal_ctx(DUMP_P66_1, &peano_nat()).expect("in grammar under Peano");
965 assert_eq!(
966 g.givens,
967 vec![("tail".to_string(), "List<Nat>".to_string())]
968 );
969 assert_eq!(g.premises.len(), 1, "only the IH survives as `when`");
970 assert_eq!(
971 render(&g),
972 "given tail: List<Nat>; \
973 when (le(len(filterZ(tail)), len(tail)) == true); \
974 le(len(filterZ(tail)), Nat.S(len(tail))) => true"
975 );
976 }
977
978 #[test]
979 fn peano_66_1_without_ctx_still_declines_nat_arithmetic() {
980 let err = untranslate_goal(DUMP_P66_1).expect_err("Nat `+1` declines without ctx");
983 assert!(err.reason.contains("natural-number"), "{}", err.reason);
984 }
985
986 #[test]
987 fn peano_66_0_drops_vacuous_binder_and_stays_in_grammar() {
988 let g = untranslate_goal_ctx(DUMP_P66_0, &peano_nat()).expect("in grammar under Peano");
991 assert_eq!(
992 g.givens,
993 vec![("tail".to_string(), "List<Nat>".to_string())]
994 );
995 assert_eq!(g.premises.len(), 1, "IH survives, vacuous binder dropped");
996 let r = render(&g);
997 assert!(
998 r.ends_with("le(len(List.concat([], filterZ(tail))), len(tail)) => true"),
999 "{r}"
1000 );
1001 assert!(!r.contains("hAppend") && !r.contains("List.cons"), "{r}");
1002 }
1003
1004 #[test]
1005 fn peano_66_2_declines_on_blocked_match() {
1006 let err = untranslate_goal_ctx(DUMP_P66_2, &peano_nat()).expect_err("blocked match");
1011 assert!(err.reason.contains("equality nested"), "{}", err.reason);
1012 }
1013
1014 #[test]
1015 fn peano_73_1_declines_on_blocked_match() {
1016 let err = untranslate_goal_ctx(DUMP_P73_1, &peano_nat()).expect_err("blocked match");
1018 assert!(err.reason.contains("equality nested"), "{}", err.reason);
1019 }
1020
1021 #[test]
1022 fn peano_73_0_in_grammar_but_still_carries_uncleaned_ite() {
1023 let g = untranslate_goal_ctx(DUMP_P73_0, &peano_nat()).expect("in grammar (messy)");
1028 let r = render(&g);
1029 assert!(r.contains("ite("), "expected uncleaned ite shell: {r}");
1030 assert!(r.contains("Nat.Z"), "{r}");
1032 }
1033
1034 #[test]
1035 fn peano_numeral_nests_succ() {
1036 let claim = format!(
1038 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
1039 var("x"),
1040 ofnat_nat("2"),
1041 );
1042 let json = forall("x", r#"{"const":"Nat"}"#, &claim);
1043 let g = untranslate_goal_ctx(&json, &peano_nat()).expect("in grammar");
1044 assert_eq!(render(&g), "given x: Nat; x => Nat.S(Nat.S(Nat.Z))");
1045 }
1046
1047 #[test]
1048 fn peano_numeral_over_eight_declines() {
1049 let claim = format!(
1050 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
1051 var("x"),
1052 ofnat_nat("9"),
1053 );
1054 let json = forall("x", r#"{"const":"Nat"}"#, &claim);
1055 let err = untranslate_goal_ctx(&json, &peano_nat()).expect_err("too large");
1056 assert!(err.reason.contains("larger than 8"), "{}", err.reason);
1057 }
1058
1059 fn ofnat_nat(n: &str) -> String {
1060 format!(
1061 r#"{{"app":{{"fn":{{"const":"OfNat.ofNat"}},"args":[{{"const":"Nat"}},{{"nat":"{n}"}},{{"opaque":"inst"}}]}}}}"#
1062 )
1063 }
1064
1065 #[test]
1066 fn negated_bool_equality_becomes_neq_when() {
1067 let f = |a: &str| format!(r#"{{"app":{{"fn":{{"const":"f"}},"args":[{a}]}}}}"#);
1071 let not_eq = format!(
1072 r#"{{"app":{{"fn":{{"const":"Not"}},"args":[{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Bool"}},{},{{"const":"Bool.true"}}]}}}}]}}}}"#,
1073 f(&var("x"))
1074 );
1075 let claim = format!(
1076 r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Bool"}},{},{{"const":"Bool.true"}}]}}}}"#,
1077 f(&var("x"))
1078 );
1079 let json = forall("x", r#"{"const":"Int"}"#, &forall("h", ¬_eq, &claim));
1080 let g = untranslate_goal(&json).expect("in grammar");
1081 assert_eq!(g.givens, vec![("x".to_string(), "Int".to_string())]);
1082 assert_eq!(g.premises.len(), 1);
1083 assert_eq!(
1084 render(&g),
1085 "given x: Int; when (f(x) != true); f(x) => true"
1086 );
1087 }
1088
1089 #[test]
1090 fn type_name_tokens_splits_containers() {
1091 assert_eq!(type_name_tokens("List<Nat>"), vec!["List", "Nat"]);
1092 assert_eq!(type_name_tokens("Nat"), vec!["Nat"]);
1093 assert_eq!(type_name_tokens("Map<Str, Int>"), vec!["Map", "Str", "Int"]);
1094 }
1095
1096 #[test]
1097 fn peano_ctx_detected_from_real_prop_66_source() {
1098 let src = include_str!("../../../proof-corpus/tip/isaplanner-mono/prop_66.av");
1103 let items = crate::source::parse_source(src).expect("prop_66 parses");
1104 let ctx = peano_ctx_for_law(&items, "filterZ", "filterLenLe");
1105 let p = ctx.peano.expect("filterLenLe has a List<Nat> given");
1106 assert_eq!(p.type_name, "Nat");
1107 assert_eq!(p.zero_ctor, "Z");
1108 assert_eq!(p.succ_ctor, "S");
1109 }
1110
1111 #[test]
1112 fn peano_ctx_absent_when_no_peano_given() {
1113 let src = "fn f(x: Int) -> Int\n x\n\n\
1115 verify f law idem\n given x: Int = 1..3\n f(x) => f(x)\n";
1116 let items = crate::source::parse_source(src).expect("parses");
1117 let ctx = peano_ctx_for_law(&items, "f", "idem");
1118 assert!(ctx.peano.is_none());
1119 }
1120}