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),
Apply {
head: String,
args: Vec<String>,
},
Raw(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Proof {
Term(String),
By(Block),
}
struct Line {
text: String,
verbatim: bool,
}
impl Line {
fn wrap(text: String) -> Self {
Line {
text,
verbatim: false,
}
}
fn verbatim(text: String) -> Self {
Line {
text,
verbatim: true,
}
}
}
fn skip_line(indent: &str) -> Line {
Line::wrap(format!("{indent}skip"))
}
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 apply(head: impl Into<String>, args: Vec<String>) -> Self {
Tactic::Apply {
head: head.into(),
args,
}
}
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::Apply { .. } | 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 lines = Vec::new();
self.write_lines(indent, width, &mut lines);
let mut out = String::new();
for line in lines {
if line.verbatim {
out.push_str(&line.text);
} else {
out.push_str(&wrap_lean(&line.text, width));
}
out.push('\n');
}
out
}
fn write_lines(&self, indent: &str, width: usize, out: &mut Vec<Line>) {
for t in &self.tactics {
t.write_lines(indent, width, out);
}
}
}
impl Tactic {
fn write_lines(&self, indent: &str, width: usize, out: &mut Vec<Line>) {
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(Line::wrap(format!("{indent}{head} := {term}")));
}
Proof::By(block) => {
out.push(Line::wrap(format!("{indent}{head} := by")));
let inner = format!("{indent} ");
let before = out.len();
block.write_lines(&inner, width, out);
if out.len() == before {
out.push(skip_line(&inner));
}
}
}
}
Tactic::Bullet(block) => {
let inner = format!("{indent} ");
let mut body = Vec::new();
block.write_lines(&inner, width, &mut body);
let Some(first) = body.first_mut() else {
out.push(Line::wrap(format!("{indent}· skip")));
return;
};
if let Some(rest) = first.text.strip_prefix(inner.as_str()) {
first.text = format!("{indent}· {rest}");
}
out.append(&mut body);
}
Tactic::Apply { head, args } => {
let cont = format!("{indent} ");
let cont_len = cont.chars().count();
let mut line = format!("{indent}{head}");
let mut used = line.chars().count();
for arg in args {
let len = arg.chars().count();
if used + 1 + len > width {
out.push(Line::verbatim(line));
line = format!("{cont}{arg}");
used = cont_len + len;
} else {
line.push(' ');
line.push_str(arg);
used += 1 + len;
}
}
out.push(Line::verbatim(line));
}
Tactic::Raw(text) => {
for (i, line) in text.lines().enumerate() {
let cont = if i > 0 { " " } else { "" };
out.push(Line::wrap(format!("{indent}{cont}{line}")));
}
if text.is_empty() {
out.push(skip_line(indent));
}
}
}
}
}
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>,
pub preamble: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeclKind {
Theorem,
Lemma,
Example,
}
impl Decl {
pub fn new(
kind: DeclKind,
name: impl Into<String>,
statement: impl Into<String>,
body: Block,
) -> Self {
Decl {
kind,
name: name.into(),
binders: Vec::new(),
statement: statement.into(),
body,
doc: None,
preamble: Vec::new(),
}
}
#[must_use]
pub fn with_binders(mut self, binders: Vec<String>) -> Self {
self.binders = binders;
self
}
#[must_use]
pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
self.doc = Some(doc.into());
self
}
#[must_use]
pub fn with_preamble(mut self, preamble: Vec<String>) -> Self {
self.preamble = preamble;
self
}
pub fn render(&self) -> String {
self.render_width(MATHLIB_LINE_WIDTH)
}
pub fn render_width(&self, width: usize) -> String {
let mut out = String::new();
for line in &self.preamble {
out.push_str(line);
out.push('\n');
}
if let Some(doc) = &self.doc {
out.push_str("/-- ");
out.push_str(&escape_doc_comment(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));
if self.body.is_empty() {
out.push_str(" skip\n");
} else {
out.push_str(&self.body.render_width(" ", width));
}
out
}
}
fn escape_doc_comment(doc: &str) -> String {
doc.replace("-/", "-\\/").replace("/-", "/\\-")
}
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 apply_packs_atoms_greedily_and_is_stable_under_wrapping() {
let fixed = [
"(20 * (j : ℝ) + 10)",
"ρ",
"r",
"t",
"u",
"T₅",
"hD",
"hρ",
"hx",
];
let args: Vec<String> = fixed
.iter()
.map(|s| s.to_string())
.chain(std::iter::repeat_n("?_".to_string(), 28))
.collect();
let tactic = Tactic::apply("refine leafG346_single_poly", args.clone());
assert!(tactic.introduced_names().is_empty());
let text = Block::new(vec![tactic.clone()]).render(" ");
let lines: Vec<&str> = text.lines().collect();
assert!(
lines[0].starts_with(
" refine leafG346_single_poly (20 * (j : ℝ) + 10) ρ r t u T₅ hD hρ hx ?_"
),
"{text}"
);
assert!(lines.len() >= 2, "{text}");
for l in &lines[1..] {
assert!(l.starts_with(" ?_") && !l.starts_with(" "), "{l:?}");
}
assert!(lines.iter().all(|l| l.chars().count() <= 100), "{text}");
for l in &lines[..lines.len() - 1] {
assert!(l.chars().count() + 3 > 100, "not greedy: {l:?}");
}
let tokens: Vec<&str> = text.split_whitespace().collect();
assert_eq!(tokens.iter().filter(|t| **t == "?_").count(), 28);
let mut expected = vec!["refine", "leafG346_single_poly"];
expected.extend(args.iter().flat_map(|a| a.split_whitespace()));
assert_eq!(tokens, expected);
assert_eq!(wrap_lean(&text, MATHLIB_LINE_WIDTH), text, "idempotent");
let bulleted = Block::new(vec![Tactic::bullet(Block::new(vec![tactic]))]).render(" ");
let bl: Vec<&str> = bulleted.lines().collect();
assert!(
bl[0].starts_with(" · refine leafG346_single_poly"),
"{bulleted}"
);
assert!(
bl[1].starts_with(" ?_") && !bl[1].starts_with(" "),
"{bulleted}"
);
assert!(bl.iter().all(|l| l.chars().count() <= 100), "{bulleted}");
assert_eq!(wrap_lean(&bulleted, MATHLIB_LINE_WIDTH), bulleted);
let narrow = Block::new(vec![Tactic::apply(
"refine long_lemma_name",
vec!["(a + b)".into(), "?_".into()],
)])
.render_width("", 24);
assert_eq!(narrow, "refine long_lemma_name\n (a + b) ?_\n");
assert_eq!(
Block::new(vec![Tactic::apply("exact h", vec![])]).render(" "),
" exact h\n"
);
}
#[test]
fn decl_builders_and_preamble() {
let d = Decl::new(
DeclKind::Theorem,
"t",
"0 ≤ x",
Block::new(vec![Tactic::raw("exact hx")]),
);
assert_eq!(d.render(), "theorem t :\n 0 ≤ x := by\n exact hx\n");
let d = d
.with_binders(vec!["(x : ℝ)".into(), "(hx : 0 ≤ x)".into()])
.with_doc("Trivial.")
.with_preamble(vec![
"set_option maxHeartbeats 400000 in".into(),
"-- a comment longer than any width would allow if it were wrapped, which it is not".into(),
]);
let text = d.render_width(40);
assert!(text.starts_with(
"set_option maxHeartbeats 400000 in\n-- a comment longer than any width would allow if it were wrapped, which it is not\n/-- Trivial. -/\ntheorem t (x : ℝ) (hx : 0 ≤ x) :\n"
), "{text}");
assert!(text.ends_with(" 0 ≤ x := by\n exact hx\n"), "{text}");
let lit = Decl {
kind: DeclKind::Example,
name: String::new(),
binders: vec![],
statement: "True".into(),
body: Block::new(vec![Tactic::raw("trivial")]),
doc: None,
preamble: vec![],
};
assert_eq!(lit.render(), "example :\n True := by\n trivial\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"
);
}
}