use serde_json::Value;
use crate::ast::{BinOp, Expr, Literal, Spanned, TopLevel, VerifyKind};
pub const AVER_DUMP_GOAL_ELAB: &str = r#"import Lean
open Lean Elab Tactic Meta in
private def averJsonEsc (s : String) : String := Id.run do
let mut out := ""
for c in s.toList do
out := out ++ (match c with
| '"' => "\\\""
| '\\' => "\\\\"
| '\n' => "\\n"
| '\r' => "\\r"
| '\t' => "\\t"
| c => toString c)
return out
open Lean in
private partial def averExprJson (names : List String) (e : Expr) : String :=
match e with
| .forallE n t b _ =>
let nm := toString n
"{\"forall\":{\"name\":\"" ++ averJsonEsc nm ++ "\",\"ty\":" ++ averExprJson names t
++ ",\"body\":" ++ averExprJson (nm :: names) b ++ "}}"
| .const n _ => "{\"const\":\"" ++ averJsonEsc (toString n) ++ "\"}"
| .bvar i =>
match names[i]? with
| some nm => "{\"var\":\"" ++ averJsonEsc nm ++ "\"}"
| none => "{\"opaque\":\"bvar\"}"
| .lit (.natVal v) => "{\"nat\":\"" ++ toString v ++ "\"}"
| .lit (.strVal s) => "{\"str\":\"" ++ averJsonEsc s ++ "\"}"
| .mdata _ e => averExprJson names e
| .app .. =>
let fn := e.getAppFn
let args := e.getAppArgs
let argsJson := String.intercalate "," (args.toList.map (averExprJson names))
"{\"app\":{\"fn\":" ++ averExprJson names fn ++ ",\"args\":[" ++ argsJson ++ "]}}"
| .proj s i e =>
"{\"proj\":{\"struct\":\"" ++ averJsonEsc (toString s) ++ "\",\"idx\":" ++ toString i
++ ",\"e\":" ++ averExprJson names e ++ "}}"
| _ => "{\"opaque\":\"other\"}"
open Lean Elab Tactic Meta in
elab "aver_dump_goal " s:str : tactic => do
match (← getUnsolvedGoals) with
| [] => pure ()
| g :: _ =>
g.withContext do
let ty ← instantiateMVars (← g.getType)
let fvars ← (← getLCtx).foldlM (init := (#[] : Array Expr)) fun acc d =>
pure (if d.isImplementationDetail then acc else acc.push d.toExpr)
let closed ← instantiateMVars (← mkForallFVars fvars ty)
logInfo m!"AVER_GOAL_JSON:{s.getString}:{averExprJson [] closed}"
"#;
pub const GOAL_JSON_MARKER: &str = "AVER_GOAL_JSON:";
#[derive(Debug, Clone, PartialEq)]
pub struct EngineGap {
pub reason: String,
}
impl EngineGap {
fn new(reason: impl Into<String>) -> Self {
EngineGap {
reason: reason.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct UntranslatedGoal {
pub givens: Vec<(String, String)>,
pub premises: Vec<Spanned<Expr>>,
pub claim: (Spanned<Expr>, Spanned<Expr>),
}
fn sp(e: Expr) -> Spanned<Expr> {
Spanned::new(e, 0)
}
#[derive(Debug, Clone, Default)]
pub struct UntranslateCtx {
pub peano: Option<PeanoCtx>,
}
#[derive(Debug, Clone)]
pub struct PeanoCtx {
pub type_name: String,
pub zero_ctor: String,
pub succ_ctor: String,
}
pub fn peano_ctx_for_law(items: &[TopLevel], fn_name: &str, law_name: &str) -> UntranslateCtx {
let peanos: Vec<_> = items
.iter()
.filter_map(|it| match it {
TopLevel::TypeDef(td) => crate::codegen::proof_recognize::detect_canonical_peano(td),
_ => None,
})
.collect();
if peanos.is_empty() {
return UntranslateCtx::default();
}
for it in items {
if let TopLevel::Verify(vb) = it
&& vb.fn_name == fn_name
&& let VerifyKind::Law(law) = &vb.kind
&& law.name == law_name
{
for g in &law.givens {
for tok in type_name_tokens(&g.type_name) {
if let Some(p) = peanos.iter().find(|p| p.type_name == tok) {
return UntranslateCtx {
peano: Some(PeanoCtx {
type_name: p.type_name.clone(),
zero_ctor: p.base_ctor.clone(),
succ_ctor: p.succ_ctor.clone(),
}),
};
}
}
}
}
}
UntranslateCtx::default()
}
fn type_name_tokens(ty: &str) -> Vec<String> {
ty.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
pub fn untranslate_goal(json: &str) -> Result<UntranslatedGoal, EngineGap> {
untranslate_goal_ctx(json, &UntranslateCtx::default())
}
pub fn untranslate_goal_ctx(
json: &str,
ctx: &UntranslateCtx,
) -> Result<UntranslatedGoal, EngineGap> {
let v: Value = serde_json::from_str(json)
.map_err(|e| EngineGap::new(format!("malformed goal JSON: {e}")))?;
let mut givens: Vec<(String, String)> = Vec::new();
let mut premises: Vec<Spanned<Expr>> = Vec::new();
let mut cur = &v;
while let Some(fa) = cur.get("forall") {
let name = fa
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| EngineGap::new("forall binder without a name"))?;
let ty = fa
.get("ty")
.ok_or_else(|| EngineGap::new("forall binder without a type"))?;
let body = fa
.get("body")
.ok_or_else(|| EngineGap::new("forall binder without a body"))?;
if is_vacuous_prop(ty) {
} else if is_prop_type(ty) {
premises.push(untranslate_bool(ty, ctx)?);
} else {
let tn = aver_type_name(ty, ctx).ok_or_else(|| {
EngineGap::new(format!(
"binder `{name}` has a type outside the grammar (cannot name it in Aver)"
))
})?;
givens.push((name.to_string(), tn));
}
cur = body;
}
let (lhs, rhs) = untranslate_eq(cur, ctx)
.ok_or_else(|| EngineGap::new("goal claim is not an equality our grammar renders"))?;
Ok(UntranslatedGoal {
givens,
premises,
claim: (lhs?, rhs?),
})
}
fn untranslate_bool(v: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
if let Some((l, r)) = untranslate_eq(v, ctx) {
return Ok(sp(Expr::BinOp(BinOp::Eq, Box::new(l?), Box::new(r?))));
}
if let Some(inner) = negated_eq_inner(v)
&& let Some((l, r)) = untranslate_eq(inner, ctx)
{
return Ok(sp(Expr::BinOp(BinOp::Neq, Box::new(l?), Box::new(r?))));
}
if let Some(app) = v.get("app")
&& let Some(op) = comparison_binop(head_const(app))
{
require_int_carrier(app)?;
let args = app_operands(app, 2)?;
return Ok(sp(Expr::BinOp(
op,
Box::new(untranslate_expr(&args[0], ctx)?),
Box::new(untranslate_expr(&args[1], ctx)?),
)));
}
Err(EngineGap::new(
"premise hypothesis is not an equality/comparison our grammar renders",
))
}
#[allow(clippy::type_complexity)]
fn untranslate_eq(
v: &Value,
ctx: &UntranslateCtx,
) -> Option<(
Result<Spanned<Expr>, EngineGap>,
Result<Spanned<Expr>, EngineGap>,
)> {
let app = v.get("app")?;
if head_const(app) != Some("Eq") {
return None;
}
let args = app.get("args")?.as_array()?;
let n = args.len();
if n < 2 {
return None;
}
Some((
untranslate_expr(&args[n - 2], ctx),
untranslate_expr(&args[n - 1], ctx),
))
}
fn untranslate_expr(v: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
if let Some(nat) = v.get("nat").and_then(Value::as_str) {
if let Some(p) = &ctx.peano {
return peano_numeral_from_str(p, nat).map(sp);
}
return Ok(sp(int_literal(nat)));
}
if let Some(s) = v.get("str").and_then(Value::as_str) {
return Ok(sp(Expr::Literal(Literal::Str(s.to_string()))));
}
if let Some(name) = v.get("var").and_then(Value::as_str) {
return Ok(sp(Expr::Ident(name.to_string())));
}
if let Some(name) = v.get("const").and_then(Value::as_str) {
return Ok(sp(const_to_expr(name)));
}
if let Some(app) = v.get("app") {
return untranslate_app(app, ctx);
}
if let Some(o) = v.get("opaque").and_then(Value::as_str) {
return Err(EngineGap::new(format!("goal contains a `{o}` node")));
}
if v.get("forall").is_some() {
return Err(EngineGap::new("nested quantifier in the goal claim"));
}
if v.get("proj").is_some() {
return Err(EngineGap::new("field projection outside the grammar"));
}
Err(EngineGap::new("unrecognised goal node"))
}
fn untranslate_app(app: &Value, ctx: &UntranslateCtx) -> Result<Spanned<Expr>, EngineGap> {
let head = head_const(app);
if head == Some("OfNat.ofNat") {
let nat = app
.get("args")
.and_then(Value::as_array)
.and_then(|args| {
args.iter()
.find_map(|a| a.get("nat").and_then(Value::as_str))
})
.ok_or_else(|| EngineGap::new("OfNat literal without a nat operand"))?;
if let Some(p) = &ctx.peano
&& carrier_const(app) == Some("Nat")
{
return peano_numeral_from_str(p, nat).map(sp);
}
return Ok(sp(int_literal(nat)));
}
if head == Some("Nat.succ")
&& let Some(p) = &ctx.peano
{
let ops = app_operands(app, 1)?;
return Ok(sp(peano_succ(p, untranslate_expr(&ops[0], ctx)?)));
}
if head == Some("Neg.neg") {
require_int_carrier(app)?;
let args = app_operands(app, 1)?;
return Ok(sp(Expr::Neg(Box::new(untranslate_expr(&args[0], ctx)?))));
}
if is_numeric_coercion(head) {
return Err(EngineGap::new("numeric coercion outside the grammar"));
}
if let Some(op) = arith_binop(head) {
if let Some(p) = &ctx.peano
&& matches!(head, Some("HAdd.hAdd") | Some("Add.add"))
&& carrier_const(app) == Some("Nat")
{
let ops = app_operands(app, 2)?;
if nat_lit_value(&ops[1]) == Some(1) {
return Ok(sp(peano_succ(p, untranslate_expr(&ops[0], ctx)?)));
}
}
require_int_carrier(app)?;
let args = app_operands(app, 2)?;
return Ok(sp(Expr::BinOp(
op,
Box::new(untranslate_expr(&args[0], ctx)?),
Box::new(untranslate_expr(&args[1], ctx)?),
)));
}
if let Some(op) = comparison_binop(head) {
require_int_carrier(app)?;
let args = app_operands(app, 2)?;
return Ok(sp(Expr::BinOp(
op,
Box::new(untranslate_expr(&args[0], ctx)?),
Box::new(untranslate_expr(&args[1], ctx)?),
)));
}
if head == Some("Eq") {
return Err(EngineGap::new("equality nested inside the claim"));
}
if head == Some("List.nil") {
return Ok(sp(Expr::List(vec![])));
}
if head == Some("List.cons") {
let ops = app_operands(app, 2)?;
let a = untranslate_expr(&ops[0], ctx)?;
let b = untranslate_expr(&ops[1], ctx)?;
return Ok(sp(Expr::FnCall(
Box::new(sp(Expr::Ident("List.concat".to_string()))),
vec![sp(Expr::List(vec![a])), b],
)));
}
if matches!(
head,
Some("HAppend.hAppend") | Some("Append.append") | Some("List.append")
) {
let ops = app_operands(app, 2)?;
return Ok(sp(Expr::FnCall(
Box::new(sp(Expr::Ident("List.concat".to_string()))),
vec![
untranslate_expr(&ops[0], ctx)?,
untranslate_expr(&ops[1], ctx)?,
],
)));
}
let Some(name) = head else {
return Err(EngineGap::new(
"application head is not a named function (higher-order)",
));
};
let all = app
.get("args")
.and_then(Value::as_array)
.ok_or_else(|| EngineGap::new("application without args"))?;
let mut args = Vec::with_capacity(all.len());
for a in all {
args.push(untranslate_expr(a, ctx)?);
}
Ok(sp(Expr::FnCall(Box::new(sp(const_to_expr(name))), args)))
}
fn head_const(app: &Value) -> Option<&str> {
app.get("fn")?.get("const")?.as_str()
}
fn app_operands(app: &Value, n: usize) -> Result<Vec<Value>, EngineGap> {
let args = app
.get("args")
.and_then(Value::as_array)
.ok_or_else(|| EngineGap::new("application without args"))?;
if args.len() < n {
return Err(EngineGap::new(
"application has fewer operands than expected",
));
}
Ok(args[args.len() - n..].to_vec())
}
fn arith_binop(head: Option<&str>) -> Option<BinOp> {
match head? {
"HAdd.hAdd" | "Add.add" => Some(BinOp::Add),
"HSub.hSub" | "Sub.sub" => Some(BinOp::Sub),
"HMul.hMul" | "Mul.mul" => Some(BinOp::Mul),
"HDiv.hDiv" | "Div.div" => Some(BinOp::Div),
_ => None,
}
}
fn comparison_binop(head: Option<&str>) -> Option<BinOp> {
match head? {
"LT.lt" => Some(BinOp::Lt),
"LE.le" => Some(BinOp::Lte),
"GT.gt" => Some(BinOp::Gt),
"GE.ge" => Some(BinOp::Gte),
"Ne" => Some(BinOp::Neq),
_ => None,
}
}
fn is_numeric_coercion(head: Option<&str>) -> bool {
matches!(
head,
Some(
"Nat.cast"
| "NatCast.natCast"
| "IntCast.intCast"
| "Int.ofNat"
| "Int.cast"
| "Int.toNat"
)
)
}
fn require_int_carrier(app: &Value) -> Result<(), EngineGap> {
match app
.get("args")
.and_then(Value::as_array)
.and_then(|a| a.first())
.and_then(|t| t.get("const"))
.and_then(Value::as_str)
{
Some("Int") => Ok(()),
Some("Nat") => Err(EngineGap::new(
"natural-number arithmetic (truncated subtraction differs from Aver's Int)",
)),
_ => Err(EngineGap::new(
"arithmetic over a non-Int carrier type outside the grammar",
)),
}
}
fn const_to_expr(name: &str) -> Expr {
match name {
"Bool.true" | "True" => return Expr::Literal(Literal::Bool(true)),
"Bool.false" | "False" => return Expr::Literal(Literal::Bool(false)),
"List.nil" => return Expr::List(vec![]),
_ => {}
}
Expr::Ident(lean_dotted_to_aver(name))
}
fn lean_dotted_to_aver(name: &str) -> String {
match name.rsplit_once('.') {
Some((prefix, last)) => format!("{prefix}.{}", super::expr::lean_name_to_aver(last)),
None => super::expr::lean_name_to_aver(name),
}
}
fn aver_type_name(v: &Value, ctx: &UntranslateCtx) -> Option<String> {
if let Some(name) = v.get("const").and_then(Value::as_str) {
return Some(match name {
"Int" => "Int".to_string(),
"Bool" => "Bool".to_string(),
"String" => "Str".to_string(),
"Nat" => match &ctx.peano {
Some(p) => p.type_name.clone(),
None => lean_dotted_to_aver("Nat"),
},
other => lean_dotted_to_aver(other),
});
}
if let Some(app) = v.get("app") {
if head_const(app) == Some("List") {
let args = app.get("args")?.as_array()?;
let inner = aver_type_name(args.last()?, ctx)?;
return Some(format!("List<{inner}>"));
}
}
None
}
fn is_prop_type(v: &Value) -> bool {
let Some(app) = v.get("app") else {
return false;
};
let head = head_const(app);
head == Some("Eq") || comparison_binop(head).is_some() || negated_eq_inner(v).is_some()
}
fn negated_eq_inner(v: &Value) -> Option<&Value> {
let app = v.get("app")?;
if head_const(app) != Some("Not") {
return None;
}
let inner = app.get("args")?.as_array()?.last()?;
if inner.get("app").map(head_const)? == Some("Eq") {
Some(inner)
} else {
None
}
}
fn int_literal(nat: &str) -> Expr {
match nat.parse::<i64>() {
Ok(n) => Expr::Literal(Literal::Int(n)),
Err(_) => Expr::Literal(Literal::BigInt(nat.to_string())),
}
}
fn is_vacuous_prop(v: &Value) -> bool {
if v.get("const").and_then(Value::as_str) == Some("True") {
return true;
}
let Some(app) = v.get("app") else {
return false;
};
match head_const(app) {
Some("Eq") => eq_operands_identical(app),
Some("Not") => app
.get("args")
.and_then(Value::as_array)
.and_then(|a| a.last())
.is_some_and(is_vacuous_prop),
_ => false,
}
}
fn eq_operands_identical(app: &Value) -> bool {
match app.get("args").and_then(Value::as_array) {
Some(args) if args.len() >= 2 => args[args.len() - 2] == args[args.len() - 1],
_ => false,
}
}
fn nat_lit_value(v: &Value) -> Option<u64> {
if let Some(n) = v.get("nat").and_then(Value::as_str) {
return n.parse().ok();
}
let app = v.get("app")?;
if head_const(app) != Some("OfNat.ofNat") {
return None;
}
app.get("args")?
.as_array()?
.iter()
.find_map(|a| a.get("nat").and_then(Value::as_str))
.and_then(|n| n.parse().ok())
}
fn carrier_const(app: &Value) -> Option<&str> {
app.get("args")?.as_array()?.first()?.get("const")?.as_str()
}
fn peano_zero(p: &PeanoCtx) -> Expr {
Expr::Ident(format!("{}.{}", p.type_name, p.zero_ctor))
}
fn peano_succ(p: &PeanoCtx, x: Spanned<Expr>) -> Expr {
Expr::FnCall(
Box::new(sp(Expr::Ident(format!("{}.{}", p.type_name, p.succ_ctor)))),
vec![x],
)
}
fn peano_numeral(p: &PeanoCtx, n: u64) -> Result<Expr, EngineGap> {
if n > 8 {
return Err(EngineGap::new(
"Peano numeral larger than 8 — declines rather than nesting Succ",
));
}
let mut e = peano_zero(p);
for _ in 0..n {
e = peano_succ(p, sp(e));
}
Ok(e)
}
fn peano_numeral_from_str(p: &PeanoCtx, nat: &str) -> Result<Expr, EngineGap> {
let n: u64 = nat
.parse()
.map_err(|_| EngineGap::new("Peano numeral is not a natural number"))?;
peano_numeral(p, n)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::unparse;
fn render(g: &UntranslatedGoal) -> String {
let mut out = String::new();
for (n, t) in &g.givens {
out.push_str(&format!("given {n}: {t}; "));
}
for p in &g.premises {
let mut buf = String::new();
unparse::write_expr_public(&mut buf, p, 0).unwrap();
out.push_str(&format!("when {buf}; "));
}
let mut l = String::new();
let mut r = String::new();
unparse::write_expr_public(&mut l, &g.claim.0, 0).unwrap();
unparse::write_expr_public(&mut r, &g.claim.1, 0).unwrap();
out.push_str(&format!("{l} => {r}"));
out
}
fn eq_json(lhs: &str, rhs: &str) -> String {
format!(r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{lhs},{rhs}]}}}}"#)
}
fn hadd(a: &str, b: &str) -> String {
format!(
r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Int"}},{{"const":"Int"}},{{"const":"Int"}},{{"opaque":"other"}},{a},{b}]}}}}"#
)
}
fn var(n: &str) -> String {
format!(r#"{{"var":"{n}"}}"#)
}
fn forall(name: &str, ty: &str, body: &str) -> String {
format!(r#"{{"forall":{{"name":"{name}","ty":{ty},"body":{body}}}}}"#)
}
#[test]
fn round_trips_int_commutativity() {
let claim = eq_json(&hadd(&var("a"), &var("b")), &hadd(&var("b"), &var("a")));
let json = forall(
"a",
r#"{"const":"Int"}"#,
&forall("b", r#"{"const":"Int"}"#, &claim),
);
let g = untranslate_goal(&json).expect("in grammar");
assert_eq!(
g.givens,
vec![
("a".to_string(), "Int".to_string()),
("b".to_string(), "Int".to_string()),
]
);
assert!(g.premises.is_empty());
assert_eq!(render(&g), "given a: Int; given b: Int; (a + b) => (b + a)");
}
#[test]
fn premise_becomes_when() {
let le = format!(
r#"{{"app":{{"fn":{{"const":"LE.le"}},"args":[{{"const":"Int"}},{{"opaque":"other"}},{},{}]}}}}"#,
r#"{"nat":"0"}"#,
var("a")
);
let claim = eq_json(&hadd(&var("a"), r#"{"nat":"0"}"#), &var("a"));
let json = forall("a", r#"{"const":"Int"}"#, &forall("h", &le, &claim));
let g = untranslate_goal(&json).expect("in grammar");
assert_eq!(g.givens, vec![("a".to_string(), "Int".to_string())]);
assert_eq!(render(&g), "given a: Int; when (0 <= a); (a + 0) => a");
}
#[test]
fn user_fn_call_round_trips() {
let call = |arg: &str| format!(r#"{{"app":{{"fn":{{"const":"length"}},"args":[{arg}]}}}}"#);
let list_ty = r#"{"app":{"fn":{"const":"List"},"args":[{"const":"Int"}]}}"#;
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
call(&var("x")),
call(&var("x"))
);
let json = forall("x", list_ty, &claim);
let g = untranslate_goal(&json).expect("in grammar");
assert_eq!(g.givens, vec![("x".to_string(), "List<Int>".to_string())]);
assert_eq!(render(&g), "given x: List<Int>; length(x) => length(x)");
}
#[test]
fn residual_maps_list_builtins_and_ih_premise() {
let list_int = r#"{"app":{"fn":{"const":"List"},"args":[{"const":"Int"}]}}"#;
let inst = r#"{"opaque":"inst"}"#;
let length = |a: &str| format!(r#"{{"app":{{"fn":{{"const":"length"}},"args":[{a}]}}}}"#);
let qrev =
|a: &str, b: &str| format!(r#"{{"app":{{"fn":{{"const":"qrev"}},"args":[{a},{b}]}}}}"#);
let plus =
|a: &str, b: &str| format!(r#"{{"app":{{"fn":{{"const":"plus"}},"args":[{a},{b}]}}}}"#);
let nil =
format!(r#"{{"app":{{"fn":{{"const":"List.nil"}},"args":[{{"const":"Int"}}]}}}}"#);
let append = format!(
r#"{{"app":{{"fn":{{"const":"HAppend.hAppend"}},"args":[{list_int},{list_int},{list_int},{inst},{nil},{}]}}}}"#,
var("y")
);
let cons = format!(
r#"{{"app":{{"fn":{{"const":"List.cons"}},"args":[{{"const":"Int"}},{},{append}]}}}}"#,
var("head")
);
let one = format!(
r#"{{"app":{{"fn":{{"const":"OfNat.ofNat"}},"args":[{{"const":"Nat"}},{{"nat":"1"}},{inst}]}}}}"#
);
let rhs = format!(
r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Int"}},{{"const":"Int"}},{{"const":"Int"}},{inst},{},{one}]}}}}"#,
plus(&length(&var("tail")), &length(&var("y")))
);
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{},{rhs}]}}}}"#,
length(&qrev(&var("tail"), &cons))
);
let ih_ty = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Int"}},{},{}]}}}}"#,
length(&qrev(&var("tail"), &var("y"))),
plus(&length(&var("tail")), &length(&var("y")))
);
let json = forall(
"y",
list_int,
&forall(
"head",
r#"{"const":"Int"}"#,
&forall("tail", list_int, &forall("ih", &ih_ty, &claim)),
),
);
let g = untranslate_goal(&json).expect("in grammar");
assert_eq!(
g.givens,
vec![
("y".to_string(), "List<Int>".to_string()),
("head".to_string(), "Int".to_string()),
("tail".to_string(), "List<Int>".to_string()),
]
);
assert_eq!(g.premises.len(), 1);
let r = render(&g);
assert!(
r.contains("when (length(qrev(tail, y)) == plus(length(tail), length(y)))"),
"{r}"
);
assert!(r.contains("List.concat([head], List.concat([], y))"), "{r}");
assert!(r.ends_with("=> (plus(length(tail), length(y)) + 1)"), "{r}");
assert!(!r.contains("List.cons") && !r.contains("hAppend"), "{r}");
}
#[test]
fn declines_out_of_grammar_lambda() {
let claim = eq_json(r#"{"opaque":"other"}"#, &var("a"));
let json = forall("a", r#"{"const":"Int"}"#, &claim);
let err = untranslate_goal(&json).expect_err("out of grammar");
assert!(err.reason.contains("other"), "{}", err.reason);
}
#[test]
fn declines_non_equality_claim() {
let claim = format!(
r#"{{"app":{{"fn":{{"const":"LE.le"}},"args":[{{"const":"Int"}},{{"opaque":"other"}},{},{}]}}}}"#,
var("a"),
var("a")
);
let json = forall("a", r#"{"const":"Int"}"#, &claim);
let err = untranslate_goal(&json).expect_err("not an equality");
assert!(err.reason.contains("equality"), "{}", err.reason);
}
#[test]
fn declines_nat_subtraction() {
let hsub = format!(
r#"{{"app":{{"fn":{{"const":"HSub.hSub"}},"args":[{{"const":"Nat"}},{{"const":"Nat"}},{{"const":"Nat"}},{{"opaque":"inst"}},{},{}]}}}}"#,
var("a"),
var("b")
);
let lhs = format!(
r#"{{"app":{{"fn":{{"const":"HAdd.hAdd"}},"args":[{{"const":"Nat"}},{{"const":"Nat"}},{{"const":"Nat"}},{{"opaque":"inst"}},{hsub},{}]}}}}"#,
var("b")
);
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{lhs},{}]}}}}"#,
var("a")
);
let json = forall(
"a",
r#"{"const":"Nat"}"#,
&forall("b", r#"{"const":"Nat"}"#, &claim),
);
let err = untranslate_goal(&json).expect_err("Nat arithmetic must decline");
assert!(err.reason.contains("natural-number"), "{}", err.reason);
}
const DUMP_P66_0: &str = include_str!("testdata/lemma_calc_krok0/p66_0.json");
const DUMP_P66_1: &str = include_str!("testdata/lemma_calc_krok0/p66_1.json");
const DUMP_P66_2: &str = include_str!("testdata/lemma_calc_krok0/p66_2.json");
const DUMP_P73_0: &str = include_str!("testdata/lemma_calc_krok0/p73_0.json");
const DUMP_P73_1: &str = include_str!("testdata/lemma_calc_krok0/p73_1.json");
fn peano_nat() -> UntranslateCtx {
UntranslateCtx {
peano: Some(PeanoCtx {
type_name: "Nat".to_string(),
zero_ctor: "Z".to_string(),
succ_ctor: "S".to_string(),
}),
}
}
#[test]
fn peano_66_1_forces_successor_lemma() {
let g = untranslate_goal_ctx(DUMP_P66_1, &peano_nat()).expect("in grammar under Peano");
assert_eq!(
g.givens,
vec![("tail".to_string(), "List<Nat>".to_string())]
);
assert_eq!(g.premises.len(), 1, "only the IH survives as `when`");
assert_eq!(
render(&g),
"given tail: List<Nat>; \
when (le(len(filterZ(tail)), len(tail)) == true); \
le(len(filterZ(tail)), Nat.S(len(tail))) => true"
);
}
#[test]
fn peano_66_1_without_ctx_still_declines_nat_arithmetic() {
let err = untranslate_goal(DUMP_P66_1).expect_err("Nat `+1` declines without ctx");
assert!(err.reason.contains("natural-number"), "{}", err.reason);
}
#[test]
fn peano_66_0_drops_vacuous_binder_and_stays_in_grammar() {
let g = untranslate_goal_ctx(DUMP_P66_0, &peano_nat()).expect("in grammar under Peano");
assert_eq!(
g.givens,
vec![("tail".to_string(), "List<Nat>".to_string())]
);
assert_eq!(g.premises.len(), 1, "IH survives, vacuous binder dropped");
let r = render(&g);
assert!(
r.ends_with("le(len(List.concat([], filterZ(tail))), len(tail)) => true"),
"{r}"
);
assert!(!r.contains("hAppend") && !r.contains("List.cons"), "{r}");
}
#[test]
fn peano_66_2_declines_on_blocked_match() {
let err = untranslate_goal_ctx(DUMP_P66_2, &peano_nat()).expect_err("blocked match");
assert!(err.reason.contains("equality nested"), "{}", err.reason);
}
#[test]
fn peano_73_1_declines_on_blocked_match() {
let err = untranslate_goal_ctx(DUMP_P73_1, &peano_nat()).expect_err("blocked match");
assert!(err.reason.contains("equality nested"), "{}", err.reason);
}
#[test]
fn peano_73_0_in_grammar_but_still_carries_uncleaned_ite() {
let g = untranslate_goal_ctx(DUMP_P73_0, &peano_nat()).expect("in grammar (messy)");
let r = render(&g);
assert!(r.contains("ite("), "expected uncleaned ite shell: {r}");
assert!(r.contains("Nat.Z"), "{r}");
}
#[test]
fn peano_numeral_nests_succ() {
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
var("x"),
ofnat_nat("2"),
);
let json = forall("x", r#"{"const":"Nat"}"#, &claim);
let g = untranslate_goal_ctx(&json, &peano_nat()).expect("in grammar");
assert_eq!(render(&g), "given x: Nat; x => Nat.S(Nat.S(Nat.Z))");
}
#[test]
fn peano_numeral_over_eight_declines() {
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Nat"}},{},{}]}}}}"#,
var("x"),
ofnat_nat("9"),
);
let json = forall("x", r#"{"const":"Nat"}"#, &claim);
let err = untranslate_goal_ctx(&json, &peano_nat()).expect_err("too large");
assert!(err.reason.contains("larger than 8"), "{}", err.reason);
}
fn ofnat_nat(n: &str) -> String {
format!(
r#"{{"app":{{"fn":{{"const":"OfNat.ofNat"}},"args":[{{"const":"Nat"}},{{"nat":"{n}"}},{{"opaque":"inst"}}]}}}}"#
)
}
#[test]
fn negated_bool_equality_becomes_neq_when() {
let f = |a: &str| format!(r#"{{"app":{{"fn":{{"const":"f"}},"args":[{a}]}}}}"#);
let not_eq = format!(
r#"{{"app":{{"fn":{{"const":"Not"}},"args":[{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Bool"}},{},{{"const":"Bool.true"}}]}}}}]}}}}"#,
f(&var("x"))
);
let claim = format!(
r#"{{"app":{{"fn":{{"const":"Eq"}},"args":[{{"const":"Bool"}},{},{{"const":"Bool.true"}}]}}}}"#,
f(&var("x"))
);
let json = forall("x", r#"{"const":"Int"}"#, &forall("h", ¬_eq, &claim));
let g = untranslate_goal(&json).expect("in grammar");
assert_eq!(g.givens, vec![("x".to_string(), "Int".to_string())]);
assert_eq!(g.premises.len(), 1);
assert_eq!(
render(&g),
"given x: Int; when (f(x) != true); f(x) => true"
);
}
#[test]
fn type_name_tokens_splits_containers() {
assert_eq!(type_name_tokens("List<Nat>"), vec!["List", "Nat"]);
assert_eq!(type_name_tokens("Nat"), vec!["Nat"]);
assert_eq!(type_name_tokens("Map<Str, Int>"), vec!["Map", "Str", "Int"]);
}
#[test]
fn peano_ctx_detected_from_real_prop_66_source() {
let src = include_str!("../../../proof-corpus/tip/isaplanner-mono/prop_66.av");
let items = crate::source::parse_source(src).expect("prop_66 parses");
let ctx = peano_ctx_for_law(&items, "filterZ", "filterLenLe");
let p = ctx.peano.expect("filterLenLe has a List<Nat> given");
assert_eq!(p.type_name, "Nat");
assert_eq!(p.zero_ctor, "Z");
assert_eq!(p.succ_ctor, "S");
}
#[test]
fn peano_ctx_absent_when_no_peano_given() {
let src = "fn f(x: Int) -> Int\n x\n\n\
verify f law idem\n given x: Int = 1..3\n f(x) => f(x)\n";
let items = crate::source::parse_source(src).expect("parses");
let ctx = peano_ctx_for_law(&items, "f", "idem");
assert!(ctx.peano.is_none());
}
}