use std::fmt;
use super::lean::{MATHLIB_LINE_WIDTH, lean_ident, wrap_lean};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Block {
pub tactics: Vec<Tactic>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Tactic {
Have {
name: String,
ty: Option<String>,
proof: Proof,
},
Bullet(Block),
Raw(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Proof {
Term(String),
By(Block),
}
impl Proof {
pub fn term(text: impl Into<String>) -> Self {
Proof::Term(text.into())
}
pub fn by(block: Block) -> Self {
Proof::By(block)
}
}
impl Tactic {
pub fn have(name: impl Into<String>, ty: Option<&str>, proof: Proof) -> Self {
Tactic::Have {
name: name.into(),
ty: ty.map(str::to_string),
proof,
}
}
pub fn bullet(block: Block) -> Self {
Tactic::Bullet(block)
}
pub fn raw(text: impl Into<String>) -> Self {
Tactic::Raw(text.into())
}
pub fn introduced_names(&self) -> Vec<String> {
let mut out = Vec::new();
self.collect_names(&mut out);
out
}
fn collect_names(&self, out: &mut Vec<String>) {
match self {
Tactic::Have { name, proof, .. } => {
out.push(name.clone());
if let Proof::By(b) = proof {
for t in &b.tactics {
t.collect_names(out);
}
}
}
Tactic::Bullet(b) => {
for t in &b.tactics {
t.collect_names(out);
}
}
Tactic::Raw(_) => {}
}
}
}
impl Block {
pub fn new(tactics: Vec<Tactic>) -> Self {
Block { tactics }
}
pub fn push(&mut self, tactic: Tactic) -> &mut Self {
self.tactics.push(tactic);
self
}
pub fn is_empty(&self) -> bool {
self.tactics.is_empty()
}
pub fn render(&self, indent: &str) -> String {
self.render_width(indent, MATHLIB_LINE_WIDTH)
}
pub fn render_width(&self, indent: &str, width: usize) -> String {
let mut out = String::new();
self.write_lines(indent, &mut out);
wrap_lean(&out, width)
}
fn write_lines(&self, indent: &str, out: &mut String) {
for t in &self.tactics {
t.write_lines(indent, out);
}
}
}
impl Tactic {
fn write_lines(&self, indent: &str, out: &mut String) {
match self {
Tactic::Have { name, ty, proof } => {
let head = match ty {
Some(t) => format!("have {} : {t}", lean_ident(name)),
None => format!("have {}", lean_ident(name)),
};
match proof {
Proof::Term(term) => {
out.push_str(indent);
out.push_str(&head);
out.push_str(" := ");
out.push_str(term);
out.push('\n');
}
Proof::By(block) => {
out.push_str(indent);
out.push_str(&head);
out.push_str(" := by\n");
let inner = format!("{indent} ");
block.write_lines(&inner, out);
}
}
}
Tactic::Bullet(block) => {
let inner = format!("{indent} ");
let mut body = String::new();
block.write_lines(&inner, &mut body);
if body.is_empty() {
out.push_str(indent);
out.push_str("· skip\n");
return;
}
let first_rest = &body[inner.len()..];
out.push_str(indent);
out.push_str("· ");
out.push_str(first_rest);
}
Tactic::Raw(text) => {
for (i, line) in text.lines().enumerate() {
out.push_str(indent);
if i > 0 {
out.push_str(" ");
}
out.push_str(line);
out.push('\n');
}
if text.is_empty() {
out.push_str(indent);
out.push_str("skip\n");
}
}
}
}
}
impl fmt::Display for Block {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.render(""))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Decl {
pub kind: DeclKind,
pub name: String,
pub binders: Vec<String>,
pub statement: String,
pub body: Block,
pub doc: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeclKind {
Theorem,
Lemma,
Example,
}
impl Decl {
pub fn render(&self) -> String {
self.render_width(MATHLIB_LINE_WIDTH)
}
pub fn render_width(&self, width: usize) -> String {
let mut out = String::new();
if let Some(doc) = &self.doc {
out.push_str("/-- ");
out.push_str(doc);
out.push_str(" -/\n");
}
let keyword = match self.kind {
DeclKind::Theorem => "theorem",
DeclKind::Lemma => "lemma",
DeclKind::Example => "example",
};
let mut header = match self.kind {
DeclKind::Example => keyword.to_string(),
_ => format!("{keyword} {}", lean_ident(&self.name)),
};
for b in &self.binders {
header.push(' ');
header.push_str(b);
}
header.push_str(" :\n ");
header.push_str(&self.statement);
header.push_str(" := by\n");
out.push_str(&wrap_lean(&header, width));
out.push_str(&self.body.render_width(" ", width));
out
}
}
impl fmt::Display for Decl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.render())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nested_by_blocks_and_bullets_indent_structurally() {
let b = Block::new(vec![
Tactic::have(
"hg",
Some("(0 : ℝ) ≤ x"),
Proof::by(Block::new(vec![Tactic::raw("nlinarith [hx]")])),
),
Tactic::bullet(Block::new(vec![
Tactic::have(
"inner",
None,
Proof::by(Block::new(vec![Tactic::raw("simp"), Tactic::raw("ring")])),
),
Tactic::bullet(Block::new(vec![Tactic::raw("exact inner")])),
])),
Tactic::raw("linarith only [hg]"),
]);
assert_eq!(
b.render(" "),
" have hg : (0 : ℝ) ≤ x := by\n nlinarith [hx]\n · have inner := by\n simp\n ring\n · exact inner\n linarith only [hg]\n"
);
}
#[test]
fn long_bullet_lines_wrap_past_the_tactic_column() {
let long = format!(
"linarith only [{}]",
(0..30)
.map(|i| format!("h{i}"))
.collect::<Vec<_>>()
.join(", ")
);
let b = Block::new(vec![Tactic::bullet(Block::new(vec![
Tactic::raw(long),
Tactic::raw("exact h"),
]))]);
let text = b.render_width(" ", 60);
let lines: Vec<&str> = text.lines().collect();
assert!(lines[0].starts_with(" · linarith only [h0,"));
assert!(lines[1].starts_with(" h"), "{text}");
assert!(lines.iter().all(|l| l.chars().count() <= 60), "{text}");
assert_eq!(*lines.last().unwrap(), " exact h");
}
#[test]
fn generator_leaf_shape_matches_mathlib_compiled_text() {
let hg_ty = "(0 : ℝ) ≤ (150 * (j : ℝ) / 431 + 1) * (-(60 * (j : ℝ) * t) + 80 * (j : ℝ) * r + 42 * (j : ℝ) + 40 * r - 30 * t + 21)";
let facet = Block::new(vec![
Tactic::have(
"hg",
Some(hg_ty),
Proof::by(Block::new(vec![Tactic::raw(
"linarith only [e0, e0J, e0JJ, e2JK, e3, e3J, e5, e5J, e5JJ, e6, hK0]",
)])),
),
Tactic::raw("have hg' := nonneg_of_mul_nonneg_right hg (by linarith only [hJ0])"),
Tactic::raw("linarith only [hg']"),
]);
let leaf = Block::new(vec![
Tactic::raw(
"refine leafG346_single_poly (20 * (j : ℝ) + 10) ρ r t u T₅ hD hρ hx\n ?_ ?_",
),
Tactic::bullet(facet),
Tactic::bullet(Block::new(vec![Tactic::raw(
"linarith only [e6, e9, e11, e15]",
)])),
]);
assert_eq!(
leaf.render(" "),
" refine leafG346_single_poly (20 * (j : ℝ) + 10) ρ r t u T₅ hD hρ hx\n\
\x20 ?_ ?_\n\
\x20 · have hg : (0 : ℝ) ≤ (150 * (j : ℝ) / 431 + 1) *\n\
\x20 (-(60 * (j : ℝ) * t) + 80 * (j : ℝ) * r + 42 * (j : ℝ) + 40 * r - 30 * t + 21) := by\n\
\x20 linarith only [e0, e0J, e0JJ, e2JK, e3, e3J, e5, e5J, e5JJ, e6, hK0]\n\
\x20 have hg' := nonneg_of_mul_nonneg_right hg (by linarith only [hJ0])\n\
\x20 linarith only [hg']\n\
\x20 · linarith only [e6, e9, e11, e15]\n"
);
}
#[test]
fn names_and_empty_blocks() {
let b = Block::new(vec![
Tactic::have("a", None, Proof::term("rfl")),
Tactic::bullet(Block::new(vec![Tactic::have(
"b",
None,
Proof::by(Block::new(vec![Tactic::have(
"c",
None,
Proof::term("rfl"),
)])),
)])),
]);
let names: Vec<String> = b
.tactics
.iter()
.flat_map(Tactic::introduced_names)
.collect();
assert_eq!(names, ["a", "b", "c"]);
assert_eq!(Block::default().render(" "), "");
assert_eq!(
Tactic::bullet(Block::default()).introduced_names(),
Vec::<String>::new()
);
assert_eq!(
Block::new(vec![Tactic::bullet(Block::default())]).render(""),
"· skip\n"
);
}
}