Skip to main content

meta_language/translation/
emit_lean.rs

1//! The Lean 4 emitter.
2//!
3//! Naturals are `Nat`, integers `Int`; machine integers are represented by
4//! `Nat`/`Int` with explicit range checks that `panic!` where Rust would
5//! panic. Recursion is structural and annotated as such, so the Lean kernel
6//! checks termination. Theorems are reconstructed from portable proof plans
7//! with Lean tactics and re-checked by the Lean kernel. A JavaScript Number
8//! is a `Float`; the kernel cannot compute with `Float`, so an assertion about
9//! Numbers is checked when `main` runs, as in the source.
10//!
11//! Mirrors `js/src/translation/emit-lean.js`.
12
13use std::collections::{HashMap, HashSet};
14use std::fmt::Write as _;
15
16use super::Language;
17use super::diagnostics::{Result, type_error, unsupported};
18use super::emit_common::{
19    CtorStyle, Dependence, EmitOptions, EmitState, Emitted, mutual_groups, order_declarations,
20    unthreaded,
21};
22use super::ir::{
23    ByZero, Case, DataDecl, Decl, Expr, FnDecl, Hints, LitValue, Node, Pattern, Plan, Program,
24    Prop, Semantics, TheoremDecl, rename_function, rename_theorem,
25};
26use super::lean_root_names::LEAN_ROOT_NAMES;
27use super::output::thread_output;
28use super::proof::prop_functions;
29use super::surface::{BinaryOp, Flavor, Rounding, UnaryOp};
30use super::types::Type;
31
32mod arrays;
33mod helpers;
34mod main;
35mod math;
36use self::helpers::HELPERS;
37
38const KEYWORDS: &[&str] = &[
39    "abbrev",
40    "at",
41    "attribute",
42    "axiom",
43    "by",
44    "calc",
45    "class",
46    "def",
47    "deriving",
48    "do",
49    "else",
50    "end",
51    "example",
52    "export",
53    "extends",
54    "for",
55    "from",
56    "fun",
57    "have",
58    "if",
59    "import",
60    "in",
61    "inductive",
62    "instance",
63    "let",
64    "local",
65    "match",
66    "mut",
67    "mutual",
68    "namespace",
69    "noncomputable",
70    "open",
71    "partial",
72    "private",
73    "protected",
74    "return",
75    "section",
76    "set_option",
77    "show",
78    "structure",
79    "suffices",
80    "then",
81    "theorem",
82    "lemma",
83    "unsafe",
84    "universe",
85    "variable",
86    "where",
87    "with",
88    "termination_by",
89    "decreasing_by",
90    "macro",
91    "syntax",
92    "notation",
93    "infix",
94    "infixl",
95    "infixr",
96    "prefix",
97    "postfix",
98    "macro_rules",
99    "elab",
100    "opaque",
101    "omit",
102    "include",
103    "nomatch",
104    "nofun",
105    "try",
106    "catch",
107    "finally",
108    "unless",
109    "break",
110    "continue",
111    "Type",
112    "Prop",
113    "Sort",
114    "this",
115    "using",
116    "obtain",
117    // Names the emitted code relies on, and names Lean derives inside a type's namespace.
118    "Nat",
119    "Int",
120    "String",
121    "Bool",
122    "Unit",
123    "IO",
124    "true",
125    "false",
126    "main",
127    "toString",
128    "decide",
129    "panic",
130    "Float",
131    "rec",
132    "recOn",
133    "casesOn",
134    "noConfusion",
135    "noConfusionType",
136    "below",
137    "brecOn",
138    "binductionOn",
139    "ibelow",
140    "ctorIdx",
141    "toCtorIdx",
142    "sizeOf_spec",
143    "injEq",
144    "inj",
145    "induct",
146    "eq_def",
147    "mk",
148];
149
150/// A legal Lean identifier for a source name.
151fn ident(name: &str) -> String {
152    let mut result: String = name
153        .chars()
154        .map(|character| {
155            if character.is_ascii_alphanumeric()
156                || character == '_'
157                || character == '\''
158                || ('\u{C0}'..='\u{FFFF}').contains(&character)
159            {
160                character
161            } else {
162                '_'
163            }
164        })
165        .collect();
166    if result.starts_with(|character: char| character.is_ascii_digit() || character == '\'') {
167        result = format!("x{result}");
168    }
169    if KEYWORDS.contains(&result.as_str()) {
170        result = format!("«{result}»");
171    }
172    result
173}
174
175/// Lean derives the equation lemma `f.eq_1` for every definition `f`.
176fn generated(name: &str) -> Vec<String> {
177    vec![format!("{name}.eq_1")]
178}
179
180/// Emits a checked program as Lean.
181///
182/// # Errors
183/// On constructs the target cannot express faithfully.
184pub fn emit_lean(source: &Program) -> Result<Emitted> {
185    let program = &*thread_output(source)?;
186    let state = EmitState::new(
187        program,
188        Language::Lean,
189        ident,
190        KEYWORDS,
191        EmitOptions {
192            ctor_style: CtorStyle::Data,
193            generated: Some(generated),
194            root_reserved: LEAN_ROOT_NAMES,
195            ..EmitOptions::default()
196        },
197    );
198    LeanEmitter {
199        program,
200        state,
201        helpers: HashSet::new(),
202        successors: HashMap::new(),
203        uses_number: Dependence::numbers(program),
204        uses_partial: Dependence::partial(program),
205    }
206    .file()
207}
208
209struct LeanEmitter<'p> {
210    program: &'p Program,
211    state: EmitState<'p>,
212    helpers: HashSet<&'static str>,
213    // Inside `| p + 1 =>` of a match on `x`, `x` is written `p + 1`: Lean's
214    // structural recursion sees through the pattern but not the variable.
215    successors: HashMap<String, String>,
216    uses_number: Dependence,
217    uses_partial: Dependence,
218}
219
220impl LeanEmitter<'_> {
221    fn file(mut self) -> Result<Emitted> {
222        let mut blocks = Vec::new();
223        let mut open: Vec<String> = Vec::new();
224        let groups = mutual_groups(self.program);
225        for entry in order_declarations(self.program, false)? {
226            self.move_to(&mut open, entry.module_path(), &mut blocks);
227            let block = match (entry, groups.get(entry.full_name())) {
228                (_, Some(group)) if std::ptr::eq(group[0], entry) => self.mutual(group)?,
229                (_, Some(_)) => continue,
230                (Decl::Data(data), None) => self.data(entry, data)?,
231                (Decl::Fn(function), None) => self.function(entry, function)?,
232                (Decl::Theorem(theorem), None) => self.theorem(entry, theorem)?,
233            };
234            blocks.push(block);
235        }
236        self.move_to(&mut open, &[], &mut blocks);
237        let main = match &self.program.main {
238            Some(main) => Some(self.main(main)?),
239            None => None,
240        };
241        let mut lines = vec![
242            format!(
243                "-- Translated from {} by meta-language: portable core, Lean target.",
244                self.program.source_language.as_str()
245            ),
246            "-- Source binders are kept even where unused or named like a constructor, and proof hints are shared by every closing tactic.".to_owned(),
247            "set_option linter.unusedVariables false".to_owned(),
248            "set_option linter.unusedSimpArgs false".to_owned(),
249            "set_option linter.constructorNameAsVariable false".to_owned(),
250            String::new(),
251        ];
252        for (name, text) in HELPERS {
253            if self.helpers.contains(name) {
254                lines.push(text.to_owned());
255                lines.push(String::new());
256            }
257        }
258        for block in blocks {
259            lines.push(block);
260            lines.push(String::new());
261        }
262        let entry = main.map(|main| {
263            lines.push(main);
264            lines.push(String::new());
265            "main".to_owned()
266        });
267        Ok(self.state.finish(lines.join("\n"), entry))
268    }
269
270    /// Closes and opens namespaces so that `path` is the open one.
271    fn move_to(&self, open: &mut Vec<String>, path: &[String], blocks: &mut Vec<String>) {
272        let common = open
273            .iter()
274            .zip(path)
275            .take_while(|(left, right)| left == right)
276            .count();
277        while open.len() > common {
278            blocks.push(format!("end {}", self.module_name(open)));
279            open.pop();
280        }
281        while open.len() < path.len() {
282            open.push(path[open.len()].clone());
283            blocks.push(format!("namespace {}", self.module_name(open)));
284        }
285    }
286
287    fn module_name(&self, path: &[String]) -> String {
288        self.state.module_name(path).unwrap_or_default().to_owned()
289    }
290
291    fn ty(&mut self, ty: &Type) -> Result<String> {
292        Ok(match ty {
293            Type::Nat => "Nat".to_owned(),
294            Type::Int => "Int".to_owned(),
295            Type::Fixed { signed, .. } => {
296                let represented = if *signed { "Int" } else { "Nat" };
297                self.state.machine_integer(ty, represented);
298                represented.to_owned()
299            }
300            Type::Float => {
301                self.state.encode(
302                    "floats",
303                    "a JavaScript Number is a Lean Float, the same IEEE-754 binary64 with the same arithmetic; % is ml_float_rem, the exact truncated remainder, and ml_js_number prints a value as JavaScript does",
304                );
305                "Float".to_owned()
306            }
307            Type::Bool => "Bool".to_owned(),
308            Type::String => "String".to_owned(),
309            Type::Unit => "Unit".to_owned(),
310            Type::Output => "List String".to_owned(),
311            Type::Data { name } => self.state.reference(name, "."),
312            Type::Array { element } => {
313                self.state.encode(
314                    "arrays",
315                    "a JavaScript array, which the portable core never mutates, is a Lean Array; a read outside it panics",
316                );
317                format!("(Array {})", self.ty(element)?)
318            }
319            other => {
320                return Err(type_error(
321                    format!("no Lean type for {}", other.kind()),
322                    None,
323                ));
324            }
325        })
326    }
327
328    fn data(&mut self, entry: &Decl, data: &DataDecl) -> Result<String> {
329        let name = self.state.local_name(&data.full_name).to_owned();
330        self.state.map(entry, &name);
331        let mut lines = vec![format!("inductive {name} where")];
332        for ctor in &data.ctors {
333            let mut fields = String::new();
334            for field in &ctor.fields {
335                let ty = self.ty(&field.ty)?;
336                let _ = write!(fields, " ({} : {ty})", ident(&field.name));
337            }
338            let local = self.state.ctor_local(&data.full_name, &ctor.name);
339            lines.push(format!("  | {local}{fields} : {name}"));
340        }
341        // A pair of printed lines and a value is only built and taken apart; its value may be a Float, which has no DecidableEq.
342        // Lean derives no DecidableEq for a Float, nor through an Array of data; the portable core never compares such values.
343        let deriving = if data.output {
344            "Inhabited"
345        } else if self.undecidable(&data.full_name, &mut HashSet::new()) {
346            "Repr, Inhabited"
347        } else {
348            "Repr, DecidableEq, Inhabited"
349        };
350        lines.push(format!("  deriving {deriving}"));
351        Ok(lines.join("\n"))
352    }
353
354    fn definition(&mut self, entry: &Decl, function: &FnDecl) -> Result<String> {
355        let (params, body) = rename_function(function, &ident, &self.state.local_reserved());
356        let name = self.state.local_name(&function.full_name).to_owned();
357        self.state.map(entry, &name);
358        let mut binders = String::new();
359        for param in &params {
360            let ty = self.ty(&param.ty)?;
361            let _ = write!(binders, " ({} : {ty})", param.name);
362        }
363        let ret = self.ty(&function.ret)?;
364        let body = self.expr(&body, 1)?;
365        Ok(format!("def {name}{binders} : {ret} :=\n  {body}"))
366    }
367
368    fn function(&mut self, entry: &Decl, function: &FnDecl) -> Result<String> {
369        let text = self.definition(entry, function)?;
370        if !function.recursive {
371            return Ok(text);
372        }
373        match function.decreasing {
374            None => {
375                self.state.encode(
376                    "general-recursion",
377                    "recursion without a structurally decreasing argument is a Lean partial def: it runs as the source does but its equations are opaque to proofs",
378                );
379                Ok(format!("partial {text}"))
380            }
381            // Lean tries structural recursion on each argument before
382            // well-founded recursion, so the structurally decreasing argument
383            // needs no annotation.
384            Some(_) => Ok(text),
385        }
386    }
387
388    /// Mutually recursive functions are one `mutual` block of partial defs.
389    fn mutual(&mut self, group: &[&Decl]) -> Result<String> {
390        self.state.encode(
391            "mutual-recursion",
392            "mutually recursive functions are a Lean mutual block of partial defs: they run as the source does but their equations are opaque to proofs",
393        );
394        let mut blocks = vec!["mutual".to_owned()];
395        for entry in group {
396            if let Decl::Fn(function) = entry {
397                blocks.push(format!("partial {}", self.definition(entry, function)?));
398            }
399        }
400        blocks.push("end".to_owned());
401        Ok(blocks.join("\n\n"))
402    }
403
404    fn theorem(&mut self, entry: &Decl, theorem: &TheoremDecl) -> Result<String> {
405        let (binders, prop, plan) = rename_theorem(theorem, &ident, &self.state.local_reserved());
406        let name = self.state.local_name(&theorem.full_name).to_owned();
407        self.state.map(entry, &name);
408        let mut functions = prop_functions(&prop, Vec::new());
409        for unfold in collect_unfold(&plan) {
410            if !functions.contains(unfold) {
411                functions.push(unfold.clone());
412            }
413        }
414        let mut statement = format!("theorem {name}");
415        for binder in &binders {
416            let ty = self.ty(&binder.ty)?;
417            let _ = write!(statement, " ({} : {ty})", binder.name);
418        }
419        let _ = write!(statement, " : {} := by", self.prop(&prop)?);
420        self.state
421            .theorem(&theorem.full_name, &name, binders.is_empty(), false);
422        Ok(format!("{statement}\n{}", self.plan(&plan, &functions, 1)))
423    }
424
425    fn plan(&self, plan: &Plan, functions: &[String], depth: usize) -> String {
426        let pad = pad(depth);
427        let (tactic, split) = match plan {
428            Plan::Close { hints } => {
429                return format!("{pad}{}", self.closer(hints, functions, depth));
430            }
431            Plan::Induction(split) => ("induction", split),
432            Plan::Cases(split) => ("cases", split),
433        };
434        // `case tag names =>` names each constructor's fields, then its induction hypotheses.
435        let mut lines = vec![format!("{pad}{tactic} {}", split.variable)];
436        for kase in &split.cases {
437            let ctor = match &split.ty {
438                Type::Nat => kase.ctor.as_str(),
439                other => self
440                    .state
441                    .ctor_local(other.data_name().unwrap_or_default(), &kase.ctor),
442            };
443            let ihs: &[String] = if tactic == "induction" {
444                &kase.ihs
445            } else {
446                &[]
447            };
448            let names: String =
449                kase.fields
450                    .iter()
451                    .chain(ihs)
452                    .fold(String::new(), |mut names, item| {
453                        let _ = write!(names, " {item}");
454                        names
455                    });
456            lines.push(format!("{pad}case {ctor}{names} =>"));
457            lines.push(self.plan(&kase.plan, functions, depth + 1));
458        }
459        lines.join("\n")
460    }
461
462    fn closer(&self, hints: &Hints, functions: &[String], depth: usize) -> String {
463        let mut unfold: Vec<&String> = Vec::new();
464        for function in functions.iter().chain(&hints.unfold) {
465            if !unfold.contains(&function) {
466                unfold.push(function);
467            }
468        }
469        let mut rules: Vec<String> = unfold
470            .into_iter()
471            .map(|function| self.state.reference(function, "."))
472            .collect();
473        rules.extend(
474            hints
475                .lemmas
476                .iter()
477                .map(|lemma| self.state.reference(lemma, ".")),
478        );
479        rules.extend(hints.hyps.iter().cloned());
480        let ring = ["Nat.mul_add", "Nat.add_mul", "Int.mul_add", "Int.add_mul"];
481        let list = rules.join(", ");
482        let mut alternatives = vec!["rfl".to_owned(), "decide".to_owned()];
483        if rules.is_empty() {
484            alternatives.push("omega".to_owned());
485            alternatives.push(format!("simp_all [{}] <;> omega", ring.join(", ")));
486        } else {
487            let with_ring: Vec<&str> = rules.iter().map(String::as_str).chain(ring).collect();
488            alternatives.extend([
489                format!("simp only [{list}] <;> done"),
490                format!("simp_all only [{list}] <;> omega"),
491                format!("simp [{list}] <;> done"),
492                format!("simp_all [{list}] <;> omega"),
493                format!("simp_all [{}] <;> omega", with_ring.join(", ")),
494            ]);
495        }
496        // Each `try` closes the goal or leaves it untouched, and once one
497        // closes it the rest find no goal, so the lines act as ordered
498        // alternatives.
499        let alternatives: Vec<String> = alternatives
500            .iter()
501            .map(|alternative| format!("try {alternative}"))
502            .collect();
503        alternatives.join(&format!("\n{}", pad(depth)))
504    }
505
506    fn prop(&mut self, prop: &Prop) -> Result<String> {
507        Ok(match prop {
508            Prop::Forall { binders, body } => {
509                let mut parts = Vec::new();
510                for binder in binders {
511                    parts.push(format!("({} : {})", binder.name, self.ty(&binder.ty)?));
512                }
513                format!("(∀ {}, {})", parts.join(" "), self.prop(body)?)
514            }
515            Prop::And { left, right } => {
516                let left = self.prop(left)?;
517                format!("({left} ∧ {})", self.prop(right)?)
518            }
519            Prop::Or { left, right } => {
520                let left = self.prop(left)?;
521                format!("({left} ∨ {})", self.prop(right)?)
522            }
523            Prop::Implies { left, right } => {
524                let left = self.prop(left)?;
525                format!("({left} → {})", self.prop(right)?)
526            }
527            Prop::Not { arg } => format!("(¬ {})", self.prop(arg)?),
528            Prop::Bool { expr } => format!("({} = true)", self.expr(expr, 0)?),
529            other => {
530                let (op, comparison) = other.comparison().expect("comparison");
531                let operator = match op {
532                    BinaryOp::Eq => "=",
533                    BinaryOp::Ne => "≠",
534                    BinaryOp::Lt => "<",
535                    BinaryOp::Le => "≤",
536                    BinaryOp::Gt => ">",
537                    _ => "≥",
538                };
539                let left = self.expr(&comparison.left, 0)?;
540                format!("({left} {operator} {})", self.expr(&comparison.right, 0)?)
541            }
542        })
543    }
544
545    fn expr(&mut self, e: &Expr, depth: usize) -> Result<String> {
546        Ok(match &e.node {
547            Node::Lit { value } => {
548                if e.ty.is_float() {
549                    self.ty(&e.ty)?;
550                }
551                literal(&e.ty, value)?
552            }
553            Node::Unit => "()".to_owned(),
554            Node::OutNil => "([] : List String)".to_owned(),
555            Node::OutCons { head, tail } => {
556                format!(
557                    "({} :: {})",
558                    self.expr(head, depth)?,
559                    self.expr(tail, depth)?
560                )
561            }
562            Node::Var { name } => self.successors.get(name).unwrap_or(name).clone(),
563            Node::Call { func, args } => {
564                let head = self.state.reference(func, ".");
565                self.application(head, args, depth)?
566            }
567            Node::Ctor { data, ctor, args } => {
568                let head = self.state.ctor_ref(data, ctor, ".");
569                self.application(head, args, depth)?
570            }
571            Node::Unary { op, arg, semantics } => {
572                if *op == UnaryOp::Not {
573                    format!("(!{})", self.expr(arg, depth)?)
574                } else {
575                    if *semantics == Some(Semantics::Checked) {
576                        unthreaded("a machine-integer negation");
577                    }
578                    format!("(-{})", self.expr(arg, depth)?)
579                }
580            }
581            Node::Binary { .. } => self.binary(e, depth)?,
582            Node::If {
583                cond,
584                then,
585                otherwise,
586            } => {
587                let cond = self.expr(cond, depth)?;
588                let then = self.expr(then, depth)?;
589                format!(
590                    "(if {cond} then {then} else {})",
591                    self.expr(otherwise, depth)?
592                )
593            }
594            Node::Let { name, value, body } => {
595                let value = self.expr(value, depth)?;
596                format!(
597                    "(let {name} := {value};\n{}{})",
598                    pad(depth + 1),
599                    self.expr(body, depth + 1)?
600                )
601            }
602            Node::Match { scrutinee, cases } => self.match_expr(scrutinee, cases, depth)?,
603            Node::ToString { arg, console } => {
604                if arg.ty == Type::String {
605                    self.expr(arg, depth)?
606                } else {
607                    self.text_of(arg, depth, *console)?
608                }
609            }
610            Node::Cast {
611                arg,
612                from,
613                to,
614                flavor,
615                ..
616            } => self.cast(arg, from, to, *flavor, depth)?,
617            Node::Abort { .. } => unthreaded("an abort"),
618            Node::Array { .. } | Node::Append { .. } | Node::Index { .. } | Node::Length { .. } => {
619                self.array_expr(e, depth)?
620            }
621            Node::Math { .. } => self.math(e, depth)?,
622            // Output threading leaves no `print` node.
623            Node::Print { .. } => {
624                return Err(type_error(
625                    "print with its output not threaded".to_owned(),
626                    e.span,
627                ));
628            }
629        })
630    }
631
632    /// A call or constructor application: the bare head without arguments.
633    fn application(&mut self, head: String, args: &[Expr], depth: usize) -> Result<String> {
634        if args.is_empty() {
635            return Ok(head);
636        }
637        let mut parts = vec![head];
638        for arg in args {
639            parts.push(self.expr(arg, depth)?);
640        }
641        Ok(format!("({})", parts.join(" ")))
642    }
643
644    fn text_of(&mut self, arg: &Expr, depth: usize, console: bool) -> Result<String> {
645        if matches!(arg.ty, Type::Data { .. } | Type::Unit) {
646            return Err(unsupported(
647                "output of structured values",
648                &format!("a {} value has no portable textual form", arg.ty.kind()),
649                arg.span,
650            ));
651        }
652        if arg.ty.is_float() {
653            self.helpers.insert("jsNumber");
654            if console {
655                self.helpers.insert("jsConsole");
656            }
657            let function = if console {
658                "ml_js_console"
659            } else {
660                "ml_js_number"
661            };
662            return Ok(format!("({function} {})", self.expr(arg, depth)?));
663        }
664        Ok(format!("(toString {})", self.expr(arg, depth)?))
665    }
666
667    fn binary(&mut self, e: &Expr, depth: usize) -> Result<String> {
668        let Node::Binary {
669            op, left, right, ..
670        } = &e.node
671        else {
672            unreachable!("binary expressions only");
673        };
674        let left_text = self.expr(left, depth)?;
675        let right_text = self.expr(right, depth)?;
676        let comparison = match op {
677            BinaryOp::And => return Ok(format!("({left_text} && {right_text})")),
678            BinaryOp::Or => return Ok(format!("({left_text} || {right_text})")),
679            BinaryOp::Concat => return Ok(format!("({left_text} ++ {right_text})")),
680            BinaryOp::Eq => return Ok(format!("({left_text} == {right_text})")),
681            BinaryOp::Ne => return Ok(format!("({left_text} != {right_text})")),
682            BinaryOp::Lt => "<",
683            BinaryOp::Le => "≤",
684            BinaryOp::Gt => ">",
685            BinaryOp::Ge => "≥",
686            _ => return self.arithmetic(e, &left_text, &right_text),
687        };
688        Ok(format!("(decide ({left_text} {comparison} {right_text}))"))
689    }
690
691    fn arithmetic(&mut self, e: &Expr, left_text: &str, right_text: &str) -> Result<String> {
692        let Node::Binary {
693            op,
694            domain,
695            rounding,
696            by_zero,
697            semantics,
698            ..
699        } = &e.node
700        else {
701            unreachable!("binary expressions only");
702        };
703        if *semantics == Some(Semantics::Ieee) {
704            let operator = match op {
705                BinaryOp::Add => "+",
706                BinaryOp::Sub => "-",
707                BinaryOp::Mul => "*",
708                BinaryOp::Div => "/",
709                _ => {
710                    self.helpers.insert("floatRem");
711                    return Ok(format!("(ml_float_rem {left_text} {right_text})"));
712                }
713            };
714            return Ok(format!("({left_text} {operator} {right_text})"));
715        }
716        // A machine-integer operation, one that may abort, reaches here as its abort test and its value computed on the representation (output.rs).
717        if *semantics == Some(Semantics::Checked)
718            || *by_zero == Some(ByZero::Abort)
719            || matches!(e.ty, Type::Fixed { .. })
720        {
721            unthreaded(&format!("a machine-integer {}", op.name()));
722        }
723        match op {
724            BinaryOp::Add => Ok(format!("({left_text} + {right_text})")),
725            BinaryOp::Mul => Ok(format!("({left_text} * {right_text})")),
726            BinaryOp::Sub => Ok(format!("({left_text} - {right_text})")),
727            BinaryOp::Div | BinaryOp::Rem => {
728                let division = *op == BinaryOp::Div;
729                if matches!(domain, Some(Type::Nat)) {
730                    return Ok(format!(
731                        "({left_text} {} {right_text})",
732                        if division { "/" } else { "%" }
733                    ));
734                }
735                if *rounding == Some(Rounding::Trunc) {
736                    return Ok(format!(
737                        "(Int.{} {left_text} {right_text})",
738                        if division { "tdiv" } else { "tmod" }
739                    ));
740                }
741                if *rounding == Some(Rounding::Floor) {
742                    return Ok(format!(
743                        "(Int.{} {left_text} {right_text})",
744                        if division { "fdiv" } else { "fmod" }
745                    ));
746                }
747                Ok(format!(
748                    "({left_text} {} {right_text})",
749                    if division { "/" } else { "%" }
750                ))
751            }
752            other => Err(type_error(
753                format!("no Lean operator {}", operator_name(*other)),
754                None,
755            )),
756        }
757    }
758
759    fn match_expr(&mut self, scrutinee: &Expr, cases: &[Case], depth: usize) -> Result<String> {
760        let subject = self.expr(scrutinee, depth)?;
761        let mut arms = Vec::new();
762        for kase in cases {
763            let text = match &kase.pattern {
764                Pattern::NatZero => "0".to_owned(),
765                Pattern::NatSucc { name } => format!("{name} + 1"),
766                Pattern::Wild => "_".to_owned(),
767                Pattern::Bind { name } => name.clone(),
768                Pattern::Ctor { data, ctor, binds } => {
769                    let mut parts = vec![self.state.ctor_ref(data, ctor, ".")];
770                    parts.extend(
771                        binds
772                            .iter()
773                            .map(|bind| bind.clone().unwrap_or_else(|| "_".to_owned())),
774                    );
775                    parts.join(" ")
776                }
777            };
778            let saved = self.successors.clone();
779            if let (Pattern::NatSucc { name }, Some(variable)) =
780                (&kase.pattern, scrutinee.var_name())
781            {
782                self.successors
783                    .insert(variable.to_owned(), format!("({name} + 1)"));
784            }
785            let body = self.expr(&kase.body, depth + 2);
786            self.successors = saved;
787            arms.push(format!("{}| {text} => {}", pad(depth + 1), body?));
788        }
789        Ok(format!("(match {subject} with\n{})", arms.join("\n")))
790    }
791
792    fn cast(
793        &mut self,
794        arg: &Expr,
795        from: &Type,
796        to: &Type,
797        flavor: Flavor,
798        depth: usize,
799    ) -> Result<String> {
800        let arg = self.expr(arg, depth)?;
801        let natural_from = from.is_natural();
802        let natural_to = to.is_natural();
803        if natural_from == natural_to {
804            return Ok(arg);
805        }
806        if natural_from {
807            return Ok(format!("(Int.ofNat {arg})"));
808        }
809        if flavor == Flavor::Checked {
810            unthreaded("a checked conversion to a natural");
811        }
812        Ok(format!("(Int.toNat {arg})"))
813    }
814}
815
816fn literal(ty: &Type, value: &LitValue) -> Result<String> {
817    Ok(match ty {
818        Type::Float => lean_float(&value.text()),
819        Type::Nat => format!("({} : Nat)", value.text()),
820        Type::Int => format!("({} : Int)", value.text()),
821        Type::Fixed { signed, .. } => {
822            format!(
823                "({} : {})",
824                value.text(),
825                if *signed { "Int" } else { "Nat" }
826            )
827        }
828        Type::Bool => value.text(),
829        Type::String => json_string(&value.text()),
830        other => {
831            return Err(type_error(
832                format!("no Lean literal for {}", other.kind()),
833                None,
834            ));
835        }
836    })
837}
838
839/// A Number's canonical JavaScript text as a Float term; Lean reads decimal
840/// literals correctly rounded.
841fn lean_float(value: &str) -> String {
842    let text = match value {
843        "NaN" => "0.0 / 0.0".to_owned(),
844        "Infinity" => "1.0 / 0.0".to_owned(),
845        "-Infinity" => "-1.0 / 0.0".to_owned(),
846        _ => value.replacen("e+", "e", 1),
847    };
848    format!("({text} : Float)")
849}
850
851/// A string literal as JavaScript's `JSON.stringify` writes it.
852fn json_string(text: &str) -> String {
853    serde_json::to_string(text).unwrap_or_default()
854}
855
856/// The operator's name in the checked program's JSON form.
857fn operator_name(op: BinaryOp) -> String {
858    serde_json::to_value(op)
859        .ok()
860        .and_then(|value| value.as_str().map(str::to_owned))
861        .unwrap_or_default()
862}
863
864fn collect_unfold(plan: &Plan) -> Vec<&String> {
865    match plan {
866        Plan::Close { hints } => hints.unfold.iter().collect(),
867        Plan::Induction(split) | Plan::Cases(split) => split
868            .cases
869            .iter()
870            .flat_map(|kase| collect_unfold(&kase.plan))
871            .collect(),
872    }
873}
874
875fn pad(depth: usize) -> String {
876    "  ".repeat(depth)
877}