catlog/tt/
text_elab.rs

1//! Elaboration from plain text for DoubleTT.
2
3use fnotation::*;
4use scopeguard::{ScopeGuard, guard};
5
6use fnotation::{ParseConfig, parser::Prec};
7use tattle::declare_error;
8
9use super::fiber_elab::{CODOMAIN_BINDER, FiberElab, FiberError, path_str};
10use super::{
11    context::*, eval::*, modelgen::*, prelude::*, stx::*, theory::*, toplevel::*, val::*, wd::*,
12};
13use crate::{
14    dbl::model::DblModelPrinter,
15    zero::{QualifiedName, name},
16};
17
18/// Parser config for DoubleTT.
19pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new(
20    &[
21        (":", Prec::nonassoc(20)),
22        (":=", Prec::nonassoc(10)),
23        ("&", Prec::lassoc(40)),
24        ("*", Prec::lassoc(60)),
25        ("==", Prec::nonassoc(30)),
26    ],
27    &[":", ":=", "&", "Unit", "Hom", "*", "=="],
28    &[
29        "model",
30        "def",
31        "instance",
32        "syn",
33        "chk",
34        "norm",
35        "generate",
36        "uwd",
37        "set_theory",
38    ],
39);
40
41/// The result of elaborating a top-level statement.
42pub enum TopElabResult {
43    /// A new declaration.
44    Declaration(TopVarName, TopDecl),
45    /// Output that should be logged.
46    Output(String),
47}
48
49/// Context for top-level elaboration.
50///
51/// Top-level elaboration is elaboration of declarations.
52pub struct TopElaborator {
53    current_theory: Option<Theory>,
54    reporter: Reporter,
55}
56
57impl TopElaborator {
58    /// Constructs a context for top-level elaboration.
59    pub fn new(reporter: Reporter) -> Self {
60        Self { current_theory: None, reporter }
61    }
62
63    fn bare_def<'c>(&self, n: &FNtn<'c>) -> Option<(TopVarName, &'c FNtn<'c>)> {
64        match n.ast0() {
65            App2(L(_, Keyword(":=")), L(_, Var(name)), tn) => {
66                Some((NameSegment::Text(ustr(name)), tn))
67            }
68            _ => None,
69        }
70    }
71
72    fn annotated_def<'c>(
73        &self,
74        n: &FNtn<'c>,
75    ) -> Option<(TopVarName, Option<&'c [&'c FNtn<'c>]>, &'c FNtn<'c>, &'c FNtn<'c>)> {
76        match n.ast0() {
77            App2(L(_, Keyword(":=")), L(_, App2(L(_, Keyword(":")), head_n, annotn)), valn) => {
78                match head_n.ast0() {
79                    App1(L(_, Var(name)), L(_, Tuple(args))) => {
80                        Some((name_seg(*name), Some(args.as_slice()), annotn, valn))
81                    }
82                    Var(name) => Some((name_seg(*name), None, annotn, valn)),
83                    _ => None,
84                }
85            }
86            _ => None,
87        }
88    }
89
90    fn expr_with_context<'c>(&self, n: &'c FNtn<'c>) -> (&'c [&'c FNtn<'c>], &'c FNtn<'c>) {
91        match n.ast0() {
92            App1(L(_, Tuple(ctx_elems)), n) => (ctx_elems.as_slice(), n),
93            _ => (&[], n),
94        }
95    }
96
97    fn get_theory(&self, loc: Loc) -> Option<Theory> {
98        let Some(theory) = &self.current_theory else {
99            return self.error(
100                loc,
101                "have not yet set a theory, set a theory via `set_theory <THEORY_NAME>`",
102            );
103        };
104        Some(theory.clone())
105    }
106
107    fn elaborator<'a>(&self, theory: &Theory, toplevel: &'a Toplevel) -> Elaborator<'a> {
108        Elaborator::new(theory.clone(), self.reporter.clone(), toplevel)
109    }
110
111    fn error<T>(&self, loc: Loc, msg: impl Into<String>) -> Option<T> {
112        self.reporter.error(loc, ELAB_ERROR, msg.into());
113        None
114    }
115
116    /// Elaborate a single top-level declaration.
117    pub fn elab(&mut self, toplevel: &Toplevel, tn: &FNtnTop) -> Option<TopElabResult> {
118        match tn.name {
119            "set_theory" => match tn.body.ast0() {
120                Var(theory_name) => match toplevel.theory_library.get(&name(*theory_name)) {
121                    Some(theory) => {
122                        self.current_theory = Some(theory.clone());
123                        Some(TopElabResult::Output(format!("set theory to {}", theory_name)))
124                    }
125                    None => self.error(tn.loc, format!("{theory_name} not found")),
126                },
127                _ => self.error(tn.loc, "expected a theory name"),
128            },
129            "model" => {
130                let theory = self.get_theory(tn.loc)?;
131                let (name, ty_n) = self.bare_def(tn.body).or_else(|| {
132                    self.error(
133                        tn.loc,
134                        "unknown syntax for model declaration, expected <name> := <model>",
135                    )
136                })?;
137                let (ty_s, ty_v) = self.elaborator(&theory, toplevel).ty(ty_n);
138                Some(TopElabResult::Declaration(
139                    name,
140                    TopDecl::Type(Type::new(theory.clone(), ty_s, ty_v)),
141                ))
142            }
143            "def" => {
144                let theory = self.get_theory(tn.loc)?;
145                let (name, args_n, ty_n, tm_n) = self.annotated_def(tn.body).or_else(|| {
146                    self.error(
147                        tn.loc,
148                        "unknown syntax for term declaration, expected <name> : <type> := <term>",
149                    )
150                })?;
151                match args_n {
152                    Some(args_n) => {
153                        let mut elab = self.elaborator(&theory, toplevel);
154                        let mut args_stx = IndexMap::new();
155                        for arg_n in args_n {
156                            let (name, label, ty_s, ty_v) = elab.binding(arg_n)?;
157                            args_stx.insert(name, (label, ty_s));
158                            elab.intro(name, label, Some(ty_v));
159                        }
160                        let (ret_ty_s, ret_ty_v) = elab.ty(ty_n);
161                        let (body_s, _) = elab.chk(&ret_ty_v, tm_n);
162                        Some(TopElabResult::Declaration(
163                            name,
164                            TopDecl::Def(Def::new(
165                                theory.clone(),
166                                args_stx.into(),
167                                ret_ty_s,
168                                body_s,
169                            )),
170                        ))
171                    }
172                    None => {
173                        let mut elab = self.elaborator(&theory, toplevel);
174                        let (ret_ty_s, ret_ty_v) = elab.ty(ty_n);
175                        let (body_s, _) = elab.chk(&ret_ty_v, tm_n);
176                        // A closed (empty-context) term: a tight transformation
177                        // S -> Unit. Unit is the empty record, i.e. the empty model.
178                        // A tight map into the empty model exists only when S is itself empty,
179                        // so the sole closed `def` is the identity on the empty
180                        // model, `tt : Unit`. Such a closed term is just a nullary
181                        // `Def` (empty argument context).
182                        Some(TopElabResult::Declaration(
183                            name,
184                            TopDecl::Def(Def::new(theory.clone(), Row::empty(), ret_ty_s, body_s)),
185                        ))
186                    }
187                }
188            }
189            "instance" => {
190                let theory = self.get_theory(tn.loc)?;
191                let (name, args_n, ty_n, tm_n) = self.annotated_def(tn.body).or_else(|| {
192                    self.error(
193                        tn.loc,
194                        "unknown syntax for instance declaration, expected <name> : <type> := [...]",
195                    )
196                })?;
197                if args_n.is_some() {
198                    return self.error(
199                        tn.loc,
200                        "an instance takes no arguments; for a parameterized map between \
201                         models, use `def`",
202                    );
203                }
204                let mut elab = self.elaborator(&theory, toplevel);
205                let (_, ret_ty_v) = elab.ty(ty_n);
206                // An instance body is checked against its codomain model, a
207                // record type.
208                let BaseTyV_::Record(r) = &*ret_ty_v else {
209                    return self
210                        .error(tn.loc, "an instance must be declared against a record type");
211                };
212                let (tm_s, tm_v) = elab.instance_body(r, tm_n);
213                Some(TopElabResult::Declaration(
214                    name,
215                    TopDecl::Instance(Instance::new(theory.clone(), tm_s, tm_v, ret_ty_v)),
216                ))
217            }
218            "syn" => {
219                let theory = self.get_theory(tn.loc)?;
220                let (ctx_ns, n) = self.expr_with_context(tn.body);
221                let mut elab = self.elaborator(&theory, toplevel);
222                for ctx_n in ctx_ns {
223                    let (name, label, _, ty_v) = elab.binding(ctx_n)?;
224                    elab.intro(name, label, Some(ty_v));
225                }
226                let (tm_s, _, ty_v) = elab.syn(n);
227                Some(TopElabResult::Output(format!(
228                    "{tm_s} : {}",
229                    elab.evaluator().quote_ty(&ty_v)
230                )))
231            }
232            "norm" => {
233                let (ctx_ns, n) = self.expr_with_context(tn.body);
234                // `norm [inst] <fiber-term>`: normalize a single instance
235                // term in the scope of the existing instance `inst`, showing
236                // its flat (composite) normal form. A fiber term is neutral
237                // at the type-theory level; the composition happens when it is
238                // extracted to a model-instance term (see
239                // [`normalize_instance_term`]).
240                if let [single] = ctx_ns
241                    && let Var(inst_name) = single.ast0()
242                    && let Some(TopDecl::Instance(inst)) =
243                        toplevel.declarations.get(&name_seg(*inst_name))
244                {
245                    let mut elab = self.elaborator(&inst.theory, toplevel);
246                    elab.enter_instance(inst)?;
247                    let (_, tm_v, ty_v) = elab.fiber_syn(n);
248                    let FiberTyV_::Over(over) = &*ty_v else {
249                        return elab
250                            .error("norm expects an instance element (a term over an object)");
251                    };
252                    let over = over.clone();
253                    return match normalize_instance_term(
254                        toplevel,
255                        &inst.theory.definition,
256                        inst,
257                        &tm_v,
258                        &over,
259                    ) {
260                        Ok(nt) => Some(TopElabResult::Output(nt.render())),
261                        Err(msg) => self.error(tn.loc, msg),
262                    };
263                }
264                let theory = self.get_theory(tn.loc)?;
265                let mut elab = self.elaborator(&theory, toplevel);
266                for ctx_n in ctx_ns {
267                    let (name, label, _, ty_v) = elab.binding(ctx_n)?;
268                    elab.intro(name, label, Some(ty_v));
269                }
270                let (_, tm_v, ty_v) = elab.syn(n);
271                let eval = elab.evaluator();
272                let tm_s = eval.quote_tm(&eval.eta(&tm_v, Some(&ty_v)));
273                Some(TopElabResult::Output(format!("{tm_s}")))
274            }
275            "chk" => {
276                let theory = self.get_theory(tn.loc)?;
277                let (ctx_ns, n) = self.expr_with_context(tn.body);
278                let mut elab = self.elaborator(&theory, toplevel);
279                for ctx_n in ctx_ns {
280                    let (name, label, _, ty_v) = elab.binding(ctx_n)?;
281                    elab.intro(name, label, Some(ty_v));
282                }
283                let (tm_n, ty_n) = match n.ast0() {
284                    App2(L(_, Keyword(":")), tm_n, ty_n) => (tm_n, ty_n),
285                    _ => return elab.error("expected <expr> : <type>"),
286                };
287                let (_, ty_v) = elab.ty(ty_n);
288                let (tm_s, _) = elab.chk(&ty_v, tm_n);
289                Some(TopElabResult::Output(format!("{tm_s}")))
290            }
291            "uwd" => {
292                let theory = self.get_theory(tn.loc)?;
293                let mut elab = self.elaborator(&theory, toplevel);
294                let (_, ty_v) = elab.ty(tn.body);
295                let Some(uwd) = record_to_uwd(&ty_v) else {
296                    return self.error(tn.loc, "expected a record type");
297                };
298                let out = uwd.to_doc().0.pretty(77).to_string().replace("\n", "\n#/ ");
299                Some(TopElabResult::Output(out))
300            }
301            "generate" => {
302                let theory = self.get_theory(tn.loc)?;
303                let mut elab = self.elaborator(&theory, toplevel);
304                let (_, ty_v) = elab.ty(tn.body);
305                let (model, ns) = Model::from_ty(toplevel, &theory.definition, &ty_v);
306                let printer = DblModelPrinter::new().include_summary(true);
307                let out = model.to_doc(&printer, &ns).0.pretty(77).to_string();
308                let out = out.trim().replace("\n", "\n#/ ");
309                Some(TopElabResult::Output(out))
310            }
311            _ => self.error(tn.loc, "unknown toplevel declaration"),
312        }
313    }
314}
315
316/// Text-based elaborator of types.
317pub struct Elaborator<'a> {
318    theory: Theory,
319    reporter: Reporter,
320    toplevel: &'a Toplevel,
321    loc: Option<Loc>,
322    ctx: Context,
323    next_meta: usize,
324}
325
326struct ElaboratorCheckpoint {
327    loc: Option<Loc>,
328    ctx: ContextCheckpoint,
329}
330
331declare_error!(ELAB_ERROR, "elab", "an error during elaboration");
332
333impl<'a> Elaborator<'a> {
334    /// Constructs a new elaborator.
335    pub fn new(theory: Theory, reporter: Reporter, toplevel: &'a Toplevel) -> Self {
336        Self {
337            theory,
338            reporter,
339            toplevel,
340            loc: None,
341            ctx: Context::new(),
342            next_meta: 0,
343        }
344    }
345
346    /// The codomain model of the instance body currently being
347    /// elaborated, if any. Its fields are the codomain's generators,
348    /// looked up by name by the instance-clause arms.
349    ///
350    /// The model is held as a record variable in the context under the
351    /// reserved [`CODOMAIN_BINDER`] name (see
352    /// [`Self::instance_body`]).
353    fn instance_codomain(&self) -> Option<Rc<RecordV>> {
354        let (_, _, ty) = self.ctx.lookup(name_seg(CODOMAIN_BINDER))?;
355        match &*ty? {
356            BaseTyV_::Record(r) => Some(Rc::new(r.clone())),
357            _ => None,
358        }
359    }
360
361    fn theory(&self) -> &TheoryDef {
362        &self.theory.definition
363    }
364
365    fn checkpoint(&self) -> ElaboratorCheckpoint {
366        ElaboratorCheckpoint {
367            loc: self.loc,
368            ctx: self.ctx.checkpoint(),
369        }
370    }
371
372    fn reset_to(&mut self, c: ElaboratorCheckpoint) {
373        self.loc = c.loc;
374        self.ctx.reset_to(c.ctx);
375    }
376
377    fn enter<'c>(&'c mut self, loc: Loc) -> ScopeGuard<&'c mut Self, impl FnOnce(&'c mut Self)> {
378        let c = self.checkpoint();
379        self.loc = Some(loc);
380        guard(self, |e| {
381            e.reset_to(c);
382        })
383    }
384
385    fn fresh_meta(&mut self) -> MetaVar {
386        let i = self.next_meta;
387        self.next_meta += 1;
388        MetaVar::new(None, i)
389    }
390
391    fn error<T>(&self, msg: impl Into<String>) -> Option<T> {
392        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into());
393        None
394    }
395
396    fn ty_hole(&mut self) -> (BaseTyS, BaseTyV) {
397        let ty_m = self.fresh_meta();
398        (BaseTyS::meta(ty_m), BaseTyV::meta(ty_m))
399    }
400
401    fn ty_error(&mut self, msg: impl Into<String>) -> (BaseTyS, BaseTyV) {
402        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into());
403        self.ty_hole()
404    }
405
406    fn syn_hole(&mut self) -> (BaseTmS, BaseTmV, BaseTyV) {
407        let tm_m = self.fresh_meta();
408        let ty_m = self.fresh_meta();
409        (BaseTmS::meta(tm_m), BaseTmV::meta(tm_m), BaseTyV::meta(ty_m))
410    }
411
412    fn syn_error(&mut self, msg: impl Into<String>) -> (BaseTmS, BaseTmV, BaseTyV) {
413        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into());
414        self.syn_hole()
415    }
416
417    fn chk_hole(&mut self) -> (BaseTmS, BaseTmV) {
418        let tm_m = self.fresh_meta();
419        (BaseTmS::meta(tm_m), BaseTmV::meta(tm_m))
420    }
421
422    fn chk_error(&mut self, msg: impl Into<String>) -> (BaseTmS, BaseTmV) {
423        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into());
424        self.chk_hole()
425    }
426
427    fn evaluator(&self) -> Evaluator<'a> {
428        Evaluator::new(self.toplevel, self.ctx.env.clone(), self.ctx.scope.len())
429    }
430
431    fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option<BaseTyV>) -> BaseTmV {
432        let v = BaseTmV::neu(
433            TmN::var(self.ctx.scope.len().into(), name, label),
434            ty.clone().unwrap_or(BaseTyV::empty_record()),
435        );
436        let v = if ty.is_some() {
437            self.evaluator().eta(&v, ty.as_ref())
438        } else {
439            v
440        };
441        self.ctx.env = self.ctx.env.snoc(v.clone());
442        self.ctx.push_scope(name, label, ty);
443        v
444    }
445
446    /// Report a text-surface error and return a synthesis hole. Errors
447    /// from the shared fiber machinery go through
448    /// [`FiberElab::report_fiber`] instead.
449    fn fiber_syn_error_msg(&mut self, msg: impl Into<String>) -> (FiberTmS, FiberTmV, FiberTyV) {
450        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into());
451        self.fiber_syn_hole()
452    }
453
454    /// Synthesize a fiber term and its fiber type. A fiber term is a
455    /// generator/import variable, a projection out of a sub-instance
456    /// (`we.e`), or a codomain-morphism application (`src(we.e)`).
457    fn fiber_syn(&mut self, n: &FNtn) -> (FiberTmS, FiberTmV, FiberTyV) {
458        let mut elab = self.enter(n.loc());
459        match n.ast0() {
460            Var(name) => match elab.lookup_fiber_tm(name_seg(*name)) {
461                Some(r) => r,
462                None => elab.fiber_syn_error(FiberError::UnknownElement(name.to_string())),
463            },
464            // Projection of a generator out of a sub-instance import: `we.e`.
465            App1(recv_n, L(_, Field(f))) => {
466                let (recv_s, recv_v, recv_ty) = elab.fiber_syn(recv_n);
467                elab.fiber_proj(recv_s, recv_v, &recv_ty, name_seg(*f))
468            }
469            // A theory object-operation on a fiber element, e.g.
470            // `@tensor [a, b]`. The resulting element lies over the
471            // operation applied to the argument's base object.
472            App1(L(_, Prim(op)), arg_n) => {
473                let op_name = name_seg(*op);
474                if !elab.check_ob_op(op_name) {
475                    return elab.fiber_syn_hole();
476                }
477                let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n);
478                elab.fiber_ob_app(op_name, arg_s, arg_v, &arg_ty)
479            }
480            // Codomain-morphism application `f(arg)`. The morphism `f` may
481            // be a nested path into the codomain (e.g. `Add.op`), so its
482            // head is a projection chain, not just a bare variable.
483            App1(head_n, arg_n) => {
484                let Some(path) = morphism_path(head_n) else {
485                    return elab.fiber_syn_error_msg(
486                        "expected a codomain morphism (a name or path like `Add.op`) applied \
487                         to a fiber element",
488                    );
489                };
490                // A display label for the argument, used only in errors.
491                let label = match arg_n.ast0() {
492                    Var(x) => x.to_string(),
493                    App1(_, L(_, Field(fld))) => fld.to_string(),
494                    _ => "argument".to_string(),
495                };
496                let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n);
497                elab.apply_codomain_morphism(&path, arg_s, arg_v, arg_ty, &label)
498            }
499            // A fiber list literal `[a, b, ...]` (the argument of a
500            // multi-ary morphism); its object is the list of the elements'
501            // objects.
502            Tuple(elems) => {
503                let mut ss = Vec::with_capacity(elems.len());
504                let mut vs = Vec::with_capacity(elems.len());
505                let mut objs = Vec::with_capacity(elems.len());
506                for e in elems.iter() {
507                    let (s, v, ty) = elab.fiber_syn(e);
508                    let FiberTyV_::Over(o) = &*ty else {
509                        return elab.fiber_syn_error(FiberError::ListElementNotOver);
510                    };
511                    objs.push(o.clone());
512                    ss.push(s);
513                    vs.push(v);
514                }
515                (FiberTmS::list(ss), FiberTmV::list(vs), FiberTyV::over(BaseTmV::list(objs)))
516            }
517            _ => elab.fiber_syn_error_msg(
518                "expected a fiber element: a generator, a projection `we.e`, a fiber list \
519                 `[..]`, an object operation `@op [..]`, or a morphism application `f[..]`",
520            ),
521        }
522    }
523
524    /// Check a fiber term against an expected fiber type. Fiber terms are
525    /// all synthesizing, so this synthesizes and checks convertibility.
526    fn fiber_chk(&mut self, expected: &FiberTyV, n: &FNtn) -> (FiberTmS, FiberTmV) {
527        let syn = self.fiber_syn(n);
528        self.check_fiber(syn, expected)
529    }
530
531    /// Elaborate a fiber-type annotation. Used for sub-instance imports
532    /// (`we : Edge`, where `Edge` names a top-level instance) and anonymous
533    /// equations (`name : (a == b)`).
534    fn fiber_ty(&mut self, n: &FNtn) -> Option<(FiberTyS, FiberTyV)> {
535        match n.ast0() {
536            Var(name) => {
537                let topvar = name_seg(*name);
538                let (imported_codomain, val) = match self.toplevel.declarations.get(&topvar) {
539                    Some(TopDecl::Instance(i)) => (i.codomain.clone(), i.val.clone()),
540                    _ => {
541                        return self.error(format!(
542                            "{name} must reference a top-level instance declaration"
543                        ));
544                    }
545                };
546                // The imported instance must be an instance of the *same*
547                // model as the enclosing one — otherwise its `Over` paths
548                // refer to objects foreign to this codomain, producing a
549                // malformed instance.
550                if let Some(cod) = self.instance_codomain() {
551                    let enclosing = BaseTyV::record((*cod).clone());
552                    if !self.codomains_match(&enclosing, &imported_codomain) {
553                        self.report_fiber(FiberError::ImportCodomainMismatch(name.to_string()));
554                        return None;
555                    }
556                }
557                // The syntax keeps the instance's name (for display); the
558                // value is the referenced instance's resolved record, just
559                // as a base top-var evaluates to its model. See
560                // [`FiberTyS_::TopVar`].
561                Some((FiberTyS::topvar(topvar), val))
562            }
563            App2(L(_, Keyword("==")), a_n, b_n) => {
564                let (a_s, a_v, a_ty) = self.fiber_syn(a_n);
565                let (b_s, b_v, b_ty) = self.fiber_syn(b_n);
566                if let Err(e) = self.evaluator().convertible_fiber_ty(&a_ty, &b_ty) {
567                    self.report_fiber(FiberError::InconvertibleEquationSides(
568                        e.pretty().to_string(),
569                    ));
570                    return None;
571                }
572                let FiberTyV_::Over(obj) = &*a_ty else {
573                    self.report_fiber(FiberError::EquationNotOver);
574                    return None;
575                };
576                Some(self.fiber_id_field(&a_ty, obj, a_s, a_v, b_s, b_v))
577            }
578            _ => self.error("expected an instance name or an equation `a == b`"),
579        }
580    }
581
582    /// The unit type, elaborated as the empty record — i.e. the empty
583    /// model. `Unit` and `tt` are surface sugar for the empty record type
584    /// and its unique element, the empty cons `[]`.
585    fn empty_record_ty(&self) -> (BaseTyS, BaseTyV) {
586        (BaseTyS::record(Row::empty()), BaseTyV::empty_record())
587    }
588
589    /// The value of the codomain `self` binding — the eta-expanded model
590    /// record. Codomain object values (`self.V`, morphism dom/cod) are
591    /// obtained by projecting / evaluating field types against it, so that
592    /// every codomain object is rooted at the same `self` neutral and thus
593    /// compares equal under [`Evaluator::equal_tm`].
594    fn codomain_self_value(&self) -> Option<BaseTmV> {
595        let (i, _, _) = self.ctx.lookup(name_seg(CODOMAIN_BINDER))?;
596        self.ctx.env.get(*i).cloned()
597    }
598
599    /// The codomain object `self.<field>` (a base object value), for a
600    /// generator declared over the object-typed codomain field `field`.
601    fn codomain_object(&self, field: FieldName, label: LabelSegment) -> Option<BaseTmV> {
602        Some(self.evaluator().proj(&self.codomain_self_value()?, field, label))
603    }
604
605    /// Apply a codomain morphism `f` to an already-elaborated fiber
606    /// argument. The argument's `Over` object must equal the morphism's
607    /// domain object (compared as base objects, so modal domains — lists,
608    /// tensors — need no special handling); the result lies over the
609    /// morphism's codomain object.
610    fn apply_codomain_morphism(
611        &mut self,
612        path: &[(FieldName, LabelSegment)],
613        arg_s: FiberTmS,
614        arg_v: FiberTmV,
615        arg_ty: FiberTyV,
616        arg_label_str: &str,
617    ) -> (FiberTmS, FiberTmV, FiberTyV) {
618        let Some(codomain) = self.instance_codomain() else {
619            return self.fiber_syn_error_msg(
620                "applied codomain morphism is only allowed inside an instance body",
621            );
622        };
623        let FiberTyV_::Over(arg_obj) = &*arg_ty else {
624            return self
625                .fiber_syn_error(FiberError::ArgNotElement(Some(arg_label_str.to_string())));
626        };
627        let arg_obj = arg_obj.clone();
628        let Some(self_val) = self.codomain_self_value() else {
629            return self.fiber_syn_error_msg(
630                "applied codomain morphism is only allowed inside an instance body",
631            );
632        };
633        // Resolve the morphism's type by walking its (possibly nested)
634        // path into the codomain model, e.g. `Add.op`.
635        let record_ty = BaseTyV::record((*codomain).clone());
636        let mor_ty = match self.evaluator().path_ty(&record_ty, &self_val, path) {
637            Ok(ty) => ty,
638            Err(e) => {
639                return self.fiber_syn_error_msg(format!(
640                    "no such codomain morphism {}: {e}",
641                    path_str(path)
642                ));
643            }
644        };
645        self.fiber_mor_app(path, &mor_ty, arg_s, arg_v, &arg_obj)
646    }
647
648    /// Elaborate an instance body — a tuple of `name : type`, `field
649    /// := [names]`, and `mor(arg) := target` clauses — against the
650    /// enclosing codomain model. Produces the instance as a fiber
651    /// [`Record`](FiberTyS_::Record): generators become
652    /// [`Over`](FiberTyS_::Over) fields, sub-instance imports nested
653    /// [`Record`](FiberTyS_::Record) fields, and equations
654    /// [`Id`](FiberTyS_::Id) fields.
655    ///
656    /// The codomain model is bound into the *base* context as a `self`-typed
657    /// record variable (and the binding is dropped on exit) so that
658    /// applied-codomain-morphism syntax resolves morphisms by name. The
659    /// instance's own generators and imports live in the separate *fiber*
660    /// scope.
661    fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (FiberTyS, FiberTyV) {
662        let c = self.checkpoint();
663        let binder = name_seg(CODOMAIN_BINDER);
664        self.intro(binder, label_seg(CODOMAIN_BINDER), Some(BaseTyV::record(codomain.clone())));
665        let result = self.instance_body_inner(n);
666        self.reset_to(c);
667        result
668    }
669
670    /// Re-establish the scope of an already-elaborated instance so a fresh
671    /// term can be elaborated against it (see the `norm [inst] <term>`
672    /// command): bind the codomain model as `self` — so codomain-morphism
673    /// syntax like `t(..)` resolves — and introduce each generator and
674    /// sub-instance import into the fiber scope by its original name.
675    fn enter_instance(&mut self, inst: &Instance) -> Option<()> {
676        self.intro(
677            name_seg(CODOMAIN_BINDER),
678            label_seg(CODOMAIN_BINDER),
679            Some(inst.codomain.clone()),
680        );
681        let FiberTyV_::Record(fields) = &*inst.val else {
682            return self.error("instance value is not a fiber record");
683        };
684        for (name, (label, field_ty)) in fields.iter() {
685            match &**field_ty {
686                // Generators (`Over`) and sub-instance imports (`Record`)
687                // become fiber-scope bindings; projections into an import
688                // resolve against the record type. Equations (`Id`) are not
689                // in scope as terms.
690                FiberTyV_::Over(_) | FiberTyV_::Record(_) => {
691                    self.intro_fiber(*name, *label, field_ty.clone());
692                }
693                FiberTyV_::Id(_, _, _) => {}
694            }
695        }
696        Some(())
697    }
698
699    /// Elaborate the clauses of an instance body (the f-notation `n`) into a
700    /// fiber [`Record`](FiberTyS_::Record). The codomain is already set on
701    /// the context by [`Self::instance_body`].
702    ///
703    /// Steps:
704    /// 1. Set up empty accumulators (see below) for the clauses to fill.
705    /// 2. Walk each clause, dispatching on its surface shape into one of
706    ///    the forms below. A malformed clause reports an error and sets
707    ///    `failed`, but the walk continues so a single pass surfaces as
708    ///    many errors as possible.
709    /// 3. If any clause failed, return an empty instance (errors already
710    ///    reported); otherwise assemble the accumulators into the paired
711    ///    instance terms.
712    ///
713    /// The clause forms, in match order:
714    /// - `name : type` — dispatched on the *elaborated type's* shape: a
715    ///   fiber type `Over(p)` declares a generator; a record type is a
716    ///   sub-instance import (must name a top-level instance def); an
717    ///   identity type `a == b` is an anonymous equation.
718    /// - `field := [k := t, ...]` — mapping-literal: sugar for a batch of
719    ///   per-key equations `field(k) := t` against a codomain *morphism*.
720    /// - `field := [n1, n2, ...]` — set-literal: declares generators in
721    ///   the fiber over a codomain *object* `field`.
722    /// - `mor(arg) := target` — a single equation witness.
723    fn instance_body_inner(&mut self, n: &FNtn) -> (FiberTyS, FiberTyV) {
724        let mut elab = self.enter(n.loc());
725        let empty = || (FiberTyS::record(Row::empty()), FiberTyV::record(Row::empty()));
726        let Tuple(field_ns) = n.ast0() else {
727            elab.error::<()>("expected a tuple instance body");
728            return empty();
729        };
730        // The instance is assembled as a fiber record: a generator is an
731        // `Over` field, a sub-instance import a nested `Record` field, and
732        // an equation an `Id` field (with a synthetic `_eqN` name).
733        // `fields_s`/`fields_v` hold the syntactic / value rows; `eq_count`
734        // names successive equation fields.
735        let mut fields_s: Row<FiberTyS> = Row::empty();
736        let mut fields_v: Row<FiberTyV> = Row::empty();
737        let mut eq_count = 0usize;
738        let mut failed = false;
739
740        for field_n in field_ns.iter() {
741            elab.loc = Some(field_n.loc());
742            match field_n.ast0() {
743                // `name : type` — a sub-instance import (`we : Edge`) or an
744                // anonymous equation (`name : (a == b)`), dispatched on the
745                // elaborated fiber type's shape.
746                App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => {
747                    let n_seg = name_seg(*name);
748                    let label = label_seg(*name);
749                    let Some((ty_s, ty_v)) = elab.fiber_ty(ty_n) else {
750                        failed = true;
751                        continue;
752                    };
753                    match &*ty_v {
754                        // A sub-instance import: bind it in the fiber scope
755                        // (so `name.gen` projections resolve) and record it.
756                        FiberTyV_::Record(_) => {
757                            elab.intro_fiber(n_seg, label, ty_v.clone());
758                            fields_s.insert(n_seg, label, ty_s);
759                            fields_v.insert(n_seg, label, ty_v);
760                        }
761                        // A named equation (e.g. `eq : (.src(e) == .src(f))`).
762                        FiberTyV_::Id(_, _, _) => {
763                            fields_s.insert(n_seg, label, ty_s);
764                            fields_v.insert(n_seg, label, ty_v);
765                        }
766                        FiberTyV_::Over(_) => {
767                            elab.error::<()>(format!(
768                                "instance clause {name} cannot be annotated with a bare \
769                                 element type",
770                            ));
771                            failed = true;
772                        }
773                    }
774                }
775                // `field := [k1 := t1, ...]` — mapping-literal: a batch of
776                // per-key equations against a morphism-typed codomain field.
777                App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(entries)))
778                    if !entries.is_empty()
779                        && entries
780                            .iter()
781                            .all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) =>
782                {
783                    let Some(codomain) = elab.instance_codomain() else {
784                        elab.error::<()>(
785                            "mapping-literal assignment is only allowed inside an instance body",
786                        );
787                        failed = true;
788                        continue;
789                    };
790                    let f_seg = name_seg(*field_name);
791                    if !codomain.fields.has(f_seg) {
792                        elab.error::<()>(format!("no such codomain field {field_name}"));
793                        failed = true;
794                        continue;
795                    }
796                    // Each `key := target` entry is the equation
797                    // `field(key) == target`: apply the codomain morphism
798                    // to the key (which also checks the key's object against
799                    // the morphism's domain and yields the codomain object),
800                    // then equate the result to the target.
801                    let mut entry_failed = false;
802                    for entry_n in entries.iter() {
803                        elab.loc = Some(entry_n.loc());
804                        let App2(L(_, Keyword(":=")), key_n, target_n) = entry_n.ast0() else {
805                            unreachable!("guard ensured all entries are `:=` clauses");
806                        };
807                        let (key_s, key_v, key_ty) = elab.fiber_syn(key_n);
808                        let label = format!("{field_name} key");
809                        let mor_path = vec![(name_seg(*field_name), label_seg(*field_name))];
810                        let (lhs_s, lhs_v, lhs_ty) =
811                            elab.apply_codomain_morphism(&mor_path, key_s, key_v, key_ty, &label);
812                        let FiberTyV_::Over(cod_obj) = &*lhs_ty else {
813                            entry_failed = true;
814                            break;
815                        };
816                        let cod_obj = cod_obj.clone();
817                        let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, target_n);
818                        let (id_s, id_v) =
819                            elab.fiber_id_field(&lhs_ty, &cod_obj, lhs_s, lhs_v, rhs_s, rhs_v);
820                        let (eqn, eql) = next_eq_field(&mut eq_count);
821                        fields_s.insert(eqn, eql, id_s);
822                        fields_v.insert(eqn, eql, id_v);
823                    }
824                    if entry_failed {
825                        failed = true;
826                        continue;
827                    }
828                }
829                // `field := [n1, n2, ...]` — set-literal: declare generators
830                // in the fiber over an object-typed codomain field.
831                App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(name_ns))) => {
832                    let Some(codomain) = elab.instance_codomain() else {
833                        elab.error::<()>(
834                            "set-literal field assignment is only allowed inside an \
835                             instance body",
836                        );
837                        failed = true;
838                        continue;
839                    };
840                    let f_seg = name_seg(*field_name);
841                    let f_label = label_seg(*field_name);
842                    let Some(field_ty_s) = codomain.fields.get(f_seg) else {
843                        elab.error::<()>(format!("no such codomain field {field_name}"));
844                        failed = true;
845                        continue;
846                    };
847                    if !matches!(&**field_ty_s, BaseTyS_::Object(_)) {
848                        elab.error::<()>(format!(
849                            "set-literal assignment requires field {field_name} to be \
850                             object-typed",
851                        ));
852                        failed = true;
853                        continue;
854                    }
855                    // Generators lie over the codomain object `self.<field>`.
856                    let Some(gen_obj_v) = elab.codomain_object(f_seg, f_label) else {
857                        elab.error::<()>(
858                            "set-literal field assignment is only allowed inside an \
859                             instance body",
860                        );
861                        failed = true;
862                        continue;
863                    };
864                    let gen_obj_s = elab.evaluator().quote_tm(&gen_obj_v);
865                    for name_n in name_ns.iter() {
866                        let Var(gen_name) = name_n.ast0() else {
867                            elab.loc = Some(name_n.loc());
868                            elab.error::<()>("set-literal entries must be bare names");
869                            failed = true;
870                            break;
871                        };
872                        let gen_seg = name_seg(*gen_name);
873                        let gen_label = label_seg(*gen_name);
874                        elab.intro_fiber(gen_seg, gen_label, FiberTyV::over(gen_obj_v.clone()));
875                        fields_s.insert(gen_seg, gen_label, FiberTyS::over(gen_obj_s.clone()));
876                        fields_v.insert(gen_seg, gen_label, FiberTyV::over(gen_obj_v.clone()));
877                    }
878                }
879                // `mor(arg) := target` — a single equation witness.
880                App2(L(_, Keyword(":=")), lhs_n, rhs_n) => {
881                    let (lhs_s, lhs_v, lhs_ty) = elab.fiber_syn(lhs_n);
882                    let FiberTyV_::Over(obj) = &*lhs_ty else {
883                        elab.loc = Some(lhs_n.loc());
884                        elab.report_fiber(FiberError::MappingLhsNotOver);
885                        failed = true;
886                        continue;
887                    };
888                    let obj = obj.clone();
889                    let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, rhs_n);
890                    let (id_s, id_v) =
891                        elab.fiber_id_field(&lhs_ty, &obj, lhs_s, lhs_v, rhs_s, rhs_v);
892                    let (eqn, eql) = next_eq_field(&mut eq_count);
893                    fields_s.insert(eqn, eql, id_s);
894                    fields_v.insert(eqn, eql, id_v);
895                }
896                _ => {
897                    elab.error::<()>(
898                        "expected fields in the form `name : type`, \
899                         `field := [names]`, or `mor(arg) := target`",
900                    );
901                    failed = true;
902                }
903            }
904        }
905
906        // On any failure, errors are already reported, so bail with an
907        // empty instance rather than a half-built one.
908        if failed {
909            return empty();
910        }
911        (FiberTyS::record(fields_s), FiberTyV::record(fields_v))
912    }
913
914    fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, BaseTyS, BaseTyV)> {
915        let mut elab = self.enter(n.loc());
916        match n.ast0() {
917            App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => {
918                let (ty_s, ty_v) = elab.ty(ty_n);
919                Some((name_seg(*name), label_seg(*name), ty_s, ty_v))
920            }
921            _ => elab.error("unexpected notation for binding"),
922        }
923    }
924
925    fn lookup_ty(&mut self, name: VarName) -> (BaseTyS, BaseTyV) {
926        let qname = QualifiedName::single(name);
927        if let Some(ob_type) = self.theory().basic_ob_type(qname) {
928            (BaseTyS::object(ob_type.clone()), BaseTyV::object(ob_type))
929        } else if let Some(d) = self.toplevel.declarations.get(&name) {
930            match d {
931                TopDecl::Type(t) => {
932                    if t.theory == self.theory {
933                        (BaseTyS::topvar(name), t.val.clone())
934                    } else {
935                        self.ty_error(format!(
936                            "{name} refers to a type in theory {}, expected a type in theory {}",
937                            t.theory, self.theory
938                        ))
939                    }
940                }
941                // An instance is a fiber type, not a base type. It can only
942                // appear as the annotation of a sub-instance import inside an
943                // instance body (handled by `fiber_ty`), not in base-type
944                // position.
945                TopDecl::Instance(_) => self.ty_error(format!(
946                    "{name} refers to an instance, which is not a base type; \
947                     an instance can only be imported inside another instance body"
948                )),
949                TopDecl::Def(_) => self.ty_error(format!("{name} refers to a term not a type")),
950            }
951        } else {
952            self.ty_error(format!("no such type {name} defined"))
953        }
954    }
955    fn morphism_ty(&mut self, n: &FNtn) -> Option<(MorType, ObType, ObType)> {
956        let elab = self.enter(n.loc());
957        let theory = elab.theory();
958        match n.ast0() {
959            App1(L(_, Keyword("Hom")), L(_, Var(name))) => {
960                let qname = QualifiedName::single(name_seg(*name));
961                if let Some(ob_type) = theory.basic_ob_type(qname) {
962                    if let Some(hom_type) = theory.hom_type(ob_type.clone()) {
963                        Some((hom_type, ob_type.clone(), ob_type))
964                    } else {
965                        elab.error(format!("object type {name} does not have hom type"))
966                    }
967                } else {
968                    elab.error(format!("no such object type {name}"))
969                }
970            }
971            Var(name) => {
972                let qname = QualifiedName::single(name_seg(*name));
973                if let Some(mor_type) = theory.basic_mor_type(qname) {
974                    let dom = theory.src_type(&mor_type);
975                    let cod = theory.tgt_type(&mor_type);
976                    Some((mor_type, dom, cod))
977                } else {
978                    elab.error(format!("no such morphism type {name}"))
979                }
980            }
981            _ => elab.error("unexpected notation for morphism type"),
982        }
983    }
984
985    fn path(&mut self, n: &FNtn) -> Option<Vec<(NameSegment, LabelSegment)>> {
986        let mut elab = self.enter(n.loc());
987        match n.ast0() {
988            Field(f) => Some(vec![(name_seg(*f), label_seg(*f))]),
989            App1(p_n, L(_, Field(f))) => {
990                let mut p = elab.path(p_n)?;
991                p.push((name_seg(*f), label_seg(*f)));
992                Some(p)
993            }
994            _ => elab.error("unexpected notation for path"),
995        }
996    }
997
998    #[allow(clippy::type_complexity)]
999    fn specialization(
1000        &mut self,
1001        n: &FNtn,
1002    ) -> Option<(Vec<(NameSegment, LabelSegment)>, BaseTyS, BaseTyV)> {
1003        let mut elab = self.enter(n.loc());
1004        match n.ast0() {
1005            App2(L(_, Keyword(":")), p_n, ty_n) => {
1006                let p = elab.path(p_n)?;
1007                let (ty_s, ty_v) = elab.ty(ty_n);
1008                Some((p, ty_s, ty_v))
1009            }
1010            App2(L(_, Keyword(":=")), p_n, tm_n) => {
1011                let p = elab.path(p_n)?;
1012                let (tm_s, tm_v, ty_v) = elab.syn(tm_n);
1013                Some((
1014                    p,
1015                    BaseTyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s),
1016                    BaseTyV::sing(ty_v, tm_v),
1017                ))
1018            }
1019            _ => elab.error("unexpected notation for specialization"),
1020        }
1021    }
1022
1023    /// Elaborates a type from notation, returning both syntax and value.
1024    pub fn ty(&mut self, n: &FNtn) -> (BaseTyS, BaseTyV) {
1025        let mut elab = self.enter(n.loc());
1026        match n.ast0() {
1027            Var(name) => elab.lookup_ty(name_seg(*name)),
1028            Keyword("Unit") => elab.empty_record_ty(),
1029            App1(L(_, Prim("sing")), tm_n) => {
1030                let (tm_s, tm_v, ty_v) = elab.syn(tm_n);
1031                (BaseTyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), BaseTyV::sing(ty_v, tm_v))
1032            }
1033            App1(mt_n, L(_, Tuple(domcod_n))) => {
1034                let [dom_n, cod_n] = domcod_n.as_slice() else {
1035                    return elab.ty_error("expected two arguments for morphism type");
1036                };
1037                let Some((mt, dom_ty, cod_ty)) = elab.morphism_ty(mt_n) else {
1038                    return elab.ty_hole();
1039                };
1040                let (dom_s, dom_v) = elab.chk(&BaseTyV::object(dom_ty.clone()), dom_n);
1041                let (cod_s, cod_v) = elab.chk(&BaseTyV::object(cod_ty.clone()), cod_n);
1042                (
1043                    BaseTyS::morphism(mt.clone(), dom_s, cod_s),
1044                    BaseTyV::morphism(mt.clone(), dom_v, cod_v),
1045                )
1046            }
1047            Tuple(field_ns) => {
1048                let mut field_ty_vs = Vec::<(FieldName, (LabelSegment, BaseTyV))>::new();
1049                let mut failed = false;
1050                let self_var = elab.intro(name_seg("self"), label_seg("self"), None).unwrap_neu();
1051                let c = elab.checkpoint();
1052                for field_n in field_ns.iter() {
1053                    elab.loc = Some(field_n.loc());
1054                    let Some((name, label, ty_n)) = (match field_n.ast0() {
1055                        App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => {
1056                            let name_seg = name_seg(*name);
1057                            Some((name_seg, label_seg(*name), ty_n))
1058                        }
1059                        _ => elab.error("expected fields in the form <name> : <type>"),
1060                    }) else {
1061                        failed = true;
1062                        continue;
1063                    };
1064                    let (_, ty_v) = elab.ty(ty_n);
1065                    field_ty_vs.push((name, (label, ty_v.clone())));
1066                    elab.ctx.push_scope(name, label, Some(ty_v.clone()));
1067                    elab.ctx.env = elab
1068                        .ctx
1069                        .env
1070                        .snoc(BaseTmV::neu(TmN::proj(self_var.clone(), name, label), ty_v));
1071                }
1072                if failed {
1073                    return elab.ty_hole();
1074                }
1075                elab.reset_to(c);
1076                let field_tys: Row<_> = field_ty_vs
1077                    .iter()
1078                    .map(|(name, (label, ty_v))| (*name, (*label, elab.evaluator().quote_ty(ty_v))))
1079                    .collect();
1080                let r_v = RecordV::new(elab.ctx.env.clone(), field_tys.clone(), Dtry::empty());
1081                (BaseTyS::record(field_tys), BaseTyV::record(r_v))
1082            }
1083            App2(L(_, Keyword("&")), ty_n, L(_, Tuple(specialization_ns))) => {
1084                let (ty_s, mut ty_v) = elab.ty(ty_n);
1085                let mut specializations = Vec::new();
1086                // Approach:
1087                //
1088                // 1. Write a try_specialize method which attempts to specialize ty_v
1089                // with a given path + type (e.g. `.x.y : @sing a`), returning a new
1090                // type or an error message.
1091                // 2. Iteratively apply try_specialize to each specialization in turn.
1092                for specialization_n in specialization_ns.iter() {
1093                    elab.loc = Some(specialization_n.loc());
1094                    let Some((path, sty_s, sty_v)) = elab.specialization(specialization_n) else {
1095                        return elab.ty_hole();
1096                    };
1097                    match elab.evaluator().try_specialize(&ty_v, &path, sty_v) {
1098                        Ok(specialized) => {
1099                            ty_v = specialized;
1100                            specializations.push((path, sty_s));
1101                        }
1102                        Err(s) => {
1103                            return elab
1104                                .ty_error(format!("Failed to specialize:\n... because {s}"));
1105                        }
1106                    }
1107                }
1108                (BaseTyS::specialize(ty_s, specializations), ty_v)
1109            }
1110            App2(L(_, Keyword("==")), tm1_n, tm2_n) => {
1111                let (tm1_s, tm1_v, tm1_ty) = elab.syn(tm1_n);
1112                let (tm2_s, tm2_v, tm2_ty) = elab.syn(tm2_n);
1113                if !matches!(&*tm1_ty, BaseTyV_::Morphism(_, _, _)) {
1114                    elab.loc = Some(tm1_n.loc());
1115                    return elab.ty_error(
1116                        "Equality types are only supported for morphisms; equations \
1117                         between instance elements live inside an instance body",
1118                    );
1119                }
1120                if let Err(e) = elab.evaluator().convertible_ty(&tm1_ty, &tm2_ty) {
1121                    let eval = elab.evaluator();
1122                    return elab.ty_error(format!(
1123                        "types {} and {} are not convertible:\n{}",
1124                        eval.quote_ty(&tm1_ty),
1125                        eval.quote_ty(&tm2_ty),
1126                        e.pretty()
1127                    ));
1128                }
1129                let eq_ty_s = BaseTyS::id(elab.evaluator().quote_ty(&tm1_ty), tm1_s, tm2_s);
1130                let eq_ty_v = BaseTyV::id(tm1_ty, tm1_v, tm2_v);
1131                (eq_ty_s, eq_ty_v)
1132            }
1133            _ => elab.ty_error("unexpected notation for type"),
1134        }
1135    }
1136
1137    fn lookup_tm(&mut self, name: Ustr) -> (BaseTmS, BaseTmV, BaseTyV) {
1138        let label = label_seg(name);
1139        let name = name_seg(name);
1140        if let Some((i, _, ty)) = self.ctx.lookup(name) {
1141            (
1142                BaseTmS::var(i, name, label),
1143                self.ctx.env.get(*i).unwrap().clone(),
1144                ty.clone().unwrap(),
1145            )
1146        } else if let Some(d) = self.toplevel.lookup(name) {
1147            match d {
1148                TopDecl::Type(_) => self.syn_error(format!("{name} refers type, not term")),
1149                // A nullary `Def` (a closed term, e.g. `tt : Unit`) used as a
1150                // bare name; evaluate its body and return type in the empty
1151                // context.
1152                TopDecl::Def(d) if d.args.is_empty() => {
1153                    let def = d.clone();
1154                    let eval = self.evaluator();
1155                    (
1156                        BaseTmS::topapp(name, vec![]),
1157                        eval.eval_tm(&def.body),
1158                        eval.eval_ty(&def.ret_ty),
1159                    )
1160                }
1161                TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")),
1162                TopDecl::Instance(_) => self.syn_error(format!(
1163                    "{name} refers to an instance; use it in type position to import it, \
1164                     not as a term"
1165                )),
1166            }
1167        } else {
1168            self.syn_error(format!("no such variable {name}"))
1169        }
1170    }
1171
1172    /// Elaborates a term from notation, returning syntax, value, and synthesized type.
1173    fn syn(&mut self, n: &FNtn) -> (BaseTmS, BaseTmV, BaseTyV) {
1174        let mut elab = self.enter(n.loc());
1175        match n.ast0() {
1176            Var(name) => elab.lookup_tm(ustr(name)),
1177            App1(tm_n, L(_, Field(f))) => {
1178                // A top-level instance has no term-position use, so projecting
1179                // a field out of one would otherwise produce a confusing
1180                // "not a term"/"not a record" cascade; catch it here with the
1181                // targeted elimination message.
1182                if let Var(inst) = tm_n.ast0()
1183                    && matches!(
1184                        elab.toplevel.declarations.get(&name_seg(*inst)),
1185                        Some(TopDecl::Instance(_))
1186                    )
1187                {
1188                    return elab.syn_error(
1189                        "cannot project a field out of an instance; an instance is \
1190                         eliminated by mapping out of it, not by projection",
1191                    );
1192                }
1193                let (tm_s, tm_v, ty_v) = elab.syn(tm_n);
1194                let BaseTyV_::Record(r) = &*ty_v else {
1195                    return elab.syn_error("can only project from record type");
1196                };
1197                let label = label_seg(*f);
1198                let f = name_seg(*f);
1199                if !r.fields.has(f) {
1200                    return elab.syn_error(format!("no such field {f}"));
1201                }
1202                (
1203                    BaseTmS::proj(tm_s, f, label),
1204                    elab.evaluator().proj(&tm_v, f, label),
1205                    elab.evaluator().field_ty(&ty_v, &tm_v, f),
1206                )
1207            }
1208            // Codomain-morphism application (`src(we.e)`, `f(x)`) is fiber
1209            // syntax, elaborated by `fiber_syn` inside an instance body — it
1210            // is not a base term, so base `syn` does not handle it.
1211            App1(L(_, Prim("id")), ob_n) => {
1212                let (ob_s, ob_v, ob_t) = elab.syn(ob_n);
1213                let BaseTyV_::Object(ob_type) = &*ob_t else {
1214                    return elab.syn_error("can only apply @id to objects");
1215                };
1216                let Some(mor_type) = elab.theory().hom_type(ob_type.clone()) else {
1217                    return elab.syn_error("object type does not have a hom type");
1218                };
1219                (
1220                    BaseTmS::id(ob_s),
1221                    BaseTmV::id(ob_v.clone()),
1222                    BaseTyV::morphism(mor_type, ob_v.clone(), ob_v),
1223                )
1224            }
1225            App1(L(_, Prim("tab")), mor_n) => {
1226                let (mor_s, mor_v, mor_t) = elab.syn(mor_n);
1227                let BaseTyV_::Morphism(mor_type, _, _) = &*mor_t else {
1228                    return elab.syn_error("can only apply @tab to morphisms");
1229                };
1230                let Some(ob_type) = elab.theory().tabulator(mor_type.clone()) else {
1231                    return elab.syn_error("theory does not have tabulators");
1232                };
1233                (BaseTmS::tab(mor_s), BaseTmV::tab(mor_v.clone()), BaseTyV::object(ob_type))
1234            }
1235            App1(L(_, Prim(name)), ob_n) => {
1236                let name = name_seg(*name);
1237                let Some(ob_op) = elab.theory().basic_ob_op([name].into()) else {
1238                    let th_name = elab.theory.name.to_string();
1239                    return elab.syn_error(format!("operation @{name} not in theory {th_name}"));
1240                };
1241                let dom = elab.theory().ob_op_dom(&ob_op);
1242                let (arg_s, arg_v) = elab.chk(&BaseTyV::object(dom), ob_n);
1243                let cod = elab.theory().ob_op_cod(&ob_op);
1244                (BaseTmS::ob_app(name, arg_s), BaseTmV::app(name, arg_v), BaseTyV::object(cod))
1245            }
1246            App2(L(_, Keyword("*")), f_n, g_n) => {
1247                let (f_s, f_v, f_ty) = elab.syn(f_n);
1248                let (g_s, g_v, g_ty) = elab.syn(g_n);
1249                let BaseTyV_::Morphism(f_mt, f_dom, f_cod) = &*f_ty else {
1250                    elab.loc = Some(f_n.loc());
1251                    return elab.syn_error("expected a morphism");
1252                };
1253                let BaseTyV_::Morphism(g_mt, g_dom, g_cod) = &*g_ty else {
1254                    elab.loc = Some(g_n.loc());
1255                    return elab.syn_error("expected a morphism");
1256                };
1257                let theory = elab.theory();
1258                if theory.tgt_type(f_mt) != theory.src_type(g_mt) {
1259                    return elab.syn_error("incompatible morphism types");
1260                }
1261                if let Err(s) = elab.evaluator().equal_tm(f_cod, g_dom) {
1262                    let f_cod_s = elab.evaluator().quote_tm(f_cod);
1263                    let g_dom_s = elab.evaluator().quote_tm(g_dom);
1264                    return elab.syn_error(format!(
1265                        "codomain {} and domain {} not equal:\n...because {}",
1266                        f_cod_s,
1267                        g_dom_s,
1268                        s.pretty(),
1269                    ));
1270                }
1271                (
1272                    BaseTmS::compose(f_s, g_s),
1273                    BaseTmV::compose(f_v, g_v),
1274                    BaseTyV::morphism(
1275                        elab.theory().compose_types2(f_mt.clone(), g_mt.clone()).unwrap(),
1276                        f_dom.clone(),
1277                        g_cod.clone(),
1278                    ),
1279                )
1280            }
1281            App1(L(_, Var(tv)), L(_, Tuple(args_n))) => {
1282                let tv = name_seg(*tv);
1283                let Some(TopDecl::Def(d)) = elab.toplevel.lookup(tv) else {
1284                    return elab.syn_error(format!("no such toplevel def {tv}"));
1285                };
1286                let mut arg_stxs = Vec::new();
1287                let mut env = Env::nil();
1288                if args_n.len() != d.args.len() {
1289                    return elab.syn_error(format!(
1290                        "wrong number of args for {tv}, expected {}, got {}",
1291                        d.args.len(),
1292                        args_n.len()
1293                    ));
1294                }
1295                for (arg_n, (_, (_, arg_ty_s))) in args_n.iter().zip(d.args.iter()) {
1296                    let arg_ty_v = elab.evaluator().with_env(env.clone()).eval_ty(arg_ty_s);
1297                    let (arg_s, arg_v) = elab.chk(&arg_ty_v, arg_n);
1298                    arg_stxs.push(arg_s);
1299                    env = env.snoc(arg_v);
1300                }
1301                let eval = elab.evaluator().with_env(env.clone());
1302                (BaseTmS::topapp(tv, arg_stxs), eval.eval_tm(&d.body), eval.eval_ty(&d.ret_ty))
1303            }
1304            Tag("tt") => {
1305                // `tt` is the unique element of `Unit`, i.e. the empty record `[]`.
1306                let (_, ty_v) = elab.empty_record_ty();
1307                (BaseTmS::cons(Row::empty()), BaseTmV::cons(Row::empty()), ty_v)
1308            }
1309            Tuple(_) => elab.syn_error("must check against a type in order to construct a record"),
1310            Prim("hole") => elab.syn_error("explicit hole"),
1311            _ => elab.syn_error("unexpected notation for term"),
1312        }
1313    }
1314
1315    /// Elaborates a term from notation, checking against an expected type, and returning syntax and value.
1316    fn chk(&mut self, ty: &BaseTyV, n: &FNtn) -> (BaseTmS, BaseTmV) {
1317        let mut elab = self.enter(n.loc());
1318        match (&**ty, n.ast0()) {
1319            (BaseTyV_::Record(r), Tuple(field_ns)) => {
1320                // Ordinary record construction (a tight transformation /
1321                // generalized element). Instance bodies are *not* dispatched
1322                // here — they are introduced by the `instance` keyword, which
1323                // calls `instance_body` directly — so this arm has no clause
1324                // shape to disambiguate.
1325                if r.fields.len() != field_ns.len() {
1326                    return elab.chk_error(format!(
1327                        "wrong number of fields provided, expected {}, got {}",
1328                        r.fields.len(),
1329                        field_ns.len(),
1330                    ));
1331                }
1332                let mut field_stxs = IndexMap::new();
1333                let mut field_vals = IndexMap::new();
1334                for (field_n, (name, (label, field_ty_s))) in field_ns.iter().zip(r.fields.iter()) {
1335                    elab.loc = Some(field_n.loc());
1336                    let tm_n = match field_n.ast0() {
1337                        App2(L(_, Keyword(":=")), L(_, Var(given_name)), field_val_n) => {
1338                            if name_seg(*given_name) == *name {
1339                                field_val_n
1340                            } else {
1341                                return elab.chk_error(format!("unexpected field {given_name}"));
1342                            }
1343                        }
1344                        _ => {
1345                            return elab.chk_error("unexpected notation for field");
1346                        }
1347                    };
1348                    let v = BaseTmV::cons(field_vals.clone().into());
1349                    let field_ty_v =
1350                        elab.evaluator().with_env(r.env.snoc(v.clone())).eval_ty(field_ty_s);
1351                    let (tm_s, tm_v) = elab.chk(&field_ty_v, tm_n);
1352                    field_stxs.insert(*name, (*label, tm_s));
1353                    field_vals.insert(*name, (*label, tm_v));
1354                }
1355                (BaseTmS::cons(field_stxs.into()), BaseTmV::cons(field_vals.into()))
1356            }
1357            (BaseTyV_::Object(ob_type), Tuple(ob_ns)) => {
1358                let Some(ob_type) = ob_type.clone().list_arg() else {
1359                    return elab.chk_error("expected to object type to be a list");
1360                };
1361                let elem_ty_v = BaseTyV::object(ob_type);
1362                let mut elem_stxs = Vec::new();
1363                let mut elem_vals = Vec::new();
1364                for ob_n in ob_ns {
1365                    elab.loc = Some(ob_n.loc());
1366                    let (tm_s, tm_v) = elab.chk(&elem_ty_v, ob_n);
1367                    elem_stxs.push(tm_s);
1368                    elem_vals.push(tm_v);
1369                }
1370                (BaseTmS::list(elem_stxs), BaseTmV::list(elem_vals))
1371            }
1372            (_, Tuple(_)) => elab.chk_error("tuple expected to be record or object/morphism type"),
1373            (_, Prim("hole")) => elab.chk_error("explicit hole"),
1374            _ => {
1375                let (tm_s, tm_v, synthed) = elab.syn(n);
1376                let eval = elab.evaluator();
1377                if let Err(e) = eval.convertible_ty(&synthed, ty) {
1378                    return elab.chk_error(format!(
1379                        "synthesized type {} does not match expected type {}:\n{}",
1380                        eval.quote_ty(&synthed),
1381                        eval.quote_ty(ty),
1382                        e.pretty()
1383                    ));
1384                }
1385                if let Err(e) = eval.element_of(&tm_v, ty) {
1386                    return elab.chk_error(format!(
1387                        "evaluated term {} is not an element of specialized type {}:\n{}",
1388                        eval.quote_tm(&tm_v),
1389                        eval.quote_ty(ty),
1390                        e.pretty()
1391                    ));
1392                }
1393                (tm_s, tm_v)
1394            }
1395        }
1396    }
1397}
1398
1399/// Extract the path to a codomain morphism from the head of an
1400/// application: a bare variable `f` gives `[f]`, and a projection chain
1401/// `Add.op` gives `[Add, op]`. Returns `None` for any other shape.
1402impl<'a> FiberElab for Elaborator<'a> {
1403    fn ctx(&self) -> &Context {
1404        &self.ctx
1405    }
1406
1407    fn ctx_mut(&mut self) -> &mut Context {
1408        &mut self.ctx
1409    }
1410
1411    fn elab_theory(&self) -> &Theory {
1412        &self.theory
1413    }
1414
1415    fn evaluator(&self) -> Evaluator<'_> {
1416        Elaborator::evaluator(self)
1417    }
1418
1419    fn fresh_meta(&mut self) -> MetaVar {
1420        Elaborator::fresh_meta(self)
1421    }
1422
1423    /// Formats fiber errors into the exact messages this elaborator has
1424    /// always reported; they appear verbatim in committed snapshots, so the
1425    /// strings must not drift.
1426    fn report_fiber(&mut self, err: FiberError) {
1427        let msg = match err {
1428            FiberError::UnknownElement(name) => format!("no such fiber element {name}"),
1429            FiberError::ProjNonRecord => {
1430                "can only project a generator out of a sub-instance".to_string()
1431            }
1432            FiberError::UnknownProj(field) => {
1433                format!("no such generator {field} in sub-instance")
1434            }
1435            FiberError::UnknownObOp(op, th) => format!("operation @{op} not in theory {th}"),
1436            FiberError::ObOpOnNonElement(op) => format!("@{op} applied to a non-fiber-element"),
1437            FiberError::ListElementNotOver => {
1438                "fiber list elements must be elements over an object".to_string()
1439            }
1440            // Only notebook cells can contain unfilled slots.
1441            FiberError::MissingTerm => "missing term".to_string(),
1442            FiberError::ArgNotElement(label) => match label {
1443                Some(label) => format!("argument {label} is not an element over an object"),
1444                None => "argument is not an element over an object".to_string(),
1445            },
1446            FiberError::NotAMorphism(path) => {
1447                format!("codomain field {path} is not a morphism")
1448            }
1449            FiberError::ArgMismatch { path, got, expected, detail } => {
1450                format!("argument to {path} lies over {got}, but it expects {expected}:\n{detail}")
1451            }
1452            FiberError::WrongFiberType(detail) => {
1453                format!("fiber element has the wrong type:\n{detail}")
1454            }
1455            FiberError::MappingLhsNotOver => {
1456                "mapping-entry clause `mor(arg) := target` requires the LHS \
1457                 to be an element over an object (a fiber element); morphism \
1458                 equations constrain the model, not an instance"
1459                    .to_string()
1460            }
1461            FiberError::EquationNotOver => {
1462                "instance equations must be between elements over an object \
1463                 (fiber elements); morphism equations constrain the model, not \
1464                 an instance"
1465                    .to_string()
1466            }
1467            FiberError::InconvertibleEquationSides(detail) => {
1468                format!("equation sides have inconvertible fiber types:\n{detail}")
1469            }
1470            FiberError::ImportCodomainMismatch(name) => {
1471                format!(
1472                    "cannot import {name}: it is an instance of a different model than \
1473                     the enclosing instance"
1474                )
1475            }
1476        };
1477        self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg);
1478    }
1479}
1480
1481fn morphism_path(n: &FNtn) -> Option<Vec<(FieldName, LabelSegment)>> {
1482    match n.ast0() {
1483        Var(f) => Some(vec![(name_seg(*f), label_seg(*f))]),
1484        App1(recv, L(_, Field(g))) => {
1485            let mut p = morphism_path(recv)?;
1486            p.push((name_seg(*g), label_seg(*g)));
1487            Some(p)
1488        }
1489        _ => None,
1490    }
1491}
1492
1493/// Render a morphism/object path as dotted labels (e.g. `Add.op`), for
1494/// error messages.
1495/// The synthetic field name/label `_eqN` for the next auto-named equation
1496/// field of an instance record, advancing the counter.
1497fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) {
1498    let key = format!("_eq{}", *eq_count);
1499    *eq_count += 1;
1500    (name_seg(key.as_str()), label_seg(key.as_str()))
1501}
1502
1503// NOTE: Most tests for the text elaborator are in the `examples` dir.
1504#[cfg(test)]
1505mod tests {
1506    use expect_test::expect;
1507    use std::rc::Rc;
1508
1509    use crate::stdlib;
1510    use crate::tt::modelgen::Model;
1511
1512    #[test]
1513    fn generate_model_from_text() {
1514        let th = Rc::new(stdlib::th_signed_category());
1515        let source = "[
1516            x : Object,
1517            loop : Negative[x, x]
1518        ]";
1519        let model = Model::from_text(&th.clone().into(), source).unwrap();
1520        let model = model.as_discrete().unwrap();
1521        assert_eq!(model, stdlib::models::negative_loop(th));
1522    }
1523
1524    /// Check that a commutative square really produces a model with exactly one equation.
1525    #[test]
1526    fn generate_model_with_eqn() {
1527        let th = Rc::new(stdlib::th_schema()).into();
1528        let source = "[
1529            NW : Entity,
1530            NE : Entity,
1531            SW : Entity,
1532            SE : Entity,
1533            t : (Hom Entity)[NW,NE],
1534            l : (Hom Entity)[NW,SW],
1535            r : (Hom Entity)[NE,SE],
1536            b : (Hom Entity)[SW, SE],
1537            comm : (t * r == l * b)
1538        ]";
1539        let model = Model::from_text(&th, source).unwrap().as_discrete().unwrap();
1540        let eqns: Vec<_> = model.category.equations().collect();
1541        assert_eq!(eqns.len(), 1);
1542    }
1543
1544    #[test]
1545    fn text_error_reporting() {
1546        let th = Rc::new(stdlib::th_schema()).into();
1547
1548        let result = Model::from_text(&th, "[ : Entit]");
1549        let expected = expect![[r#"
1550            error[elab]: expected fields in the form <name> : <type>
1551            --> <none>:1:3
1552            1| [ : Entit]
1553            1|   ^^^^^^^
1554        "#]];
1555        expected.assert_eq(&result.err().unwrap());
1556
1557        let result = Model::from_text(&th, "[x : Entity, f : Hom(Entit)[x,x]]");
1558        let expected = expect![[r#"
1559            error[elab]: no such object type Entit
1560            --> <none>:1:18
1561            1| [x : Entity, f : Hom(Entit)[x,x]]
1562            1|                  ^^^^^^^^^^
1563        "#]];
1564        expected.assert_eq(&result.err().unwrap());
1565
1566        let result = Model::from_text(&th, "[x : Entity, f : Hom(Entity)[x,y]]");
1567        let expected = expect![[r#"
1568            error[elab]: no such variable y
1569            --> <none>:1:32
1570            1| [x : Entity, f : Hom(Entity)[x,y]]
1571            1|                                ^
1572            error[elab]: synthesized type ?1 does not match expected type Entity:
1573            tried to convert between types of different type constructors
1574            --> <none>:1:32
1575            1| [x : Entity, f : Hom(Entity)[x,y]]
1576            1|                                ^
1577        "#]];
1578        expected.assert_eq(&result.err().unwrap());
1579    }
1580}