1use std::collections::HashMap;
23
24use crate::tactic::combinators as c;
25use crate::tactic::{ProofState, Tactic};
26use crate::verify::{citation_order, LibraryResult, LibraryTheorem};
27use crate::{ProofExpr, ProofTerm};
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct ScriptError(pub String);
32
33impl std::fmt::Display for ScriptError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "tactic script error: {}", self.0)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq)]
40enum Tok {
41 Word(String),
42 Semi,
43 ThenAll,
44 LParen,
45 RParen,
46 LBracket,
47 RBracket,
48 Bar,
49}
50
51fn lex(src: &str) -> Result<Vec<Tok>, ScriptError> {
52 let mut toks = Vec::new();
53 let bytes = src.as_bytes();
54 let mut i = 0;
55 while i < bytes.len() {
56 let ch = bytes[i] as char;
57 match ch {
58 c if c.is_whitespace() => i += 1,
59 ';' | '.' | ',' => {
62 toks.push(Tok::Semi);
63 i += 1;
64 }
65 '(' => {
66 toks.push(Tok::LParen);
67 i += 1;
68 }
69 ')' => {
70 toks.push(Tok::RParen);
71 i += 1;
72 }
73 '[' => {
74 toks.push(Tok::LBracket);
75 i += 1;
76 }
77 ']' => {
78 toks.push(Tok::RBracket);
79 i += 1;
80 }
81 '|' => {
82 toks.push(Tok::Bar);
83 i += 1;
84 }
85 '<' if src[i..].starts_with("<;>") => {
86 toks.push(Tok::ThenAll);
87 i += 3;
88 }
89 c if c.is_alphanumeric() || c == '_' => {
90 let start = i;
91 while i < bytes.len() {
92 let cc = bytes[i] as char;
93 if cc.is_alphanumeric() || cc == '_' {
94 i += 1;
95 } else {
96 break;
97 }
98 }
99 toks.push(Tok::Word(src[start..i].to_string()));
100 }
101 other => return Err(ScriptError(format!("unexpected character {other:?}"))),
102 }
103 }
104 Ok(toks)
105}
106
107#[derive(Clone, Default)]
114pub struct TacticEnv {
115 defs: std::collections::HashMap<String, String>,
116}
117
118impl TacticEnv {
119 pub fn new() -> Self {
120 Self::default()
121 }
122 pub fn define(&mut self, name: &str, script: &str) {
124 self.defs.insert(name.to_ascii_lowercase(), script.to_string());
125 }
126 fn get(&self, name: &str) -> Option<&String> {
127 self.defs.get(&name.to_ascii_lowercase())
128 }
129}
130
131const MAX_TACTIC_DEPTH: usize = 128;
134
135struct Parser {
136 toks: Vec<Tok>,
137 pos: usize,
138 env: TacticEnv,
139 depth: usize,
140}
141
142impl Parser {
143 fn peek(&self) -> Option<&Tok> {
144 self.toks.get(self.pos)
145 }
146 fn bump(&mut self) -> Option<Tok> {
147 let t = self.toks.get(self.pos).cloned();
148 if t.is_some() {
149 self.pos += 1;
150 }
151 t
152 }
153 fn expect(&mut self, t: &Tok) -> Result<(), ScriptError> {
154 match self.bump() {
155 Some(got) if &got == t => Ok(()),
156 other => Err(ScriptError(format!("expected {t:?}, found {other:?}"))),
157 }
158 }
159
160 fn at_separator(&self) -> bool {
163 matches!(self.peek(), Some(Tok::Semi))
164 || matches!(self.peek(), Some(Tok::Word(w)) if w.eq_ignore_ascii_case("then"))
165 }
166
167 fn skip_filler(&mut self) {
170 while matches!(self.peek(), Some(Tok::Word(w)) if is_filler(w)) {
171 self.bump();
172 }
173 }
174
175 fn parse_seq(&mut self) -> Result<Tactic, ScriptError> {
178 let mut steps = vec![self.parse_chain()?];
179 loop {
180 let mut consumed = false;
181 while self.at_separator() {
182 self.bump();
183 consumed = true;
184 }
185 if !consumed {
186 break;
187 }
188 if matches!(self.peek(), None | Some(Tok::RParen) | Some(Tok::RBracket) | Some(Tok::Bar)) {
190 break;
191 }
192 steps.push(self.parse_chain()?);
193 }
194 Ok(if steps.len() == 1 {
195 steps.into_iter().next().unwrap()
196 } else {
197 c::seq(steps)
198 })
199 }
200
201 fn parse_chain(&mut self) -> Result<Tactic, ScriptError> {
203 let mut acc = self.parse_atom()?;
204 while matches!(self.peek(), Some(Tok::ThenAll)) {
205 self.bump();
206 let rhs = self.parse_atom()?;
207 acc = c::then_all(acc, rhs);
208 }
209 Ok(acc)
210 }
211
212 fn parse_atom(&mut self) -> Result<Tactic, ScriptError> {
213 self.skip_filler();
214 match self.peek() {
215 Some(Tok::LParen) => {
216 self.bump();
217 let inner = self.parse_seq()?;
218 self.expect(&Tok::RParen)?;
219 Ok(inner)
220 }
221 Some(Tok::Word(w)) if w == "first" => {
222 self.bump();
223 self.expect(&Tok::LBracket)?;
224 let mut alts = vec![self.parse_seq()?];
225 while matches!(self.peek(), Some(Tok::Bar)) {
226 self.bump();
227 alts.push(self.parse_seq()?);
228 }
229 self.expect(&Tok::RBracket)?;
230 Ok(c::first(alts))
231 }
232 Some(Tok::Word(w)) if w == "try" => {
233 self.bump();
234 Ok(c::try_(self.parse_atom()?))
235 }
236 Some(Tok::Word(w)) if w == "repeat" => {
237 self.bump();
238 Ok(c::repeat(self.parse_atom()?))
239 }
240 Some(Tok::Word(_)) => self.parse_primitive(),
241 other => Err(ScriptError(format!("expected a tactic, found {other:?}"))),
242 }
243 }
244
245 fn parse_primitive(&mut self) -> Result<Tactic, ScriptError> {
246 let raw = match self.bump() {
247 Some(Tok::Word(w)) => w,
248 other => return Err(ScriptError(format!("expected a tactic name, found {other:?}"))),
249 };
250 if let Some(src) = self.env.get(&raw).cloned() {
254 if self.depth >= MAX_TACTIC_DEPTH {
255 return Err(ScriptError(format!(
256 "user tactic `{raw}` expands too deeply (recursive definition?)"
257 )));
258 }
259 return parse_with_env(&src, &self.env, self.depth + 1);
260 }
261 let name = canonical(&raw)
262 .ok_or_else(|| ScriptError(format!("unknown tactic `{raw}`")))?;
263 let mut arg = || -> Result<String, ScriptError> {
265 self.skip_filler();
266 match self.peek() {
267 Some(Tok::Word(w)) => {
268 let w = w.clone();
269 self.bump();
270 Ok(w)
271 }
272 _ => Err(ScriptError(format!("tactic `{name}` expects an argument"))),
273 }
274 };
275 match name {
276 "intro" => Ok(c::intro(&arg()?)),
277 "exact" => Ok(c::exact(&arg()?)),
278 "cases" => Ok(c::cases(&arg()?)),
279 "rewrite" => Ok(c::rewrite(&arg()?)),
280 "exists" => Ok(c::exists(ProofTerm::Constant(arg()?))),
281 "assumption" => Ok(c::assumption()),
282 "simp" => Ok(c::simp()),
283 "decide" => Ok(c::decide()),
284 "omega" => Ok(c::omega()),
285 "crush" => Ok(c::crush()),
286 "split" => Ok(c::split()),
287 "left" => Ok(c::left()),
288 "right" => Ok(c::right()),
289 "auto" => Ok(c::auto()),
290 "induction" => Ok(c::induction()),
291 _ => Err(ScriptError(format!("unknown tactic `{name}`"))),
292 }
293 }
294}
295
296fn is_filler(w: &str) -> bool {
299 matches!(
300 w.to_ascii_lowercase().as_str(),
301 "by" | "the" | "on" | "that" | "now" | "with" | "we" | "it" | "a" | "an" | "of" | "to" | "and"
302 )
303}
304
305fn canonical(word: &str) -> Option<&'static str> {
309 match word.to_ascii_lowercase().as_str() {
310 "intro" | "assume" | "suppose" | "let" | "introduce" | "given" | "fix" => Some("intro"),
311 "exact" | "from" | "because" => Some("exact"),
312 "cases" | "destruct" | "consider" | "casework" => Some("cases"),
313 "rewrite" | "rw" | "substitute" | "subst" => Some("rewrite"),
314 "exists" | "use" | "witness" | "choose" | "provide" => Some("exists"),
315 "assumption" | "done" | "trivial" | "established" | "hypothesis" => Some("assumption"),
316 "simp" | "simplify" | "normalize" | "simplification" => Some("simp"),
317 "decide" | "compute" | "evaluate" | "calculate" => Some("decide"),
318 "omega" | "arithmetic" | "linarith" | "integers" => Some("omega"),
319 "crush" | "grind" | "blast" => Some("crush"),
320 "split" | "constructor" | "both" | "conjunction" => Some("split"),
321 "left" => Some("left"),
322 "right" => Some("right"),
323 "auto" | "tauto" | "automatically" | "automation" | "trivially" | "directly" => Some("auto"),
324 "induction" | "induct" => Some("induction"),
325 _ => None,
326 }
327}
328
329pub fn parse_script(src: &str) -> Result<Tactic, ScriptError> {
331 parse_with_env(src, &TacticEnv::new(), 0)
332}
333
334pub fn parse_script_with_env(src: &str, env: &TacticEnv) -> Result<Tactic, ScriptError> {
336 parse_with_env(src, env, 0)
337}
338
339fn parse_with_env(src: &str, env: &TacticEnv, depth: usize) -> Result<Tactic, ScriptError> {
340 let toks = lex(src)?;
341 if toks.is_empty() {
342 return Err(ScriptError("empty script".to_string()));
343 }
344 let mut p = Parser { toks, pos: 0, env: env.clone(), depth };
345 let t = p.parse_seq()?;
346 if p.pos != p.toks.len() {
347 return Err(ScriptError(format!(
348 "trailing tokens after the script: {:?}",
349 &p.toks[p.pos..]
350 )));
351 }
352 Ok(t)
353}
354
355impl ProofState {
356 pub fn run_script(&mut self, src: &str) -> Result<&mut Self, ScriptError> {
359 let tactic = parse_script(src)?;
360 tactic(self).map_err(|e| ScriptError(format!("{e:?}")))?;
361 Ok(self)
362 }
363
364 pub fn run_script_with_env(
366 &mut self,
367 src: &str,
368 env: &TacticEnv,
369 ) -> Result<&mut Self, ScriptError> {
370 let tactic = parse_script_with_env(src, env)?;
371 tactic(self).map_err(|e| ScriptError(format!("{e:?}")))?;
372 Ok(self)
373 }
374}
375
376pub struct ScriptedTheorem {
386 pub name: String,
387 pub premises: Vec<ProofExpr>,
388 pub goal: ProofExpr,
389 pub script: String,
391 pub cites: Vec<String>,
393 pub simp: bool,
396}
397
398pub fn prove_scripted_library(
404 axioms: &[ProofExpr],
405 theorems: &[ScriptedTheorem],
406) -> Vec<LibraryResult> {
407 let stubs: Vec<LibraryTheorem> = theorems
409 .iter()
410 .map(|t| LibraryTheorem {
411 name: t.name.clone(),
412 premises: Vec::new(),
413 goal: t.goal.clone(),
414 cites: t.cites.clone(),
415 })
416 .collect();
417
418 let mut proved: HashMap<String, ProofExpr> = HashMap::new();
419 let mut by_name: HashMap<String, LibraryResult> = HashMap::new();
420 let mut simp_pool: Vec<ProofExpr> = Vec::new();
423
424 for &i in &citation_order(&stubs) {
425 let t = &theorems[i];
426 let mut premises = axioms.to_vec();
427 premises.extend(t.premises.iter().cloned());
428 for cite in &t.cites {
429 if let Some(goal) = proved.get(cite) {
430 premises.push(goal.clone());
431 }
432 }
433 for lemma in &simp_pool {
434 if !premises.contains(lemma) {
435 premises.push(lemma.clone());
436 }
437 }
438
439 let mut st = ProofState::start(premises, t.goal.clone());
440 let (verified, error) = match st.run_script(&t.script).err() {
441 Some(e) => (false, Some(e.0)),
442 None => match st.qed() {
443 Ok(vp) => (vp.verified, vp.verification_error),
444 Err(e) => (false, Some(format!("{e:?}"))),
445 },
446 };
447 if verified {
448 proved.insert(t.name.clone(), t.goal.clone());
449 if t.simp {
450 simp_pool.push(t.goal.clone());
451 }
452 }
453 by_name.insert(
454 t.name.clone(),
455 LibraryResult { name: t.name.clone(), verified, verification_error: error },
456 );
457 }
458
459 theorems
460 .iter()
461 .map(|t| by_name.remove(&t.name).expect("every theorem produces a result"))
462 .collect()
463}