Skip to main content

logicaffeine_kernel/interface/
printer.rs

1//! The kernel's surface, written back: the exact inverse of `term_parser` and `command_parser`.
2//!
3//! For every term and command the printer spells, parsing the text in a fresh context yields the
4//! same term (up to α) — `vernacular_roundtrip.rs` locks that. A term with no surface spelling is a
5//! [`PrintError`], never an approximation: the caller reports source export as unsupported.
6//!
7//! # What "the same term" means
8//!
9//! The text is read back exactly as the `Repl` reads it: the term door, then name resolution at
10//! the root scope against the environment the term lives in. That second stage is where a
11//! qualified reference `Nat.add` becomes the constant `Nat.add` rather than the field `add` of
12//! `Nat` — LOGOS's own staging, the one Lean has — so the printer writes qualified names as
13//! qualified names rather than escaping them.
14//!
15//! # Binders
16//!
17//! A kernel term names its binders, and a name that does not read back BARE as that binder is
18//! RENAMED rather than printed as is — α-renaming changes nothing the kernel can see. A binder is
19//! renamed when its name would capture a global the body mentions (a binder `x` over a body naming
20//! the global `x`, or a binder `Nat` over a body naming `Nat.add`), when the plain grammar reads it
21//! as something else (a keyword, a sort, `_` that is referred to), or when it could be written only
22//! in guillemets (the importer's `arena.local.3`) — a quoted binder name tells a reader nothing. A
23//! fresh name is fresh against EVERY name the term spells, so a renamed binder can neither capture
24//! nor be captured. Every other binder keeps its name. Globals are never renamed: a constant is
25//! spelled bare, qualified, or `«quoted»`, and always means itself.
26//!
27//! # Depth
28//!
29//! Imported terms are deep. Both passes here — the scan that learns which binders are referred
30//! to, and the printing itself — run on an explicit work stack, so printing a term never recurses
31//! once per level of it.
32
33use super::command::{Command, InductiveMember};
34use super::surface::{Assoc, Name, OpenClause, OperatorForm, SurfaceDecl};
35use super::term_parser::{is_reserved_word, is_sort_word, scan_name, TermParser, MAX_LEVEL_OFFSET};
36use crate::{Literal, Term, Universe};
37use std::collections::{HashMap, HashSet};
38
39/// A term or command the LOGOS surface cannot spell (yet).
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum PrintError {
42    NoSpelling { what: String },
43}
44
45impl std::fmt::Display for PrintError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            PrintError::NoSpelling { what } => {
49                write!(f, "the LOGOS surface has no spelling for {what}")
50            }
51        }
52    }
53}
54
55fn no_spelling(what: impl Into<String>) -> PrintError {
56    PrintError::NoSpelling { what: what.into() }
57}
58
59/// Spell `term` in the kernel term surface.
60pub fn print_term(term: &Term) -> Result<String, PrintError> {
61    let mut printer = Printer::new(&[term]);
62    printer.term(term, Ctx::Top)?;
63    Ok(printer.out)
64}
65
66/// Spell `term` read in a context whose locals are `locals`: each prints as its own name. A goal and
67/// its hypotheses are spelled this way for a reader; a closed term's round trip is [`print_term`]'s,
68/// which refuses a free name because the surface would read it as a global.
69pub fn print_term_in(term: &Term, locals: &[&str]) -> Result<String, PrintError> {
70    let mut printer = Printer::new(&[term]);
71    for local in locals {
72        printer
73            .scope
74            .entry(local.to_string())
75            .or_default()
76            .push(local.to_string());
77        printer.avoid.insert(local.to_string());
78    }
79    printer.term(term, Ctx::Top)?;
80    Ok(printer.out)
81}
82
83/// Spell one tactic of a script, its terms in the vernacular's own syntax, so the script reads back.
84fn print_tactic(step: &crate::tactics::TacticStep) -> Result<String, PrintError> {
85    use crate::tactics::TacticStep;
86    let pattern = |cases: &[Vec<String>]| {
87        if cases.is_empty() {
88            String::new()
89        } else {
90            let cases: Vec<String> = cases.iter().map(|c| c.join(" ")).collect();
91            format!(" as [{}]", cases.join(" | "))
92        }
93    };
94    Ok(match step {
95        TacticStep::Intro(names) => format!("intro {}", names.join(" ")),
96        TacticStep::Exact(t) => format!("exact {}", slot(print_term(t)?, &[";"])),
97        TacticStep::Apply(t) => format!("apply {}", slot(print_term(t)?, &[";"])),
98        TacticStep::Exists(t) => format!("exists {}", slot(print_term(t)?, &[";"])),
99        TacticStep::Change(t) => format!("change {}", slot(print_term(t)?, &[";"])),
100        TacticStep::Have(n, t) => format!("have {n} : {}", slot(print_term(t)?, &[";"])),
101        TacticStep::Rewrite(t, reverse) => {
102            format!(
103                "rw [{}{}]",
104                if *reverse { "<- " } else { "" },
105                slot(print_term(t)?, &[",", "]"])
106            )
107        }
108        TacticStep::Assumption => "assumption".to_string(),
109        TacticStep::Exfalso => "exfalso".to_string(),
110        TacticStep::Constructor => "constructor".to_string(),
111        TacticStep::Left => "left".to_string(),
112        TacticStep::Right => "right".to_string(),
113        TacticStep::Rfl => "rfl".to_string(),
114        TacticStep::Induction(x, cases) => format!("induction {x}{}", pattern(cases)),
115        TacticStep::Cases(x, cases) => format!("cases {x}{}", pattern(cases)),
116        TacticStep::Refine(t) => format!("refine {}", slot(print_term(t)?, &[";"])),
117        TacticStep::Subst(h) => format!("subst {h}"),
118        TacticStep::Injection(h, names) if names.is_empty() => format!("injection {h}"),
119        TacticStep::Injection(h, names) => format!("injection {h} as [{}]", names.join(" ")),
120        TacticStep::Calc(steps) => {
121            let steps = steps
122                .iter()
123                .map(|(r, p)| {
124                    Ok(format!(
125                        "{} := {}",
126                        slot(print_term(r)?, &[":=", ",", "]"]),
127                        slot(print_term(p)?, &[",", "]"])
128                    ))
129                })
130                .collect::<Result<Vec<_>, PrintError>>()?;
131            format!("calc [{}]", steps.join(", "))
132        }
133        TacticStep::Cc => "cc".to_string(),
134        TacticStep::Decide => "decide".to_string(),
135        TacticStep::Simp(lemmas) if lemmas.is_empty() => "simp".to_string(),
136        TacticStep::Simp(lemmas) => {
137            let lemmas = lemmas
138                .iter()
139                .map(|l| Ok(slot(print_term(l)?, &[",", "]"])))
140                .collect::<Result<Vec<_>, PrintError>>()?;
141            format!("simp [{}]", lemmas.join(", "))
142        }
143    })
144}
145
146/// Spell `command` as one vernacular command, terminated by its period.
147pub fn print_command(command: &Command) -> Result<String, PrintError> {
148    match command {
149        Command::Universes { params, inner } => declaration(inner, Some(params)),
150        Command::Definition { .. }
151        | Command::Theorem { .. }
152        | Command::TheoremBy { .. }
153        | Command::Opaque { .. }
154        | Command::Axiom { .. }
155        | Command::InductiveBlock { .. }
156        | Command::Quotient { .. } => declaration(command, None),
157        Command::Inductive {
158            name,
159            params,
160            sort,
161            constructors,
162        } => {
163            if *sort != Term::Sort(Universe::Type(0)) {
164                return Err(no_spelling(format!(
165                    "the undeclared-arity inductive `{name}` at `{sort}` \u{2014} that form is always at \
166                     `Type`; an inductive at another sort declares its arity, as a block"
167                )));
168            }
169            if constructors.is_empty() {
170                return Err(no_spelling(format!(
171                    "the undeclared-arity inductive `{name}` with no constructors \u{2014} that form \
172                     needs at least one; an empty inductive declares its arity, as a block"
173                )));
174            }
175            let mut out = format!("Inductive {}", declaration_name(name)?);
176            for (param, ty) in params {
177                out.push_str(&parameter(param, ty)?);
178            }
179            out.push_str(" := ");
180            out.push_str(&constructor_list(constructors)?);
181            out.push('.');
182            Ok(out)
183        }
184        Command::Check(t) => Ok(format!("Check {}.", plain_term(t, "`Check`")?)),
185        Command::Eval(t) => Ok(format!("Eval {}.", plain_term(t, "`Eval`")?)),
186        Command::Comment => Ok("/- -/".to_string()),
187        Command::Structure {
188            inductive,
189            projections,
190        } => {
191            let mut out = format!("Structure {} :=", global_name(inductive)?);
192            for projection in projections {
193                out.push(' ');
194                out.push_str(&global_name(projection)?);
195            }
196            out.push('.');
197            Ok(out)
198        }
199        Command::PrintAxioms(name) => Ok(format!("Print Axioms {}.", name_path(name)?)),
200        Command::Protected(inner) => match inner.as_ref() {
201            // The qualifier is read off the front of a Coq-style sentence, so it can only precede
202            // a command that dispatch reaches after it.
203            Command::Protected(_)
204            | Command::Open(_)
205            | Command::OpenIn { .. }
206            | Command::Comment => Err(no_spelling(
207                "`Protected` in front of a command the qualifier cannot precede",
208            )),
209            _ => Ok(format!("Protected {}", print_command(inner)?)),
210        },
211        Command::Export { namespace, names } => {
212            let names = names.iter().map(name_path).collect::<Result<Vec<_>, _>>()?;
213            if names.is_empty() {
214                return Err(no_spelling("an `Export` naming nothing"));
215            }
216            Ok(format!(
217                "Export {} ({}).",
218                name_path(namespace)?,
219                names.join(" ")
220            ))
221        }
222        Command::Open(clauses) => Ok(format!("Open {}.", open_clauses(clauses)?)),
223        Command::OpenIn { opens, inner } => Ok(format!(
224            "Open {} in {}",
225            open_clauses(opens)?,
226            print_command(inner)?
227        )),
228        Command::Surface(decl) => surface_declaration(decl),
229    }
230}
231
232/// The vernacular command that registers the inductive block `name` belongs to, EXACTLY as `ctx`
233/// holds it — its universe telescope, each member's written parameters (the context's declared
234/// parameter count), its arity past them, and each constructor's type past them.
235///
236/// ★ THE PARAMETER SPLIT IS THE SUBTLE PART, SO IT IS WRITTEN ONCE, HERE. The command surface
237/// refers to a parameter, inside the arity and the constructor types, as the GLOBAL of its name —
238/// the registration then binds every global of that spelling. So a parameter must be named by a
239/// spelling nothing else in those terms uses: not a constant they mention (it would be captured
240/// and bound), and not a binder inside them (the conversion would reach under it and be captured
241/// in turn). The kernel's own binder name is kept when it is safe, which keeps Lean's `α`, and a
242/// fresh one is chosen otherwise.
243///
244/// The command is built, not printed: pass it to [`print_command`] for the text.
245pub fn inductive_command(ctx: &crate::Context, name: &str) -> Result<Command, PrintError> {
246    if !ctx.is_inductive(name) {
247        return Err(no_spelling(format!(
248            "`{name}` as an inductive \u{2014} the context declares no such type"
249        )));
250    }
251    let block: Vec<String> = ctx
252        .mutual_block_of(name)
253        .map(|b| b.to_vec())
254        .unwrap_or_else(|| vec![name.to_string()]);
255    let telescope = ctx
256        .poly_declaration(name)
257        .map(|(params, _)| params.to_vec());
258    let mut members = Vec::with_capacity(block.len());
259    for member in &block {
260        let arity = ctx
261            .get_global(member)
262            .ok_or_else(|| no_spelling(format!("the header of the block member `{member}`")))?;
263        let count = ctx.inductive_num_params(member);
264        let constructors = ctx.get_constructors(member);
265        // Every name the block member's terms spell (a fresh name avoids all of them), and the
266        // names a KEPT parameter name must not collide with: a constant or the head of a qualified
267        // one, and any binder that is not one of the parameter binders being peeled off.
268        let mut avoid: HashSet<String> = HashSet::new();
269        let mut conflicts: HashSet<String> = HashSet::new();
270        for t in std::iter::once(arity).chain(constructors.iter().map(|(_, t)| *t)) {
271            spelled_names(t, &mut avoid);
272            let mut rest = t;
273            for _ in 0..count {
274                if let Term::Pi {
275                    param_type,
276                    body_type,
277                    ..
278                } = rest
279                {
280                    constants_and_binders(param_type, &mut conflicts);
281                    rest = body_type;
282                }
283            }
284            constants_and_binders(rest, &mut conflicts);
285        }
286        let mut names: Vec<String> = Vec::with_capacity(count);
287        let mut cur = arity;
288        for _ in 0..count {
289            let Term::Pi {
290                param, body_type, ..
291            } = cur
292            else {
293                return Err(no_spelling(format!(
294                    "`{member}` declaring {count} parameters with an arity of fewer binders"
295                )));
296            };
297            let keep = plain_atom(param) && !names.contains(param) && !conflicts.contains(param);
298            let chosen = if keep {
299                param.clone()
300            } else {
301                let mut k = 0usize;
302                loop {
303                    let candidate = if k == 0 {
304                        "A".to_string()
305                    } else {
306                        format!("A{k}")
307                    };
308                    k += 1;
309                    if !avoid.contains(&candidate) && !names.contains(&candidate) {
310                        break candidate;
311                    }
312                }
313            };
314            names.push(chosen);
315            cur = body_type;
316        }
317        let (params, sort) = split_telescope(arity, &names, member)?;
318        let split = constructors
319            .iter()
320            .map(|(ctor, ty)| split_telescope(ty, &names, ctor))
321            .collect::<Result<Vec<_>, PrintError>>()?;
322        // A constructor whose own parameter binders do not spell the member's parameter types
323        // (Lean's `HAdd.mk` keeps `outParam (Type w)` where the normalised arity says `Type w`)
324        // cannot be written in the short form, which rebuilds them from the member: then every
325        // constructor of the member restates its telescope, and is kept exactly as registered.
326        let full = split
327            .iter()
328            .any(|(restated, _)| restated.iter().zip(&params).any(|((_, t), (_, p))| t != p));
329        let constructors = if full {
330            constructors
331                .iter()
332                .map(|(ctor, ty)| (ctor.to_string(), (*ty).clone()))
333                .collect()
334        } else {
335            constructors
336                .iter()
337                .zip(split)
338                .map(|((ctor, _), (_, rest))| (ctor.to_string(), rest))
339                .collect()
340        };
341        members.push(InductiveMember {
342            name: member.clone(),
343            params,
344            sort,
345            constructors,
346            full_constructors: full,
347        });
348    }
349    let block = Command::InductiveBlock { members };
350    Ok(match telescope {
351        Some(params) => Command::Universes {
352            params,
353            inner: Box::new(block),
354        },
355        None => block,
356    })
357}
358
359/// The constants a term mentions (and the head of each qualified one) and the binders it
360/// introduces — every spelling a parameter referred to by name could be captured by.
361fn constants_and_binders(t: &Term, out: &mut HashSet<String>) {
362    let mut work = vec![t];
363    while let Some(t) = work.pop() {
364        match t {
365            Term::Global(n) | Term::Const { name: n, .. } => {
366                out.insert(n.clone());
367                if let Some(name) = Name::parse(n) {
368                    out.insert(name.parts()[0].clone());
369                }
370            }
371            Term::Pi {
372                param,
373                param_type,
374                body_type: body,
375            }
376            | Term::Lambda {
377                param,
378                param_type,
379                body,
380            } => {
381                out.insert(param.clone());
382                work.push(param_type);
383                work.push(body);
384            }
385            Term::Let {
386                name,
387                ty,
388                value,
389                body,
390            } => {
391                out.insert(name.clone());
392                work.push(ty);
393                work.push(value);
394                work.push(body);
395            }
396            Term::Fix { name, body } => {
397                out.insert(name.clone());
398                work.push(body);
399            }
400            Term::MutualFix { defs, .. } => {
401                for (n, b) in defs {
402                    out.insert(n.clone());
403                    work.push(b);
404                }
405            }
406            Term::App(f, a) => {
407                work.push(f);
408                work.push(a);
409            }
410            Term::Match {
411                discriminant,
412                motive,
413                cases,
414            } => {
415                work.push(discriminant);
416                work.push(motive);
417                work.extend(cases.iter());
418            }
419            Term::Annotated { term, .. } => work.push(term),
420            Term::Var(_) | Term::Sort(_) | Term::Lit(_) | Term::Hole => {}
421        }
422    }
423}
424
425/// The commands that make `name` a STRUCTURE exactly as `ctx` holds it: each projection's
426/// definition (under the inductive's universe telescope), then `Structure name := proj₀ … projₙ.`
427/// Empty when `ctx` holds no structure information for `name`. Emit them right after the block
428/// from [`inductive_command`], before anything — a recursor whose body is written through the
429/// projections, say — relies on structure η.
430pub fn structure_commands(ctx: &crate::Context, name: &str) -> Result<Vec<Command>, PrintError> {
431    let Some(info) = ctx.struct_info(name) else {
432        return Ok(Vec::new());
433    };
434    let mut out = Vec::with_capacity(info.projections.len() + 1);
435    for projection in &info.projections {
436        let (params, ty, body) = match (ctx.get_universe_poly(projection), ctx.get_definition_body(projection)) {
437            (Some((params, ty, body)), _) if !params.is_empty() => (params.clone(), ty.clone(), body.clone()),
438            (_, Some(body)) => (
439                Vec::new(),
440                ctx.get_definition_type(projection)
441                    .ok_or_else(|| no_spelling(format!("the type of the projection `{projection}`")))?
442                    .clone(),
443                body.clone(),
444            ),
445            (Some((params, ty, body)), None) => (params.clone(), ty.clone(), body.clone()),
446            (None, None) => {
447                return Err(no_spelling(format!(
448                    "the projection `{projection}` of `{name}`, which the context holds no definition for"
449                )))
450            }
451        };
452        let definition = Command::Definition {
453            name: projection.clone(),
454            ty: Some(ty),
455            body,
456            is_hint: false,
457            implicit_count: 0,
458            proof_goal: None,
459        };
460        out.push(if params.is_empty() {
461            definition
462        } else {
463            Command::Universes {
464                params,
465                inner: Box::new(definition),
466            }
467        });
468    }
469    out.push(Command::Structure {
470        inductive: name.to_string(),
471        projections: info.projections.clone(),
472    });
473    Ok(out)
474}
475
476/// Every name a term spells — binders, variables, globals and each component of a qualified one.
477fn spelled_names(t: &Term, out: &mut HashSet<String>) {
478    let mut work = vec![t];
479    while let Some(t) = work.pop() {
480        match t {
481            Term::Var(n) => {
482                out.insert(n.clone());
483            }
484            Term::Global(n) | Term::Const { name: n, .. } => {
485                out.insert(n.clone());
486                if let Some(name) = Name::parse(n) {
487                    out.extend(name.parts().iter().cloned());
488                }
489            }
490            Term::Pi {
491                param,
492                param_type,
493                body_type: body,
494            }
495            | Term::Lambda {
496                param,
497                param_type,
498                body,
499            } => {
500                out.insert(param.clone());
501                work.push(param_type);
502                work.push(body);
503            }
504            Term::Let {
505                name,
506                ty,
507                value,
508                body,
509            } => {
510                out.insert(name.clone());
511                work.push(ty);
512                work.push(value);
513                work.push(body);
514            }
515            Term::Fix { name, body } => {
516                out.insert(name.clone());
517                work.push(body);
518            }
519            Term::MutualFix { defs, .. } => {
520                for (n, b) in defs {
521                    out.insert(n.clone());
522                    work.push(b);
523                }
524            }
525            Term::App(f, a) => {
526                work.push(f);
527                work.push(a);
528            }
529            Term::Match {
530                discriminant,
531                motive,
532                cases,
533            } => {
534                work.push(discriminant);
535                work.push(motive);
536                work.extend(cases.iter());
537            }
538            Term::Annotated { term, .. } => work.push(term),
539            Term::Sort(_) | Term::Lit(_) | Term::Hole => {}
540        }
541    }
542}
543
544/// Peel `names.len()` leading `Π`s off `ty`, turning each binder's variable into the global of the
545/// chosen parameter name — the command surface's convention — and return the parameters and the
546/// rest.
547fn split_telescope(
548    ty: &Term,
549    names: &[String],
550    owner: &str,
551) -> Result<(Vec<(String, Term)>, Term), PrintError> {
552    let mut params = Vec::with_capacity(names.len());
553    let mut cur = ty.clone();
554    for name in names {
555        let Term::Pi {
556            param,
557            param_type,
558            body_type,
559        } = cur
560        else {
561            return Err(no_spelling(format!(
562                "`{owner}` with fewer binders than its block's parameters"
563            )));
564        };
565        params.push((name.clone(), *param_type));
566        cur = crate::substitute(&body_type, &param, &Term::Global(name.clone()));
567    }
568    Ok((params, cur))
569}
570
571// =================================================================================================
572// Commands
573// =================================================================================================
574
575/// A declaring command, with the universe telescope written after its name when it has one.
576fn declaration(cmd: &Command, universes: Option<&Vec<String>>) -> Result<String, PrintError> {
577    let telescope = match universes {
578        Some(params) => universe_telescope(params)?,
579        None => String::new(),
580    };
581    match cmd {
582        Command::Definition {
583            name,
584            ty,
585            body,
586            is_hint,
587            implicit_count,
588            proof_goal,
589        } => {
590            if *is_hint {
591                return Err(no_spelling(format!(
592                    "the hint flag on `{name}` \u{2014} no vernacular command registers a hint"
593                )));
594            }
595            if proof_goal.is_some() {
596                return Err(no_spelling(format!(
597                    "the tactic goal carried by `{name}` \u{2014} only a literate `## Theorem` proof \
598                     carries one, and the vernacular `Definition` cannot state it"
599                )));
600            }
601            let declared = ty.as_ref().unwrap_or(body);
602            if leading_implicits(declared) != *implicit_count {
603                return Err(no_spelling(format!(
604                    "`{name}` with {implicit_count} leading implicit parameter(s) its written type \
605                     does not bind \u{2014} the count is read off the `{{…}}` binders, so it cannot \
606                     be stated apart from them"
607                )));
608            }
609            let head = format!("Definition {}{telescope}", declaration_name(name)?);
610            match ty {
611                Some(ty) => Ok(format!(
612                    "{head} : {} := {}.",
613                    slot(print_term(ty)?, &[":="]),
614                    plain_term(body, "a definition body under a declared type")?
615                )),
616                None => Ok(format!("{head} := {}.", print_term(body)?)),
617            }
618        }
619        Command::Theorem { name, ty, proof } => Ok(format!(
620            "Theorem {}{telescope} : {} := {}.",
621            declaration_name(name)?,
622            slot(print_term(ty)?, &[":="]),
623            plain_term(proof, "a theorem's proof")?
624        )),
625        Command::TheoremBy { name, ty, script } => {
626            let steps: Vec<String> = script.iter().map(print_tactic).collect::<Result<_, _>>()?;
627            Ok(format!(
628                "Theorem {}{telescope} : {} := by {}.",
629                declaration_name(name)?,
630                slot(print_term(ty)?, &[":="]),
631                steps.join("; ")
632            ))
633        }
634        Command::Opaque { name, ty, value } => Ok(format!(
635            "Opaque {}{telescope} : {} := {}.",
636            declaration_name(name)?,
637            slot(print_term(ty)?, &[":="]),
638            plain_term(value, "an opaque declaration's value")?
639        )),
640        Command::Axiom { name, ty } => Ok(format!(
641            "Axiom {}{telescope} : {}.",
642            declaration_name(name)?,
643            slot(print_term(ty)?, &[":="])
644        )),
645        Command::InductiveBlock { members } => {
646            if members.is_empty() {
647                return Err(no_spelling("an inductive block with no members"));
648            }
649            let members = members
650                .iter()
651                .enumerate()
652                .map(|(i, m)| block_member(m, if i == 0 { &telescope } else { "" }))
653                .collect::<Result<Vec<_>, _>>()?;
654            Ok(format!("Inductive {}.", members.join(" with ")))
655        }
656        Command::Quotient { kind, equality } => {
657            use crate::quotient::Kind;
658            if universes.is_none() {
659                return Err(no_spelling(format!(
660                    "the quotient primitive `{}` without its universe parameters \u{2014} they are \
661                     always written",
662                    kind.name()
663                )));
664            }
665            let word = match kind {
666                Kind::Type => "Type",
667                Kind::Constructor => "Constructor",
668                Kind::Lift => "Lift",
669                Kind::Induction => "Induction",
670            };
671            match (kind, equality) {
672                (Kind::Lift, Some(eq)) => Ok(format!(
673                    "Quotient {word}{telescope} using {}.",
674                    global_name(eq)?
675                )),
676                (Kind::Lift, None) => Err(no_spelling("a quotient lift naming no equality")),
677                (_, Some(_)) => Err(no_spelling(format!(
678                    "`Quotient {word}` naming an equality \u{2014} only `Lift` states one"
679                ))),
680                (_, None) => Ok(format!("Quotient {word}{telescope}.")),
681            }
682        }
683        Command::Universes { .. } => Err(no_spelling(
684            "a universe telescope on a declaration that already has one",
685        )),
686        other => Err(no_spelling(format!(
687            "a universe telescope on {} \u{2014} universe parameters bind over a declaration",
688            match other {
689                Command::Inductive { .. } => "the undeclared-arity `Inductive` form",
690                _ => "a command that declares nothing",
691            }
692        ))),
693    }
694}
695
696/// One member of an inductive block: `Name.{u} (p : T) : arity := C : T | …`.
697fn block_member(m: &InductiveMember, telescope: &str) -> Result<String, PrintError> {
698    let mut out = format!("{}{telescope}", declaration_name(&m.name)?);
699    for (param, ty) in &m.params {
700        out.push_str(&parameter(param, ty)?);
701    }
702    out.push_str(" : ");
703    out.push_str(&slot(
704        plain_term(&m.sort, "an inductive's arity")?,
705        &[":=", "|", "with"],
706    ));
707    if m.full_constructors && m.params.is_empty() {
708        return Err(no_spelling(format!(
709            "restated constructor telescopes on `{}`, which has no parameters to restate",
710            m.name
711        )));
712    }
713    if !m.constructors.is_empty() {
714        out.push_str(" := ");
715        out.push_str(&if m.full_constructors {
716            restated_constructor_list(&m.constructors, m.params.len())?
717        } else {
718            constructor_list(&m.constructors)?
719        });
720    }
721    Ok(out)
722}
723
724/// `C (p₁ : T₁) … (pₙ : Tₙ) : rest | …` — each constructor restating its parameter telescope, read
725/// back as the full type `(p₁ : T₁) -> … -> rest`.
726fn restated_constructor_list(
727    constructors: &[(String, Term)],
728    params: usize,
729) -> Result<String, PrintError> {
730    let parts = constructors
731        .iter()
732        .map(|(name, ty)| {
733            if has_kind_marker(ty) {
734                return Err(no_spelling(format!(
735                    "a binder-kind marker in the constructor `{name}`"
736                )));
737            }
738            let mut printer = Printer::new(&[ty]);
739            let (binders, rest) = printer.telescope(ty, params, name)?;
740            Ok(format!(
741                "{}{binders} : {}",
742                declaration_name(name)?,
743                slot(rest, &[":=", "|", "with"])
744            ))
745        })
746        .collect::<Result<Vec<_>, PrintError>>()?;
747    Ok(parts.join(" | "))
748}
749
750/// ` (p : T)` — one written parameter.
751fn parameter(name: &str, ty: &Term) -> Result<String, PrintError> {
752    Ok(format!(
753        " ({} : {})",
754        declaration_name(name)?,
755        plain_term(ty, "a parameter's type")?
756    ))
757}
758
759/// `C₁ : T₁ | C₂ : T₂` — each type bracketed if it holds a separator the sentence is split at.
760fn constructor_list(constructors: &[(String, Term)]) -> Result<String, PrintError> {
761    let parts = constructors
762        .iter()
763        .map(|(name, ty)| {
764            Ok(format!(
765                "{} : {}",
766                declaration_name(name)?,
767                slot(
768                    plain_term(ty, "a constructor's type")?,
769                    &[":=", "|", "with"]
770                )
771            ))
772        })
773        .collect::<Result<Vec<_>, PrintError>>()?;
774    Ok(parts.join(" | "))
775}
776
777/// `.{u, v}` — a universe telescope.
778fn universe_telescope(params: &[String]) -> Result<String, PrintError> {
779    let names = params
780        .iter()
781        .map(|p| level_name(p))
782        .collect::<Result<Vec<_>, _>>()?;
783    Ok(format!(".{{{}}}", names.join(", ")))
784}
785
786/// A term in a slot the command door reads with binder-kind markers STRIPPED (`parse_in`). A
787/// marker there would be dropped on the way back in, so a term carrying one has no spelling in
788/// that slot.
789fn plain_term(t: &Term, slot_name: &str) -> Result<String, PrintError> {
790    if has_kind_marker(t) {
791        return Err(no_spelling(format!(
792            "a binder-kind marker (`{{…}}`, `⦃…⦄`, `[…]`) in {slot_name} \u{2014} that slot reads its \
793             term with the kinds stripped, so the binder kind would not survive"
794        )));
795    }
796    print_term(t)
797}
798
799/// `text`, parenthesised if any of `separators` occurs in it at TOP LEVEL — where the command
800/// door would split the sentence. A parenthesised term is the same term.
801fn slot(text: String, separators: &[&str]) -> String {
802    let splits = separators.iter().any(|sep| {
803        if sep.chars().all(char::is_alphabetic) {
804            top_level_word(&text, sep)
805        } else {
806            TermParser::find_top_level(&text, sep).is_some()
807        }
808    });
809    if splits {
810        format!("({text})")
811    } else {
812        text
813    }
814}
815
816/// Does the WORD `word` occur at top level of `text`?
817fn top_level_word(text: &str, word: &str) -> bool {
818    let is_ident = |c: char| c.is_alphanumeric() || c == '_' || c == '\'';
819    let mut from = 0;
820    while let Some(rel) = TermParser::find_top_level(&text[from..], word) {
821        let at = from + rel;
822        let before = text[..at].chars().next_back();
823        let after = text[at + word.len()..].chars().next();
824        if before.is_none_or(|c| !is_ident(c)) && after.is_none_or(|c| !is_ident(c)) {
825            return true;
826        }
827        from = at + word.len();
828    }
829    false
830}
831
832/// The number of LEADING implicit binders of a declared type (or a lambda telescope) — the count
833/// the command door reads off it.
834fn leading_implicits(t: &Term) -> usize {
835    TermParser::binder_kinds_of(t)
836        .iter()
837        .take_while(|k| **k == TermParser::IMPLICIT)
838        .count()
839}
840
841fn open_clauses(clauses: &[OpenClause]) -> Result<String, PrintError> {
842    if clauses.is_empty() {
843        return Err(no_spelling("an `Open` naming no namespace"));
844    }
845    let mut parts = Vec::with_capacity(clauses.len());
846    for clause in clauses {
847        // A clause list ends at the words that scope or narrow it, so a namespace spelled like one
848        // of them is quoted.
849        let parts_of = clause.namespace.parts();
850        let mut part = if matches!(parts_of[0].as_str(), "in" | "hiding" | "renaming") {
851            let mut quoted = format!("\u{ab}{}\u{bb}", parts_of[0]);
852            for p in &parts_of[1..] {
853                quoted.push('.');
854                quoted.push_str(&tail_component(p)?);
855            }
856            quoted
857        } else {
858            name_path(&clause.namespace)?
859        };
860        let forms = clause.only.is_some() as usize
861            + (!clause.hiding.is_empty()) as usize
862            + (!clause.renaming.is_empty()) as usize;
863        if forms > 1 {
864            return Err(no_spelling(format!(
865                "an `Open {part}` clause that both narrows and renames \u{2014} a clause is one form"
866            )));
867        }
868        if let Some(only) = &clause.only {
869            let names = only.iter().map(name_path).collect::<Result<Vec<_>, _>>()?;
870            if names.is_empty() {
871                return Err(no_spelling(format!("an `Open {part} ()` naming nothing")));
872            }
873            part.push_str(&format!(" ({})", names.join(" ")));
874        } else if !clause.hiding.is_empty() {
875            let names = clause
876                .hiding
877                .iter()
878                .map(name_path)
879                .collect::<Result<Vec<_>, _>>()?;
880            part.push_str(&format!(" hiding ({})", names.join(" ")));
881        } else if !clause.renaming.is_empty() {
882            let pairs = clause
883                .renaming
884                .iter()
885                .map(|(member, local)| {
886                    Ok(format!("{} -> {}", name_path(member)?, name_path(local)?))
887                })
888                .collect::<Result<Vec<_>, PrintError>>()?;
889            part.push_str(&format!(" renaming {}", pairs.join(", ")));
890        }
891        parts.push(part);
892    }
893    Ok(parts.join(" "))
894}
895
896fn surface_declaration(decl: &SurfaceDecl) -> Result<String, PrintError> {
897    let quoted = |pattern: &str| {
898        if pattern.contains('"') {
899            Err(no_spelling(format!(
900                "the pattern {pattern:?} \u{2014} a notation's pattern is a quoted string with no `\"` in it"
901            )))
902        } else {
903            Ok(format!("\"{pattern}\""))
904        }
905    };
906    let expansion = |template: &str| {
907        let text = template.trim();
908        let sentence = format!("{text}.");
909        if text.is_empty() || super::command_parser::sentence_end(&sentence) != sentence.len() {
910            Err(no_spelling(format!(
911                "the expansion {template:?} \u{2014} it would end the sentence early"
912            )))
913        } else {
914            Ok(text.to_string())
915        }
916    };
917    let prec = |p: &Option<u32>| p.map(|p| format!("{p} ")).unwrap_or_default();
918    match decl {
919        SurfaceDecl::Notation {
920            pattern,
921            template,
922            precedence,
923            assoc,
924            operator_form,
925        } => {
926            let word = match (operator_form, assoc) {
927                (None, Assoc::Right) => "Notation",
928                (Some(OperatorForm::Infix), Assoc::Left) => "Infixl",
929                (Some(OperatorForm::Infix), Assoc::Right) => "Infixr",
930                (Some(OperatorForm::Prefix), Assoc::Right) => "Prefix",
931                (Some(OperatorForm::Postfix), Assoc::Left) => "Postfix",
932                _ => {
933                    return Err(no_spelling(format!(
934                        "the notation {pattern:?} with {assoc:?} associativity \u{2014} no command \
935                         word declares that combination"
936                    )))
937                }
938            };
939            if word.starts_with("Infix") && precedence.is_none() {
940                return Err(no_spelling(format!(
941                    "the infix notation {pattern:?} with no precedence \u{2014} `{word}` requires one"
942                )));
943            }
944            Ok(format!(
945                "{word} {}{} := {}.",
946                prec(precedence),
947                quoted(pattern)?,
948                expansion(template)?
949            ))
950        }
951        SurfaceDecl::Macro { pattern, template } => Ok(format!(
952            "Macro {} := {}.",
953            quoted(pattern)?,
954            expansion(template)?
955        )),
956        SurfaceDecl::OpenSection(None) => Ok("Section.".to_string()),
957        SurfaceDecl::OpenSection(Some(name)) => Ok(format!("Section {}.", declaration_name(name)?)),
958        SurfaceDecl::CloseSection(None) => Ok("End.".to_string()),
959        SurfaceDecl::CloseSection(Some(name)) => Ok(format!("End {}.", declaration_name(name)?)),
960        SurfaceDecl::OpenNamespace(name) => Ok(format!("Namespace {}.", name_path(name)?)),
961    }
962}
963
964// =================================================================================================
965// Names
966// =================================================================================================
967
968/// May `s` be written BARE as a term atom and read back as the name `s`?
969fn plain_atom(s: &str) -> bool {
970    matches!(scan_name(s), Some((_, len)) if len == s.len())
971        && !s.starts_with('\u{ab}')
972        && !is_reserved_word(s)
973        && !is_sort_word(s)
974        && s != "_"
975}
976
977/// Can `s` be written in guillemets at all?
978fn quotable(s: &str) -> bool {
979    !s.is_empty() && !s.contains(['\u{ab}', '\u{bb}', '\u{27e8}', '\u{27e9}'])
980}
981
982/// A single name as one token: bare when it reads back bare, `«quoted»` otherwise.
983fn token(s: &str, what: &str) -> Result<String, PrintError> {
984    if plain_atom(s) {
985        Ok(s.to_string())
986    } else if quotable(s) {
987        Ok(format!("\u{ab}{s}\u{bb}"))
988    } else if s.is_empty() {
989        Err(no_spelling(format!("the empty name as {what}")))
990    } else {
991        Err(no_spelling(format!(
992            "the name {s:?} as {what} \u{2014} a name holding a guillemet or an angle bracket has no \
993             quoted form"
994        )))
995    }
996}
997
998/// A component written after a `.` — the term door's `dot_component` reads it with the name
999/// grammar alone, so a reserved word is fine there and only an unscannable one is quoted.
1000fn tail_component(s: &str) -> Result<String, PrintError> {
1001    if matches!(scan_name(s), Some((_, len)) if len == s.len()) && !s.starts_with('\u{ab}') {
1002        Ok(s.to_string())
1003    } else {
1004        token(s, "a name component")
1005    }
1006}
1007
1008/// The spelling of the constant keyed `key` in REFERENCE position: a qualified name when the key
1009/// is one, a bare name, or a `«quoted»` one.
1010///
1011/// A key written with the root escape (`_root_.a.b`, which an emitter uses for a reference its own
1012/// namespace would otherwise capture) is printed verbatim; the reader's resolution strips the
1013/// escape, so it reads back as the absolute `a.b` it stands for.
1014fn global_name(key: &str) -> Result<String, PrintError> {
1015    if key == "_" {
1016        return Err(no_spelling(
1017            "the global `_` \u{2014} `_` is a hole, and the kernel's own match-case placeholder; it is \
1018             never a name a term refers to",
1019        ));
1020    }
1021    match Name::parse(key) {
1022        Some(name) if name.parts().len() > 1 => {
1023            let parts = name.parts();
1024            let mut out = token(&parts[0], "a qualified name's head")?;
1025            for part in &parts[1..] {
1026                out.push('.');
1027                out.push_str(&tail_component(part)?);
1028            }
1029            Ok(out)
1030        }
1031        _ => token(key, "a global"),
1032    }
1033}
1034
1035/// The spelling of a DECLARATION name — the same spelling as a reference, because the declaration
1036/// door keys a name only if a term can spell it back.
1037fn declaration_name(key: &str) -> Result<String, PrintError> {
1038    global_name(key)
1039}
1040
1041/// A hierarchical `Name` as written in `Open`, `Export` and `Print Axioms`.
1042fn name_path(name: &Name) -> Result<String, PrintError> {
1043    let parts = name.parts();
1044    let mut out = token(&parts[0], "a name")?;
1045    for part in &parts[1..] {
1046        out.push('.');
1047        out.push_str(&tail_component(part)?);
1048    }
1049    Ok(out)
1050}
1051
1052/// A universe variable's name: bare unless the level grammar reads that word as syntax.
1053fn level_name(name: &str) -> Result<String, PrintError> {
1054    if plain_atom(name) && name != "max" && name != "imax" {
1055        Ok(name.to_string())
1056    } else {
1057        let quoted = token(name, "a universe variable")?;
1058        if quoted.starts_with('\u{ab}') {
1059            Ok(quoted)
1060        } else {
1061            Ok(format!("\u{ab}{name}\u{bb}"))
1062        }
1063    }
1064}
1065
1066// =================================================================================================
1067// Levels
1068// =================================================================================================
1069
1070/// A level; `atom` when it stands where the level grammar reads one ATOM (after `Sort`, as an
1071/// operand of `max`/`imax`, as the base of `+k`).
1072fn level(u: &Universe, atom: bool) -> Result<String, PrintError> {
1073    let text = match u {
1074        Universe::Prop => return Ok("0".to_string()),
1075        Universe::Type(n) => return Ok((u64::from(*n) + 1).to_string()),
1076        Universe::Var(name) => return level_name(name),
1077        Universe::SProp => {
1078            return Err(no_spelling(
1079                "`SProp` inside a universe level \u{2014} it is a sort, not a level, and collapses out \
1080                 of every `max`, `imax` and successor",
1081            ))
1082        }
1083        Universe::Succ(_) => {
1084            let mut base = u;
1085            let mut k: u64 = 0;
1086            while let Universe::Succ(inner) = base {
1087                k += 1;
1088                base = inner;
1089            }
1090            let mut text = level(base, true)?;
1091            while k > 0 {
1092                let step = k.min(MAX_LEVEL_OFFSET);
1093                text.push_str(&format!("+{step}"));
1094                k -= step;
1095            }
1096            text
1097        }
1098        Universe::Max(a, b) => format!("max {} {}", level(a, true)?, level(b, true)?),
1099        Universe::IMax(a, b) => format!("imax {} {}", level(a, true)?, level(b, true)?),
1100    };
1101    Ok(if atom { format!("({text})") } else { text })
1102}
1103
1104/// A sort: `Prop`, `SProp`, `Type`, `Type n`, or `Sort ℓ`.
1105fn sort(u: &Universe) -> Result<String, PrintError> {
1106    Ok(match u {
1107        Universe::Prop => "Prop".to_string(),
1108        Universe::SProp => "SProp".to_string(),
1109        Universe::Type(0) => "Type".to_string(),
1110        Universe::Type(n) => format!("Type {n}"),
1111        other => format!("Sort {}", level(other, true)?),
1112    })
1113}
1114
1115// =================================================================================================
1116// Literals
1117// =================================================================================================
1118
1119fn literal(l: &Literal) -> Result<String, PrintError> {
1120    Ok(match l {
1121        Literal::Int(n) => n.to_string(),
1122        Literal::BigInt(n) => {
1123            if n.to_i64().is_some() {
1124                return Err(no_spelling(format!(
1125                    "the BigInt literal {n}, which fits a machine integer \u{2014} an integer has ONE \
1126                     literal representation, and this value's is `Int`"
1127                )));
1128            }
1129            n.to_string()
1130        }
1131        Literal::Nat(n) => {
1132            if n.is_negative() {
1133                return Err(no_spelling(format!("the negative Nat literal {n}")));
1134            }
1135            format!("{n}n")
1136        }
1137        Literal::Float(x) if x.is_finite() => format!("{x:?}f64"),
1138        Literal::Float(x) => format!("0x{:016x}f64", x.to_bits()),
1139        Literal::Text(s) => text(s),
1140        Literal::Duration(n) => duration(*n),
1141        Literal::Date(d) => date(i64::from(*d)),
1142        Literal::Moment(n) => moment(*n),
1143    })
1144}
1145
1146fn text(s: &str) -> String {
1147    let mut out = String::with_capacity(s.len() + 2);
1148    out.push('"');
1149    for c in s.chars() {
1150        match c {
1151            '"' => out.push_str("\\\""),
1152            '\\' => out.push_str("\\\\"),
1153            '\n' => out.push_str("\\n"),
1154            '\t' => out.push_str("\\t"),
1155            c if c.is_control() || matches!(c, '\u{2028}' | '\u{2029}' | '\u{feff}') => {
1156                out.push_str(&format!("\\u{{{:x}}}", c as u32))
1157            }
1158            c => out.push(c),
1159        }
1160    }
1161    out.push('"');
1162    out
1163}
1164
1165/// An annotation's surface spelling, inside `@[…]` — the term parser's `annotation` production.
1166fn annotation_text(annotation: &crate::Annotation) -> String {
1167    use crate::{Annotation, Provenance};
1168    match annotation {
1169        Annotation::Binder(kind) => format!("binder {}", kind.lean_binder_info()),
1170        Annotation::Source(Provenance::Position(position)) => {
1171            format!("source {}", text(position.as_str()))
1172        }
1173        Annotation::Source(Provenance::LeanMetadata { key, value }) => {
1174            format!("mdata {} {}", text(key), text(value))
1175        }
1176    }
1177}
1178
1179/// A duration in the coarsest unit that divides it exactly.
1180fn duration(n: i64) -> String {
1181    if n == 0 {
1182        return "0s".to_string();
1183    }
1184    const UNITS: [(&str, i64); 6] = [
1185        ("h", 3_600_000_000_000),
1186        ("min", 60_000_000_000),
1187        ("s", 1_000_000_000),
1188        ("ms", 1_000_000),
1189        ("us", 1_000),
1190        ("ns", 1),
1191    ];
1192    let (unit, per) = UNITS
1193        .iter()
1194        .find(|(_, per)| n % per == 0)
1195        .expect("every count divides by 1");
1196    format!("{}{unit}", n / per)
1197}
1198
1199fn calendar_date(days: i64) -> String {
1200    let (y, m, d) = super::term_parser::civil_from_days(days);
1201    let year = if y < 0 {
1202        format!("-{:04}", -y)
1203    } else {
1204        format!("{y:04}")
1205    };
1206    format!("{year}-{m:02}-{d:02}")
1207}
1208
1209fn date(days: i64) -> String {
1210    calendar_date(days)
1211}
1212
1213fn moment(nanos: i64) -> String {
1214    let secs = nanos.div_euclid(1_000_000_000);
1215    let frac = nanos.rem_euclid(1_000_000_000);
1216    let days = secs.div_euclid(86_400);
1217    let tod = secs.rem_euclid(86_400);
1218    let mut out = format!(
1219        "{}T{:02}:{:02}:{:02}",
1220        calendar_date(days),
1221        tod / 3600,
1222        (tod % 3600) / 60,
1223        tod % 60
1224    );
1225    if frac != 0 {
1226        let digits = format!("{frac:09}");
1227        out.push('.');
1228        out.push_str(digits.trim_end_matches('0'));
1229    }
1230    out.push('Z');
1231    out
1232}
1233
1234/// Does a literal print with a leading `-`?
1235fn negative_literal(l: &Literal) -> bool {
1236    match l {
1237        Literal::Int(n) | Literal::Duration(n) | Literal::Moment(n) => *n < 0,
1238        Literal::BigInt(n) => n.is_negative(),
1239        Literal::Float(x) => x.is_finite() && x.is_sign_negative(),
1240        Literal::Date(d) => super::term_parser::civil_from_days(i64::from(*d)).0 < 0,
1241        Literal::Nat(_) | Literal::Text(_) => false,
1242    }
1243}
1244
1245// =================================================================================================
1246// Terms
1247// =================================================================================================
1248
1249/// Where a term stands, which decides what needs parentheses.
1250#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1251enum Ctx {
1252    /// Nothing follows that the term could swallow: the whole term, inside brackets, before `|`,
1253    /// `end`, `:=`, `;`, `,` or `⟩`.
1254    Top,
1255    /// Before `with` or `return` — a mutual `fix`'s bodies. A `fix` at the tail would read those
1256    /// words as its own.
1257    BeforeWith,
1258    /// An application or anything tighter: the left of `->`, a match's discriminant or motive.
1259    Operand,
1260    /// An argument or an application's head: an atom.
1261    Atom,
1262}
1263
1264/// How a term prints, for the purpose of bracketing.
1265#[derive(Clone, Copy, PartialEq, Eq)]
1266enum Shape {
1267    /// Binds as tightly as anything.
1268    Atom,
1269    /// An application, or a sort or literal that reads as one.
1270    App,
1271    /// Closed on the right by `end`, but not an atom.
1272    Closed,
1273    /// Extends as far right as it can: `fun`, a binder, `let`.
1274    Open,
1275    /// A `fix` — open on the right, and it reads a following `with` or `return` as its own.
1276    Fix,
1277}
1278
1279fn needs_parens(shape: Shape, ctx: Ctx) -> bool {
1280    match ctx {
1281        Ctx::Top => false,
1282        Ctx::BeforeWith => shape == Shape::Fix,
1283        Ctx::Operand => !matches!(shape, Shape::Atom | Shape::App),
1284        Ctx::Atom => shape != Shape::Atom,
1285    }
1286}
1287
1288/// The identity of a binder, stable across the two passes: the address of the node that binds it,
1289/// and for a mutual block which member.
1290type BinderId = (usize, usize);
1291
1292fn node_id(t: &Term) -> usize {
1293    t as *const Term as usize
1294}
1295
1296/// A pending piece of printing work.
1297enum Job<'t> {
1298    Text(&'static str),
1299    Owned(String),
1300    Term(&'t Term, Ctx),
1301    /// Bring binders into scope: (the kernel's name, the spelling that now reads as it).
1302    Bind(Vec<(String, String)>),
1303    /// Take the last `n` binders back out of scope.
1304    Unbind(usize),
1305}
1306
1307struct Printer {
1308    out: String,
1309    /// Binders some variable in their scope refers to.
1310    referenced: HashSet<BinderId>,
1311    /// Names a binder may not keep: every global the terms reference, and every head of a
1312    /// qualified one — keeping them would capture that global.
1313    avoid: HashSet<String>,
1314    /// Every name the terms spell anywhere; a fresh name is chosen outside it.
1315    taken: HashSet<String>,
1316    fresh: usize,
1317    /// The binders in scope, innermost last: kernel name → spelling.
1318    scope: HashMap<String, Vec<String>>,
1319    scope_order: Vec<String>,
1320}
1321
1322impl Printer {
1323    fn new(roots: &[&Term]) -> Self {
1324        let mut printer = Printer {
1325            out: String::new(),
1326            referenced: HashSet::new(),
1327            avoid: HashSet::new(),
1328            taken: HashSet::new(),
1329            fresh: 0,
1330            scope: HashMap::new(),
1331            scope_order: Vec::new(),
1332        };
1333        for root in roots {
1334            printer.scan(root);
1335        }
1336        printer
1337    }
1338
1339    /// The first pass: which binders are referred to, which names are globals, which are taken.
1340    fn scan(&mut self, root: &Term) {
1341        enum Visit<'t> {
1342            Enter(&'t Term),
1343            Bind(String, BinderId),
1344            Unbind(String),
1345        }
1346        let mut bound: HashMap<String, Vec<BinderId>> = HashMap::new();
1347        let mut work = vec![Visit::Enter(root)];
1348        while let Some(visit) = work.pop() {
1349            let t = match visit {
1350                Visit::Bind(name, id) => {
1351                    bound.entry(name).or_default().push(id);
1352                    continue;
1353                }
1354                Visit::Unbind(name) => {
1355                    if let Some(stack) = bound.get_mut(&name) {
1356                        stack.pop();
1357                    }
1358                    continue;
1359                }
1360                Visit::Enter(t) => t,
1361            };
1362            match t {
1363                Term::Var(n) => {
1364                    self.taken.insert(n.clone());
1365                    if let Some(id) = bound.get(n).and_then(|s| s.last()) {
1366                        self.referenced.insert(*id);
1367                    }
1368                }
1369                Term::Global(n) | Term::Const { name: n, .. } => self.note_global(n),
1370                Term::Sort(_) | Term::Lit(_) | Term::Hole => {}
1371                Term::Pi {
1372                    param,
1373                    param_type,
1374                    body_type: body,
1375                }
1376                | Term::Lambda {
1377                    param,
1378                    param_type,
1379                    body,
1380                } => {
1381                    self.taken.insert(param.clone());
1382                    work.push(Visit::Unbind(param.clone()));
1383                    work.push(Visit::Enter(body));
1384                    work.push(Visit::Bind(param.clone(), (node_id(t), 0)));
1385                    work.push(Visit::Enter(param_type));
1386                }
1387                Term::Let {
1388                    name,
1389                    ty,
1390                    value,
1391                    body,
1392                } => {
1393                    self.taken.insert(name.clone());
1394                    work.push(Visit::Unbind(name.clone()));
1395                    work.push(Visit::Enter(body));
1396                    work.push(Visit::Bind(name.clone(), (node_id(t), 0)));
1397                    work.push(Visit::Enter(value));
1398                    work.push(Visit::Enter(ty));
1399                }
1400                Term::Fix { name, body } => {
1401                    self.taken.insert(name.clone());
1402                    work.push(Visit::Unbind(name.clone()));
1403                    work.push(Visit::Enter(body));
1404                    work.push(Visit::Bind(name.clone(), (node_id(t), 0)));
1405                }
1406                Term::MutualFix { defs, .. } => {
1407                    for (n, _) in defs.iter().rev() {
1408                        work.push(Visit::Unbind(n.clone()));
1409                    }
1410                    for (_, b) in defs.iter().rev() {
1411                        work.push(Visit::Enter(b));
1412                    }
1413                    for (i, (n, _)) in defs.iter().enumerate().rev() {
1414                        self.taken.insert(n.clone());
1415                        work.push(Visit::Bind(n.clone(), (node_id(t), i)));
1416                    }
1417                }
1418                Term::Match {
1419                    discriminant,
1420                    motive,
1421                    cases,
1422                } => {
1423                    for c in cases.iter().rev() {
1424                        work.push(Visit::Enter(c));
1425                    }
1426                    work.push(Visit::Enter(motive));
1427                    work.push(Visit::Enter(discriminant));
1428                }
1429                Term::App(f, a) => {
1430                    // A LABEL is not a reference: a named argument's parameter name, a projection's
1431                    // field and an auto-param's tactic are spelled as names but denote no global.
1432                    if let Some(label_free) = label_positions(t) {
1433                        for part in label_free {
1434                            work.push(Visit::Enter(part));
1435                        }
1436                        continue;
1437                    }
1438                    work.push(Visit::Enter(a));
1439                    work.push(Visit::Enter(f));
1440                }
1441                Term::Annotated { term, .. } => work.push(Visit::Enter(term)),
1442            }
1443        }
1444    }
1445
1446    fn note_global(&mut self, key: &str) {
1447        self.taken.insert(key.to_string());
1448        self.avoid.insert(key.to_string());
1449        if let Some(name) = Name::parse(key) {
1450            for part in name.parts() {
1451                self.taken.insert(part.clone());
1452            }
1453            self.avoid.insert(name.parts()[0].clone());
1454        }
1455    }
1456
1457    /// The spelling a binder named `name` will have: its own name when that reads back as this
1458    /// binder, a fresh one otherwise.
1459    fn choose(&mut self, name: &str, referenced: bool) -> String {
1460        if name == "_" && !referenced {
1461            return "_".to_string();
1462        }
1463        // A binder keeps its name when the name reads back BARE. One that would need guillemets
1464        // (`arena.local.3`, the importer's own binder names) is renamed instead: renaming a binder
1465        // changes nothing the kernel can see, and a quoted binder name carries nothing a reader
1466        // can use.
1467        if !self.avoid.contains(name) && plain_atom(name) {
1468            return name.to_string();
1469        }
1470        loop {
1471            let candidate = if self.fresh == 0 {
1472                "x".to_string()
1473            } else {
1474                format!("x{}", self.fresh)
1475            };
1476            self.fresh += 1;
1477            if !self.taken.contains(&candidate) && !self.avoid.contains(&candidate) {
1478                self.taken.insert(candidate.clone());
1479                return candidate;
1480            }
1481        }
1482    }
1483
1484    fn bind(&mut self, pairs: Vec<(String, String)>) {
1485        for (name, spelled) in pairs {
1486            self.scope.entry(name.clone()).or_default().push(spelled);
1487            self.scope_order.push(name);
1488        }
1489    }
1490
1491    fn unbind(&mut self, n: usize) {
1492        for _ in 0..n {
1493            if let Some(name) = self.scope_order.pop() {
1494                if let Some(stack) = self.scope.get_mut(&name) {
1495                    stack.pop();
1496                }
1497            }
1498        }
1499    }
1500
1501    /// The first `count` binders of the `Π` telescope `t` as ` (x : A)` groups — every one NAMED,
1502    /// referred to or not, because a restated parameter is a binder the reader counts — and the
1503    /// rest of `t`, printed with those binders in scope.
1504    fn telescope(
1505        &mut self,
1506        t: &Term,
1507        count: usize,
1508        owner: &str,
1509    ) -> Result<(String, String), PrintError> {
1510        let mut binders = String::new();
1511        let mut cur = t;
1512        let mut bound = 0;
1513        for _ in 0..count {
1514            let Term::Pi {
1515                param,
1516                param_type,
1517                body_type,
1518            } = cur
1519            else {
1520                return Err(no_spelling(format!(
1521                    "`{owner}` restating {count} parameters with fewer binders"
1522                )));
1523            };
1524            if !matches!(split_kind(param_type).0, Kind::Explicit)
1525                || split_default(param_type)?.1.is_some()
1526            {
1527                return Err(no_spelling(format!(
1528                    "a marked parameter binder in `{owner}`"
1529                )));
1530            }
1531            let spelled = self.choose(param, true);
1532            let start = self.out.len();
1533            self.term(param_type, Ctx::Top)?;
1534            let ty = self.out.split_off(start);
1535            binders.push_str(&format!(" ({spelled} : {ty})"));
1536            self.bind(vec![(param.clone(), spelled)]);
1537            bound += 1;
1538            cur = body_type;
1539        }
1540        let start = self.out.len();
1541        self.term(cur, Ctx::Top)?;
1542        let rest = self.out.split_off(start);
1543        self.unbind(bound);
1544        Ok((binders, rest))
1545    }
1546
1547    fn term(&mut self, root: &Term, ctx: Ctx) -> Result<(), PrintError> {
1548        let mut work: Vec<Job<'_>> = vec![Job::Term(root, ctx)];
1549        while let Some(job) = work.pop() {
1550            match job {
1551                Job::Text(s) => self.out.push_str(s),
1552                Job::Owned(s) => self.out.push_str(&s),
1553                Job::Bind(pairs) => self.bind(pairs),
1554                Job::Unbind(n) => self.unbind(n),
1555                Job::Term(t, ctx) => {
1556                    let shape = self.shape(t);
1557                    if needs_parens(shape, ctx) {
1558                        work.push(Job::Text(")"));
1559                        work.push(Job::Term(t, Ctx::Top));
1560                        work.push(Job::Text("("));
1561                        continue;
1562                    }
1563                    let mut jobs = self.expand(t, ctx)?;
1564                    jobs.reverse();
1565                    work.extend(jobs);
1566                }
1567            }
1568        }
1569        Ok(())
1570    }
1571
1572    /// How `t` will print.
1573    fn shape(&self, t: &Term) -> Shape {
1574        match t {
1575            Term::Var(_) | Term::Global(_) | Term::Const { .. } | Term::Hole => Shape::Atom,
1576            Term::Sort(Universe::Prop | Universe::SProp | Universe::Type(0)) => Shape::Atom,
1577            Term::Sort(_) => Shape::App,
1578            Term::Lit(l) if negative_literal(l) => Shape::App,
1579            Term::Lit(_) => Shape::Atom,
1580            Term::App(..) => match spine_form(t) {
1581                SpineForm::Named | SpineForm::Anon | SpineForm::Projection => Shape::Atom,
1582                _ => Shape::App,
1583            },
1584            Term::Match { .. } => Shape::Closed,
1585            Term::Fix { .. } | Term::MutualFix { .. } => Shape::Fix,
1586            Term::Pi { .. } | Term::Lambda { .. } | Term::Let { .. } => Shape::Open,
1587            // `@[…] t` is a prefix on an atom: bracketed as an argument, a head or a projection's
1588            // receiver, where a following `.field` or argument would otherwise be read as its own.
1589            Term::Annotated { .. } => Shape::App,
1590        }
1591    }
1592
1593    /// The jobs that print `t` in `ctx`, in order.
1594    fn expand<'t>(&mut self, t: &'t Term, ctx: Ctx) -> Result<Vec<Job<'t>>, PrintError> {
1595        // The tail of an open construct inherits where the construct stands.
1596        let tail = if ctx == Ctx::BeforeWith {
1597            Ctx::BeforeWith
1598        } else {
1599            Ctx::Top
1600        };
1601        Ok(match t {
1602            Term::Var(n) => match self.scope.get(n).and_then(|s| s.last()) {
1603                Some(spelled) => vec![Job::Owned(spelled.clone())],
1604                None => {
1605                    return Err(no_spelling(format!(
1606                        "the free local variable `{n}` \u{2014} the surface reads a free name as a \
1607                         GLOBAL, so printing it would denote something else"
1608                    )))
1609                }
1610            },
1611            Term::Global(n) => vec![Job::Owned(global_name(n)?)],
1612            Term::Const { name, levels } => {
1613                let levels = levels
1614                    .iter()
1615                    .map(|l| level(l, false))
1616                    .collect::<Result<Vec<_>, _>>()?;
1617                vec![Job::Owned(format!(
1618                    "{}.{{{}}}",
1619                    global_name(name)?,
1620                    levels.join(", ")
1621                ))]
1622            }
1623            Term::Sort(u) => vec![Job::Owned(sort(u)?)],
1624            Term::Lit(l) => vec![Job::Owned(literal(l)?)],
1625            Term::Hole => vec![Job::Text("_")],
1626            Term::App(..) => self.application(t)?,
1627            Term::Pi {
1628                param,
1629                param_type,
1630                body_type,
1631            } => self.pi(t, param, param_type, body_type, tail)?,
1632            Term::Lambda { .. } => self.lambda(t, tail)?,
1633            Term::Let {
1634                name,
1635                ty,
1636                value,
1637                body,
1638            } => {
1639                let spelled = self.choose(name, self.referenced.contains(&(node_id(t), 0)));
1640                let wrap = |x: &Term| {
1641                    if matches!(x, Term::Let { .. }) {
1642                        Ctx::Operand
1643                    } else {
1644                        Ctx::Top
1645                    }
1646                };
1647                vec![
1648                    Job::Text("let "),
1649                    Job::Owned(spelled.clone()),
1650                    Job::Text(" : "),
1651                    Job::Term(ty, wrap(ty)),
1652                    Job::Text(" := "),
1653                    Job::Term(value, wrap(value)),
1654                    Job::Text("; "),
1655                    Job::Bind(vec![(name.clone(), spelled)]),
1656                    Job::Term(body, tail),
1657                    Job::Unbind(1),
1658                ]
1659            }
1660            Term::Fix { name, body } => {
1661                let spelled = self.choose(name, self.referenced.contains(&(node_id(t), 0)));
1662                vec![
1663                    Job::Text("fix "),
1664                    Job::Owned(spelled.clone()),
1665                    Job::Text(" => "),
1666                    Job::Bind(vec![(name.clone(), spelled)]),
1667                    Job::Term(body, tail),
1668                    Job::Unbind(1),
1669                ]
1670            }
1671            Term::MutualFix { defs, index } => {
1672                let mut seen = HashSet::new();
1673                for (n, _) in defs {
1674                    if !seen.insert(n.as_str()) {
1675                        return Err(no_spelling(format!(
1676                            "a mutual fix naming `{n}` twice \u{2014} `return {n}` could not say which \
1677                             member the term denotes"
1678                        )));
1679                    }
1680                }
1681                if *index >= defs.len() {
1682                    return Err(no_spelling(format!(
1683                        "a mutual fix of {} member(s) selecting member {index}",
1684                        defs.len()
1685                    )));
1686                }
1687                let spelled: Vec<String> = defs
1688                    .iter()
1689                    .enumerate()
1690                    // A member's name is also how `return` selects it, so it is always spelled, even
1691                    // when no body refers to it.
1692                    .map(|(_, (n, _))| self.choose(n, true))
1693                    .collect();
1694                let mut jobs = vec![
1695                    Job::Text("fix "),
1696                    Job::Bind(
1697                        defs.iter()
1698                            .map(|(n, _)| n.clone())
1699                            .zip(spelled.iter().cloned())
1700                            .collect(),
1701                    ),
1702                ];
1703                for (i, (_, body)) in defs.iter().enumerate() {
1704                    if i > 0 {
1705                        jobs.push(Job::Text(" with "));
1706                    }
1707                    jobs.push(Job::Owned(spelled[i].clone()));
1708                    jobs.push(Job::Text(" => "));
1709                    jobs.push(Job::Term(body, Ctx::BeforeWith));
1710                }
1711                jobs.push(Job::Unbind(defs.len()));
1712                jobs.push(Job::Text(" return "));
1713                jobs.push(Job::Owned(spelled[*index].clone()));
1714                jobs
1715            }
1716            Term::Match {
1717                discriminant,
1718                motive,
1719                cases,
1720            } => self.matching(discriminant, motive, cases)?,
1721            Term::Annotated { annotation, term } => vec![
1722                Job::Text("@["),
1723                Job::Owned(annotation_text(annotation)),
1724                Job::Text("] "),
1725                Job::Term(term, Ctx::Atom),
1726            ],
1727        })
1728    }
1729
1730    /// `Π(x : A). B` — as `A -> B` when nothing refers to `x`, else as a binder, whose bracket says
1731    /// how the argument is supplied.
1732    fn pi<'t>(
1733        &mut self,
1734        node: &'t Term,
1735        param: &str,
1736        param_type: &'t Term,
1737        body: &'t Term,
1738        tail: Ctx,
1739    ) -> Result<Vec<Job<'t>>, PrintError> {
1740        let referenced = self.referenced.contains(&(node_id(node), 0));
1741        let (kind, declared) = split_kind(param_type);
1742        let (declared, default) = split_default(declared)?;
1743        if kind == Kind::Explicit && default.is_none() && !referenced {
1744            return Ok(vec![
1745                Job::Term(declared, Ctx::Operand),
1746                Job::Text(" -> "),
1747                Job::Term(body, tail),
1748            ]);
1749        }
1750        let spelled = self.choose(param, referenced);
1751        let (open, close) = kind.brackets();
1752        let mut jobs = vec![Job::Text(open)];
1753        let anonymous_instance = kind == Kind::Instance && spelled == "_" && default.is_none();
1754        if !anonymous_instance {
1755            jobs.push(Job::Owned(spelled.clone()));
1756            jobs.push(Job::Text(" : "));
1757        }
1758        jobs.push(Job::Term(declared, Ctx::Top));
1759        match default {
1760            Some(Default::Value(v)) if kind == Kind::Explicit => {
1761                jobs.push(Job::Text(" := "));
1762                jobs.push(Job::Term(v, Ctx::Top));
1763            }
1764            Some(Default::Tactic(tac)) if kind == Kind::Explicit => {
1765                jobs.push(Job::Owned(format!(
1766                    " := by {}",
1767                    token(tac, "an auto-param tactic")?
1768                )));
1769            }
1770            Some(_) => {
1771                return Err(no_spelling(format!(
1772                "a default on the {} binder `{param}` \u{2014} only an explicit binder carries one",
1773                kind.describe()
1774            )))
1775            }
1776            None => {}
1777        }
1778        jobs.push(Job::Text(close));
1779        jobs.push(Job::Text(" -> "));
1780        jobs.push(Job::Bind(vec![(param.to_string(), spelled)]));
1781        jobs.push(Job::Term(body, tail));
1782        jobs.push(Job::Unbind(1));
1783        Ok(jobs)
1784    }
1785
1786    /// `fun (x : A) {y} … => body` — consecutive lambdas share one `fun`.
1787    fn lambda<'t>(&mut self, t: &'t Term, tail: Ctx) -> Result<Vec<Job<'t>>, PrintError> {
1788        let mut jobs = vec![Job::Text("fun")];
1789        let mut cur = t;
1790        let mut bound = 0;
1791        while let Term::Lambda {
1792            param,
1793            param_type,
1794            body,
1795        } = cur
1796        {
1797            let referenced = self.referenced.contains(&(node_id(cur), 0));
1798            let (kind, declared) = split_kind(param_type);
1799            if TermParser::as_default_param(param_type).is_some()
1800                || TermParser::as_auto_param(param_type).is_some()
1801            {
1802                return Err(no_spelling(format!(
1803                    "a default on the `fun` binder `{param}` \u{2014} a `fun` binder carries none"
1804                )));
1805            }
1806            let spelled = self.choose(param, referenced);
1807            let (open, close) = kind.brackets();
1808            jobs.push(Job::Text(" "));
1809            match (kind, declared) {
1810                (Kind::Explicit, Term::Hole) => jobs.push(Job::Owned(spelled.clone())),
1811                (Kind::Instance, _) if spelled == "_" => {
1812                    jobs.push(Job::Text(open));
1813                    jobs.push(Job::Term(declared, Ctx::Top));
1814                    jobs.push(Job::Text(close));
1815                }
1816                (_, Term::Hole) => {
1817                    jobs.push(Job::Text(open));
1818                    jobs.push(Job::Owned(spelled.clone()));
1819                    jobs.push(Job::Text(close));
1820                }
1821                _ => {
1822                    jobs.push(Job::Text(open));
1823                    jobs.push(Job::Owned(spelled.clone()));
1824                    jobs.push(Job::Text(" : "));
1825                    jobs.push(Job::Term(declared, Ctx::Top));
1826                    jobs.push(Job::Text(close));
1827                }
1828            }
1829            jobs.push(Job::Bind(vec![(param.clone(), spelled)]));
1830            bound += 1;
1831            cur = body;
1832        }
1833        jobs.push(Job::Text(" => "));
1834        jobs.push(Job::Term(cur, tail));
1835        jobs.push(Job::Unbind(bound));
1836        Ok(jobs)
1837    }
1838
1839    /// An application spine, and the parser's argument-position forms that ride on one.
1840    fn application<'t>(&mut self, t: &'t Term) -> Result<Vec<Job<'t>>, PrintError> {
1841        let (head, args) = spine(t);
1842        // `f ..` must END its spine, and `..` followed by a space would end the SENTENCE, so an
1843        // application closed by an ellipsis is always bracketed: `(f ..)`.
1844        let ellipsis = args
1845            .last()
1846            .is_some_and(|a| is_marker(a, TermParser::ELLIPSIS_MARKER))
1847            && !matches!(spine_form(t), SpineForm::Anon);
1848        let mut jobs = if ellipsis {
1849            vec![Job::Text("(")]
1850        } else {
1851            Vec::new()
1852        };
1853        let mut rest: &[&'t Term] = &args;
1854        match spine_form(t) {
1855            SpineForm::Named => {
1856                let (Term::Global(label), value) = (args[0], args[1]) else {
1857                    unreachable!()
1858                };
1859                jobs.push(Job::Owned(format!(
1860                    "({} := ",
1861                    token(label, "a named argument's parameter")?
1862                )));
1863                jobs.push(Job::Term(value, Ctx::Top));
1864                jobs.push(Job::Text(")"));
1865                rest = &args[2..];
1866            }
1867            SpineForm::Anon => {
1868                jobs.push(Job::Text("\u{27e8}"));
1869                for (i, c) in args.iter().enumerate() {
1870                    if i > 0 {
1871                        jobs.push(Job::Text(", "));
1872                    }
1873                    if is_marker(c, TermParser::ELLIPSIS_MARKER) {
1874                        if i + 1 != args.len() {
1875                            return Err(no_spelling(
1876                                "an ellipsis before the last component of `⟨…⟩`",
1877                            ));
1878                        }
1879                        jobs.push(Job::Text(".."));
1880                    } else {
1881                        jobs.push(Job::Term(c, Ctx::Top));
1882                    }
1883                }
1884                jobs.push(Job::Text("\u{27e9}"));
1885                rest = &[];
1886            }
1887            SpineForm::Projection => {
1888                let Term::Global(field) = args[1] else {
1889                    unreachable!()
1890                };
1891                let field = if !field.is_empty() && field.chars().all(|c| c.is_ascii_digit()) {
1892                    if matches!(args[0], Term::Lit(_)) {
1893                        return Err(no_spelling(format!(
1894                            "the numeric projection `.{field}` of a literal \u{2014} `3.1` is a decimal"
1895                        )));
1896                    }
1897                    field.clone()
1898                } else {
1899                    tail_component(field)?
1900                };
1901                jobs.push(Job::Term(args[0], Ctx::Atom));
1902                jobs.push(Job::Owned(format!(".{field}")));
1903                rest = &args[2..];
1904            }
1905            SpineForm::Explicit => {
1906                let named = args[0];
1907                if !starts_with_name(named) {
1908                    return Err(no_spelling(
1909                        "`@` before something that is not a name \u{2014} `@` binds to a constant's name",
1910                    ));
1911                }
1912                jobs.push(Job::Text("@"));
1913                jobs.push(Job::Term(named, Ctx::Atom));
1914                rest = &args[1..];
1915            }
1916            SpineForm::Plain => {
1917                push_spine_atom(&mut jobs, head, rest.first().copied());
1918            }
1919        }
1920        for (i, arg) in rest.iter().enumerate() {
1921            if is_marker(arg, TermParser::ELLIPSIS_MARKER) {
1922                if i + 1 != rest.len() {
1923                    return Err(no_spelling(
1924                        "an ellipsis before an application's last argument",
1925                    ));
1926                }
1927                jobs.push(Job::Text(" .."));
1928            } else {
1929                jobs.push(Job::Text(" "));
1930                push_spine_atom(&mut jobs, arg, rest.get(i + 1).copied());
1931            }
1932        }
1933        if ellipsis {
1934            jobs.push(Job::Text(")"));
1935        }
1936        Ok(jobs)
1937    }
1938
1939    /// A kernel match — `match d return M with c₀ | c₁ end` — or the parser's pattern form, whose
1940    /// clauses the context compiles later.
1941    fn matching<'t>(
1942        &mut self,
1943        discriminant: &'t Term,
1944        motive: &'t Term,
1945        cases: &'t [Term],
1946    ) -> Result<Vec<Job<'t>>, PrintError> {
1947        let raw: Vec<Option<(&Term, &Term)>> =
1948            cases.iter().map(super::raw_match::as_case).collect();
1949        let patterned = !cases.is_empty() && raw.iter().all(Option::is_some);
1950        if !patterned && raw.iter().any(Option::is_some) {
1951            return Err(no_spelling(
1952                "a match mixing pattern clauses with positional cases",
1953            ));
1954        }
1955        let mut jobs = vec![Job::Text("match "), Job::Term(discriminant, Ctx::Operand)];
1956        if !(patterned && matches!(motive, Term::Hole)) {
1957            jobs.push(Job::Text(" return "));
1958            jobs.push(Job::Term(motive, Ctx::Operand));
1959        }
1960        jobs.push(Job::Text(" with"));
1961        if patterned {
1962            for clause in raw.into_iter().flatten() {
1963                let (pattern, wrapped) = clause;
1964                let mut binders = Vec::new();
1965                super::raw_match::candidates(pattern, &mut binders);
1966                let mut body = wrapped;
1967                for name in &binders {
1968                    match body {
1969                        Term::Lambda { param, param_type, body: inner }
1970                            if param == name && matches!(param_type.as_ref(), Term::Hole) =>
1971                        {
1972                            body = inner;
1973                        }
1974                        _ => {
1975                            return Err(no_spelling(format!(
1976                                "a pattern clause whose body does not bind its pattern variable `{name}`"
1977                            )))
1978                        }
1979                    }
1980                }
1981                for name in &binders {
1982                    if mentions_global(body, name) {
1983                        return Err(no_spelling(format!(
1984                            "the pattern variable `{name}` over a body naming the global `{name}` \u{2014} \
1985                             a pattern variable cannot be renamed, since the same word may name a \
1986                             constructor"
1987                        )));
1988                    }
1989                }
1990                jobs.push(Job::Text(" | "));
1991                jobs.push(Job::Owned(pattern_text(pattern, false)?));
1992                jobs.push(Job::Text(" => "));
1993                let spelled = binders
1994                    .iter()
1995                    .map(|n| Ok((n.clone(), token(n, "a pattern variable")?)))
1996                    .collect::<Result<Vec<_>, PrintError>>()?;
1997                let count = spelled.len();
1998                jobs.push(Job::Bind(spelled));
1999                jobs.push(Job::Term(body, Ctx::Top));
2000                jobs.push(Job::Unbind(count));
2001            }
2002        } else {
2003            for (i, case) in cases.iter().enumerate() {
2004                jobs.push(Job::Text(if i == 0 { " " } else { " | " }));
2005                jobs.push(Job::Term(case, Ctx::Top));
2006            }
2007        }
2008        jobs.push(Job::Text(" end"));
2009        Ok(jobs)
2010    }
2011}
2012
2013/// One atom of an application spine, followed by `next`.
2014///
2015/// ★ `Type` READS A LEVEL AFTER IT. `Type 3` is the sort `Type 3`, so the sort `Type` followed
2016/// by an argument that prints with a leading digit — `f Type 3n` — would read the argument's digits
2017/// as its level. That one atom is bracketed there; everywhere else `Type` stays bare.
2018fn push_spine_atom<'t>(jobs: &mut Vec<Job<'t>>, t: &'t Term, next: Option<&Term>) {
2019    if matches!(t, Term::Sort(Universe::Type(0))) && next.is_some_and(prints_with_digit) {
2020        jobs.push(Job::Text("(Type)"));
2021    } else {
2022        jobs.push(Job::Term(t, Ctx::Atom));
2023    }
2024}
2025
2026/// Does `t`, printed as a spine atom, begin with a decimal digit?
2027fn prints_with_digit(t: &Term) -> bool {
2028    matches!(t, Term::Lit(l) if !negative_literal(l) && !matches!(l, Literal::Text(_)))
2029}
2030
2031/// A pattern of a raw match clause, as the pattern grammar reads it.
2032fn pattern_text(p: &Term, atom: bool) -> Result<String, PrintError> {
2033    let name = |n: &str| -> Result<String, PrintError> {
2034        match Name::parse(n) {
2035            Some(parsed) if parsed.parts().len() > 1 => {
2036                let parts = parsed.parts();
2037                let mut out = token(&parts[0], "a pattern's constructor")?;
2038                for part in &parts[1..] {
2039                    out.push('.');
2040                    out.push_str(&tail_component(part)?);
2041                }
2042                Ok(out)
2043            }
2044            _ => token(n, "a pattern name"),
2045        }
2046    };
2047    match p {
2048        Term::Var(n) | Term::Global(n) => name(n),
2049        Term::Hole => Ok("_".to_string()),
2050        Term::Lit(Literal::Nat(n)) if n.to_string().parse::<u64>().is_ok() => Ok(n.to_string()),
2051        Term::App(..) => {
2052            let (head, args) = spine(p);
2053            let Term::Global(ctor) = head else {
2054                return Err(no_spelling(
2055                    "a pattern application whose head is not a constructor name",
2056                ));
2057            };
2058            let mut out = name(ctor)?;
2059            for arg in args {
2060                out.push(' ');
2061                out.push_str(&pattern_text(arg, true)?);
2062            }
2063            Ok(if atom { format!("({out})") } else { out })
2064        }
2065        other => Err(no_spelling(format!("the pattern `{other}`"))),
2066    }
2067}
2068
2069/// The head and arguments of an application spine.
2070fn spine(t: &Term) -> (&Term, Vec<&Term>) {
2071    let mut args = Vec::new();
2072    let mut head = t;
2073    while let Term::App(f, a) = head {
2074        args.push(a.as_ref());
2075        head = f;
2076    }
2077    args.reverse();
2078    (head, args)
2079}
2080
2081/// Which parser form an application spine is.
2082enum SpineForm {
2083    Plain,
2084    /// `(x := v)` — `⟨named⟩ x v`.
2085    Named,
2086    /// `⟨a, b⟩` — `⟨anon⟩ a b`.
2087    Anon,
2088    /// `b.f` — `⟨proj⟩ b f`, possibly applied further.
2089    Projection,
2090    /// `@f a b` — `⟨explicit⟩ f a b`.
2091    Explicit,
2092}
2093
2094fn spine_form(t: &Term) -> SpineForm {
2095    let (head, args) = spine(t);
2096    let Term::Global(h) = head else {
2097        return SpineForm::Plain;
2098    };
2099    if h == TermParser::NAMED_ARG_MARKER && args.len() == 2 && matches!(args[0], Term::Global(_)) {
2100        SpineForm::Named
2101    } else if h == crate::elaborate::ANON_CTOR_MARKER {
2102        SpineForm::Anon
2103    } else if h == crate::elaborate::DOT_MARKER
2104        && args.len() >= 2
2105        && matches!(args[1], Term::Global(_))
2106    {
2107        SpineForm::Projection
2108    } else if h == TermParser::EXPLICIT_ALL_MARKER && !args.is_empty() {
2109        SpineForm::Explicit
2110    } else {
2111        SpineForm::Plain
2112    }
2113}
2114
2115/// For an application carrying a LABEL (a named argument's name, a projection's field, an
2116/// auto-param's tactic), the sub-terms that are ordinary terms — `None` when it carries none.
2117fn label_positions(t: &Term) -> Option<Vec<&Term>> {
2118    let Term::App(f, x) = t else { return None };
2119    let Term::App(head, first) = f.as_ref() else {
2120        return None;
2121    };
2122    let Term::Global(m) = head.as_ref() else {
2123        return None;
2124    };
2125    if m == TermParser::NAMED_ARG_MARKER && matches!(first.as_ref(), Term::Global(_)) {
2126        Some(vec![x.as_ref()])
2127    } else if (m == crate::elaborate::DOT_MARKER || m == TermParser::AUTO_PARAM_MARKER)
2128        && matches!(x.as_ref(), Term::Global(_))
2129    {
2130        Some(vec![first.as_ref()])
2131    } else {
2132        None
2133    }
2134}
2135
2136fn is_marker(t: &Term, marker: &str) -> bool {
2137    matches!(t, Term::Global(n) if n == marker)
2138}
2139
2140/// Does `t` print starting with a name — what `@` requires right after it?
2141fn starts_with_name(t: &Term) -> bool {
2142    match t {
2143        Term::Var(_) | Term::Global(_) | Term::Const { .. } => true,
2144        Term::App(..) => {
2145            matches!(spine_form(t), SpineForm::Projection) && {
2146                let (_, args) = spine(t);
2147                starts_with_name(args[0])
2148            }
2149        }
2150        _ => false,
2151    }
2152}
2153
2154/// How a binder's argument is supplied, read off its type's marker.
2155#[derive(Clone, Copy, PartialEq, Eq)]
2156enum Kind {
2157    Explicit,
2158    Implicit,
2159    Strict,
2160    Instance,
2161}
2162
2163impl Kind {
2164    fn brackets(self) -> (&'static str, &'static str) {
2165        match self {
2166            Kind::Explicit => ("(", ")"),
2167            Kind::Implicit => ("{", "}"),
2168            Kind::Strict => ("\u{2983}", "\u{2984}"),
2169            Kind::Instance => ("[", "]"),
2170        }
2171    }
2172
2173    fn describe(self) -> &'static str {
2174        match self {
2175            Kind::Explicit => "explicit",
2176            Kind::Implicit => "implicit",
2177            Kind::Strict => "strict-implicit",
2178            Kind::Instance => "instance",
2179        }
2180    }
2181}
2182
2183fn split_kind(param_type: &Term) -> (Kind, &Term) {
2184    if let Term::App(f, declared) = param_type {
2185        if let Term::Global(m) = f.as_ref() {
2186            let kind = if m == TermParser::IMPLICIT_BINDER_MARKER {
2187                Some(Kind::Implicit)
2188            } else if m == TermParser::STRICT_IMPLICIT_BINDER_MARKER {
2189                Some(Kind::Strict)
2190            } else if m == TermParser::INSTANCE_BINDER_MARKER {
2191                Some(Kind::Instance)
2192            } else {
2193                None
2194            };
2195            if let Some(kind) = kind {
2196                return (kind, declared.as_ref());
2197            }
2198        }
2199    }
2200    (Kind::Explicit, param_type)
2201}
2202
2203enum Default<'t> {
2204    Value(&'t Term),
2205    Tactic(&'t str),
2206}
2207
2208/// A binder type's default value or auto-param tactic, and the declared type inside it.
2209fn split_default(t: &Term) -> Result<(&Term, Option<Default<'_>>), PrintError> {
2210    if let Term::App(f, payload) = t {
2211        if let Term::App(m, declared) = f.as_ref() {
2212            if let Term::Global(m) = m.as_ref() {
2213                if m == TermParser::PARAM_DEFAULT_MARKER {
2214                    return Ok((declared.as_ref(), Some(Default::Value(payload.as_ref()))));
2215                }
2216                if m == TermParser::AUTO_PARAM_MARKER {
2217                    return match payload.as_ref() {
2218                        Term::Global(tac) => Ok((declared.as_ref(), Some(Default::Tactic(tac)))),
2219                        other => Err(no_spelling(format!("the auto-param tactic `{other}`"))),
2220                    };
2221                }
2222            }
2223        }
2224    }
2225    Ok((t, None))
2226}
2227
2228/// Does `t` mention the global `name`, or a qualified global whose head is `name`?
2229fn mentions_global(t: &Term, name: &str) -> bool {
2230    let mut work = vec![t];
2231    while let Some(t) = work.pop() {
2232        match t {
2233            Term::Global(k) | Term::Const { name: k, .. } => {
2234                if k == name || Name::parse(k).is_some_and(|n| n.parts()[0] == name) {
2235                    return true;
2236                }
2237            }
2238            Term::Pi {
2239                param_type,
2240                body_type,
2241                ..
2242            } => {
2243                work.push(param_type);
2244                work.push(body_type);
2245            }
2246            Term::Lambda {
2247                param_type, body, ..
2248            } => {
2249                work.push(param_type);
2250                work.push(body);
2251            }
2252            Term::App(f, a) => {
2253                work.push(f);
2254                work.push(a);
2255            }
2256            Term::Match {
2257                discriminant,
2258                motive,
2259                cases,
2260            } => {
2261                work.push(discriminant);
2262                work.push(motive);
2263                work.extend(cases.iter());
2264            }
2265            Term::Fix { body, .. } => work.push(body),
2266            Term::MutualFix { defs, .. } => work.extend(defs.iter().map(|(_, b)| b)),
2267            Term::Let {
2268                ty, value, body, ..
2269            } => {
2270                work.push(ty);
2271                work.push(value);
2272                work.push(body);
2273            }
2274            Term::Annotated { term, .. } => work.push(term),
2275            _ => {}
2276        }
2277    }
2278    false
2279}
2280
2281/// Does `t` carry a binder-kind marker anywhere?
2282fn has_kind_marker(t: &Term) -> bool {
2283    let mut work = vec![t];
2284    while let Some(t) = work.pop() {
2285        match t {
2286            Term::Global(m)
2287                if m == TermParser::IMPLICIT_BINDER_MARKER
2288                    || m == TermParser::STRICT_IMPLICIT_BINDER_MARKER
2289                    || m == TermParser::INSTANCE_BINDER_MARKER =>
2290            {
2291                return true
2292            }
2293            Term::Pi {
2294                param_type,
2295                body_type,
2296                ..
2297            } => {
2298                work.push(param_type);
2299                work.push(body_type);
2300            }
2301            Term::Lambda {
2302                param_type, body, ..
2303            } => {
2304                work.push(param_type);
2305                work.push(body);
2306            }
2307            Term::App(f, a) => {
2308                work.push(f);
2309                work.push(a);
2310            }
2311            Term::Match {
2312                discriminant,
2313                motive,
2314                cases,
2315            } => {
2316                work.push(discriminant);
2317                work.push(motive);
2318                work.extend(cases.iter());
2319            }
2320            Term::Fix { body, .. } => work.push(body),
2321            Term::MutualFix { defs, .. } => work.extend(defs.iter().map(|(_, b)| b)),
2322            Term::Let {
2323                ty, value, body, ..
2324            } => {
2325                work.push(ty);
2326                work.push(value);
2327                work.push(body);
2328            }
2329            Term::Annotated { term, .. } => work.push(term),
2330            _ => {}
2331        }
2332    }
2333    false
2334}