catlog/tt/
modelgen.rs

1//! Generate catlog models from DoubleTT types.
2
3use all_the_same::all_the_same;
4use derive_more::{From, TryInto};
5use tattle::display::SourceInfo;
6
7use std::rc::Rc;
8
9use super::{eval::*, prelude::*, text_elab, theory::*, toplevel::*, val::*};
10use crate::dbl::{
11    discrete::{self, DiscreteDblModelInstance, DiscreteInstanceTerm},
12    discrete_tabulator, modal,
13    model::{DblModel, DblModelPrinter, FpDblModel, MutDblModel},
14    theory::{DblTheory, DblTheoryKind, NonUnital, Unital},
15};
16use crate::one::{
17    Category,
18    path::{Path, PathEq},
19};
20use crate::zero::{Namespace, QualifiedName, SkelColumn};
21
22/// A model generated by DoubleTT.
23///
24/// Similarly to [`TheoryDef`], this enum boxes the different types of models
25/// that can be generated by DoubleTT.
26pub enum Model {
27    /// A model of a discrete double theory.
28    Discrete(Box<discrete::DiscreteDblModel>),
29    /// A model of a discrete tabulator theory.
30    DiscreteTab(Box<discrete_tabulator::DiscreteTabModel>),
31    /// A model of a unital modal double theory.
32    ModalUnital(Box<modal::ModalDblModel<Unital>>),
33    /// A model of a non-unital modal double theory.
34    ModalNonUnital(Box<modal::ModalDblModel<NonUnital>>),
35}
36
37/// An object in a model generated by DoubleTT.
38#[derive(Debug, From, TryInto)]
39enum Ob {
40    Discrete(QualifiedName),
41    DiscreteTab(discrete_tabulator::TabOb),
42    Modal(modal::ModalOb),
43}
44
45/// A morphism in a model generated by DoubleTT.
46#[derive(Debug, From, TryInto)]
47enum Mor {
48    Discrete(Path<QualifiedName, QualifiedName>),
49    DiscreteTab(discrete_tabulator::TabMor),
50    Modal(modal::ModalMor),
51}
52
53impl Model {
54    /// Constructs an empty model of a theory.
55    pub fn new(theory: &TheoryDef) -> Self {
56        match theory {
57            TheoryDef::Discrete(theory) => {
58                Model::Discrete(Box::new(discrete::DiscreteDblModel::new(theory.clone())))
59            }
60            TheoryDef::DiscreteTab(theory) => Model::DiscreteTab(Box::new(
61                discrete_tabulator::DiscreteTabModel::new(theory.clone()),
62            )),
63            TheoryDef::ModalUnital(theory) => {
64                Model::ModalUnital(Box::new(modal::ModalDblModel::new(theory.clone())))
65            }
66            TheoryDef::ModalNonUnital(theory) => {
67                Model::ModalNonUnital(Box::new(modal::ModalDblModel::new(theory.clone())))
68            }
69        }
70    }
71
72    /// Parses and generates a model from plain text.
73    ///
74    /// If there is an error in parsing, an error message is returned.
75    pub fn from_text(th: &TheoryDef, s: &str) -> Result<Self, String> {
76        let theory = Theory::new("_".into(), th.clone());
77        let reporter = Reporter::new();
78        let toplevel: Toplevel = Default::default();
79        let maybe_elab = text_elab::TT_PARSE_CONFIG.with_parsed(s, reporter.clone(), |fntn| {
80            let mut elaborator = text_elab::Elaborator::new(theory, reporter.clone(), &toplevel);
81            Some(elaborator.ty(fntn))
82        });
83        if let Some((_, ty_v)) = maybe_elab
84            && !reporter.errored()
85        {
86            let (model, _) = Self::from_ty(&toplevel, th, &ty_v);
87            Ok(model)
88        } else {
89            let source_info = SourceInfo::new(None, s);
90            Err(source_info.extract_report_to_string(reporter))
91        }
92    }
93
94    /// Generates a model from a type.
95    ///
96    /// Precondition: `ty` must be valid in the empty context.
97    pub fn from_ty(toplevel: &Toplevel, th: &TheoryDef, ty: &BaseTyV) -> (Self, Namespace) {
98        let mut generator = ModelGenerator::new(toplevel, th);
99        let namespace = generator.generate(ty);
100        (generator.model, namespace)
101    }
102
103    /// Tries to extract a model of a discrete theory.
104    pub fn as_discrete(self) -> Option<discrete::DiscreteDblModel> {
105        match self {
106            Model::Discrete(model) => Some(*model),
107            _ => None,
108        }
109    }
110
111    /// Tries to extract a model of a unital modal theory.
112    pub fn as_modal(self) -> Option<modal::ModalDblModel<Unital>> {
113        match self {
114            Model::ModalUnital(model) => Some(*model),
115            _ => None,
116        }
117    }
118
119    /// Tries to extract a model of a non-unital modal theory.
120    pub fn as_modal_non_unital(self) -> Option<modal::ModalDblModel<NonUnital>> {
121        match self {
122            Model::ModalNonUnital(model) => Some(*model),
123            _ => None,
124        }
125    }
126
127    /// Constructs the identity morphism on an object.
128    fn id(&self, ob: Ob) -> Mor {
129        all_the_same!(match self {
130            Model::[Discrete, ModalUnital, DiscreteTab, ModalNonUnital](model) => {
131                model.id(ob.try_into().unwrap()).into()
132            }
133        })
134    }
135
136    /// Composes a pair of morphisms.
137    fn compose2(&self, mor1: Mor, mor2: Mor) -> Mor {
138        all_the_same!(match self {
139            Model::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => {
140                model.compose2(mor1.try_into().unwrap(), mor2.try_into().unwrap()).into()
141            }
142        })
143    }
144
145    /// Tabulates a morphism, if possible.
146    fn tabulated(&self, mor: Mor) -> Option<Ob> {
147        match self {
148            Model::Discrete(_) => None,
149            Model::DiscreteTab(model) => Some(model.tabulated(mor.try_into().unwrap()).into()),
150            Model::ModalUnital(_) | Model::ModalNonUnital(_) => None,
151        }
152    }
153
154    /// Adds an object generator to the model.
155    fn add_ob(&mut self, name: QualifiedName, ob_type: ObType) {
156        all_the_same!(match self {
157            Model::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => {
158                model.add_ob(name, ob_type.try_into().unwrap())
159            }
160        });
161    }
162
163    /// Adds a morphism generator to the model.
164    fn add_mor(&mut self, name: QualifiedName, dom: Ob, cod: Ob, mor_type: MorType) {
165        all_the_same!(match self {
166            Model::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => {
167                model.add_mor(
168                    name,
169                    dom.try_into().unwrap(),
170                    cod.try_into().unwrap(),
171                    mor_type.try_into().unwrap()
172                );
173            }
174        });
175    }
176
177    /// Adds an equation between two morphisms to the model.
178    fn add_equation(&mut self, lhs: Mor, rhs: Mor) {
179        match self {
180            Model::Discrete(model) => {
181                model.add_equation(PathEq::new(lhs.try_into().unwrap(), rhs.try_into().unwrap()));
182            }
183            Model::DiscreteTab(_) => {
184                // Discrete tabulator models currently do not support equations, so we ignore them.
185            }
186            Model::ModalUnital(_) | Model::ModalNonUnital(_) => {
187                // Modal models currently do not support equations, so we ignore them.
188            }
189        }
190    }
191
192    /// Pretty prints a summary of the model.
193    pub fn summary(&self, printer: &DblModelPrinter) -> String {
194        all_the_same!(match self {
195            Model::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => printer.summary(model.as_ref())
196        })
197    }
198
199    /// Pretty prints the model in the given namespace.
200    pub fn to_doc<'a>(&self, printer: &DblModelPrinter, ns: &Namespace) -> D<'a> {
201        all_the_same!(match self {
202            Model::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => printer.namespaced_doc(model.as_ref(), ns, ns)
203        })
204    }
205}
206
207struct ModelGenerator<'a> {
208    eval: Evaluator<'a>,
209    theory: TheoryDef,
210    model: Model,
211}
212
213impl<'a> ModelGenerator<'a> {
214    fn new(toplevel: &'a Toplevel, theory: &TheoryDef) -> Self {
215        let eval = Evaluator::empty(toplevel);
216        let theory = theory.clone();
217        let model = Model::new(&theory);
218        Self { eval, theory, model }
219    }
220
221    fn generate(&mut self, ty: &BaseTyV) -> Namespace {
222        let tm_n;
223        (tm_n, self.eval) = self.eval.bind_self(ty.clone());
224        let tm_v = self.eval.eta_neu(&tm_n, ty);
225        self.extract(vec![], &tm_v, ty).unwrap_or_else(Namespace::new_for_uuid)
226    }
227
228    /// Constructs a generating morphism.
229    fn mor_generator(&self, name: QualifiedName) -> Mor {
230        match &self.model {
231            Model::Discrete(_) => Mor::Discrete(Path::single(name)),
232            Model::DiscreteTab(_) => Mor::DiscreteTab(name.into()),
233            Model::ModalUnital(_) | Model::ModalNonUnital(_) => {
234                Mor::Modal(modal::ModalMor::Generator(name))
235            }
236        }
237    }
238
239    /// Constructs an application of an object operation, if the theory is modal.
240    ///
241    /// Returns the constructed object along with the expected object type of the result.
242    fn ob_app(&self, name: &NameSegment, tm_v: &BaseTmV) -> Option<(Ob, ObType)> {
243        let name: QualifiedName = [*name].into();
244        match &self.model {
245            Model::Discrete(_) | Model::DiscreteTab(_) => None,
246            Model::ModalUnital(model) => self.op_app_modal(model, name, tm_v),
247            Model::ModalNonUnital(model) => self.op_app_modal(model, name, tm_v),
248        }
249    }
250
251    fn op_app_modal<Kind: DblTheoryKind>(
252        &self,
253        model: &modal::ModalDblModel<Kind>,
254        name: QualifiedName,
255        tm_v: &BaseTmV,
256    ) -> Option<(Ob, ObType)> {
257        let theory = model.theory();
258        let op = modal::ModalObOp::generator(name.clone());
259        let ot: ObType = theory.ob_op_cod(&op).into();
260        let ob = self.make_ob_check_type(tm_v, &theory.ob_op_dom(&op).into())?;
261        let ob = ob.try_into().unwrap();
262        Some((Ob::Modal(modal::ModalOb::App(Box::new(ob), name)), ot))
263    }
264
265    /// Constructs a list of objects, if allowed.
266    fn ob_list(&self, elems: Vec<Ob>, ob_type: &ObType) -> Option<Ob> {
267        match &self.model {
268            Model::Discrete(_) | Model::DiscreteTab(_) => None,
269            Model::ModalUnital(_) | Model::ModalNonUnital(_) => {
270                let ob_type: &modal::ModalObType = ob_type.try_into().unwrap();
271                let Some(modal::Modality::List(list_type)) = ob_type.modalities.last() else {
272                    unreachable!() // We know this is a list modality from the call site.
273                };
274                Some(Ob::Modal(modal::ModalOb::List(
275                    *list_type,
276                    elems.into_iter().map(|ob| ob.try_into().unwrap()).collect(),
277                )))
278            }
279        }
280    }
281
282    /// Attempts to make an object of a model from a term.
283    ///
284    /// Returns the object together with the appropriate type.
285    fn make_ob_synth_type(&self, val: &BaseTmV) -> Option<(Ob, ObType)> {
286        match &**val {
287            BaseTmV_::Neu(n, ty_v) => {
288                let BaseTyV_::Object(ob_type) = &**ty_v else {
289                    return None;
290                };
291                let name = n.to_qualified_name();
292                let ob = match &self.model {
293                    Model::Discrete(_) => Ob::Discrete(name),
294                    Model::DiscreteTab(_) => Ob::DiscreteTab(name.into()),
295                    Model::ModalUnital(_) | Model::ModalNonUnital(_) => {
296                        Ob::Modal(modal::ModalOb::Generator(name))
297                    }
298                };
299                Some((ob, ob_type.clone()))
300            }
301            BaseTmV_::App(name, tm_v) => self.ob_app(name, tm_v),
302            BaseTmV_::Tab(mor_tm_v) => {
303                let (mor, mor_type) = self.synth_mor(mor_tm_v)?;
304                Some((self.model.tabulated(mor)?, self.theory.tabulator(mor_type)?))
305            }
306            _ => None,
307        }
308    }
309
310    /// Attempts to make an object of a model from a term.
311    ///
312    /// Also checks that the term constructs an object of the given type, returning only the object if successful.
313    fn make_ob_check_type(&self, val: &BaseTmV, ob_type: &ObType) -> Option<Ob> {
314        match &**val {
315            // ob_type checked recursively.
316            BaseTmV_::List(elems) => {
317                let el_type = ob_type.clone().list_arg()?;
318                let elems: Option<Vec<_>> =
319                    elems.iter().map(|tm| self.make_ob_check_type(tm, &el_type)).collect();
320                self.ob_list(elems?, ob_type)
321            }
322            _ => {
323                let (ob, ot) = self.make_ob_synth_type(val)?;
324                (ot == *ob_type).then_some(ob)
325            }
326        }
327    }
328
329    /// Attempts to make a morphism of a model from a term.
330    ///
331    /// Also returns the expected morphism type of the result.
332    fn synth_mor(&self, val: &BaseTmV) -> Option<(Mor, MorType)> {
333        match &**val {
334            BaseTmV_::Neu(n, ty_v) => {
335                let BaseTyV_::Morphism(mor_type, _, _) = &**ty_v else {
336                    return None;
337                };
338                let name = n.to_qualified_name();
339                Some((self.mor_generator(name), mor_type.clone()))
340            }
341            BaseTmV_::Id(x) => {
342                let (dom, dom_type) = self.make_ob_synth_type(x)?;
343                let mor_type = self.theory.hom_type(dom_type)?;
344                Some((self.model.id(dom), mor_type))
345            }
346            BaseTmV_::Compose(f, g) => {
347                let (mf, mtf) = self.synth_mor(f)?;
348                let (mg, mtg) = self.synth_mor(g)?;
349                Some((self.model.compose2(mf, mg), self.theory.compose_types2(mtf, mtg)?))
350            }
351            _ => None,
352        }
353    }
354
355    /// Attempts to make a morphism from a term of the given morphism type.
356    ///
357    /// At this time, all morphism constructors allow for type synthesis, but
358    /// eventually this will change.
359    fn make_mor(&self, val: &BaseTmV, mor_type: &MorType) -> Option<Mor> {
360        let (mor, mt) = self.synth_mor(val)?;
361        (mt == *mor_type).then_some(mor)
362    }
363
364    fn extract(
365        &mut self,
366        prefix: Vec<NameSegment>,
367        val: &BaseTmV,
368        ty: &BaseTyV,
369    ) -> Option<Namespace> {
370        match &**ty {
371            BaseTyV_::Object(ot) => {
372                self.model.add_ob(prefix.into(), ot.clone());
373                None
374            }
375            BaseTyV_::Morphism(mt, dom, cod) => {
376                let dom = self.make_ob_check_type(dom, &self.theory.src_type(mt))?;
377                let cod = self.make_ob_check_type(cod, &self.theory.tgt_type(mt))?;
378                self.model.add_mor(prefix.into(), dom, cod, mt.clone());
379                None
380            }
381            BaseTyV_::Record(r) => {
382                let mut namespace = Namespace::new_for_uuid();
383                for (name, (label, _)) in r.fields.iter() {
384                    let mut prefix = prefix.clone();
385                    prefix.push(*name);
386                    if let NameSegment::Uuid(uuid) = name {
387                        namespace.set_label(*uuid, *label);
388                    }
389                    let field_tm_v = self.eval.proj(val, *name, *label);
390                    let field_ty_v = self.eval.field_ty(ty, val, *name);
391                    if let Some(inner) = self.extract(prefix, &field_tm_v, &field_ty_v) {
392                        namespace.add_inner(*name, inner);
393                    };
394                }
395                Some(namespace)
396            }
397            BaseTyV_::Sing(_, _) => None,
398            BaseTyV_::Id(mor_ty, lhs, rhs) => {
399                let BaseTyV_::Morphism(mt, _, _) = &**mor_ty else {
400                    return None;
401                };
402                if let (Some(lhs), Some(rhs)) = (self.make_mor(lhs, mt), self.make_mor(rhs, mt)) {
403                    self.model.add_equation(lhs, rhs);
404                }
405                None
406            }
407            BaseTyV_::Meta(_) => None,
408        }
409    }
410
411    /// Extracts a [`ModelInstance`] from the fiber record of an instance body,
412    /// dispatching on the doctrine of the (already generated) codomain model.
413    fn instance(
414        &self,
415        fields: &Row<FiberTyV>,
416        namespace: &mut Namespace,
417    ) -> Result<ModelInstance, String> {
418        match &self.model {
419            Model::Discrete(model) => {
420                let mut instance = DiscreteDblModelInstance::new(Rc::new((**model).clone()));
421                extract_instance_record(&mut instance, namespace, &[], fields)?;
422                Ok(ModelInstance::Discrete(instance))
423            }
424            Model::DiscreteTab(_) => {
425                Err("instance generation does not support discrete tabulator theories".into())
426            }
427            Model::ModalUnital(model) => {
428                Ok(ModelInstance::ModalUnital(self.modal_instance(model, namespace, fields)?))
429            }
430            Model::ModalNonUnital(model) => {
431                Ok(ModelInstance::ModalNonUnital(self.modal_instance(model, namespace, fields)?))
432            }
433        }
434    }
435
436    /// Builds a modal instance over the given codomain model by walking the
437    /// fiber record.
438    fn modal_instance<Kind: DblTheoryKind + Clone>(
439        &self,
440        model: &modal::ModalDblModel<Kind>,
441        namespace: &mut Namespace,
442        fields: &Row<FiberTyV>,
443    ) -> Result<modal::ModalDblModelInstance<Kind>, String> {
444        let mut instance = modal::ModalDblModelInstance::new(Rc::new(model.clone()));
445        self.extract_modal_record(&mut instance, namespace, &[], fields)?;
446        Ok(instance)
447    }
448
449    /// Registers the contents of an instance (a fiber record) into a modal
450    /// model instance, recursing into sub-instance imports under their prefix.
451    ///
452    /// Two passes, matching [`extract_instance_record`]: generators and
453    /// sub-instances first, then the equation ([`Id`](FiberTyV_::Id)) fields,
454    /// so every generator a term may mention is already registered.
455    fn extract_modal_record<Kind: DblTheoryKind>(
456        &self,
457        instance: &mut modal::ModalDblModelInstance<Kind>,
458        namespace: &mut Namespace,
459        prefix: &[NameSegment],
460        fields: &Row<FiberTyV>,
461    ) -> Result<(), String> {
462        for (name, (label, field_ty)) in fields.iter() {
463            match &**field_ty {
464                FiberTyV_::Over(obj) => {
465                    if let NameSegment::Uuid(uuid) = name {
466                        namespace.set_label(*uuid, *label);
467                    }
468                    let mut qsegs = prefix.to_vec();
469                    qsegs.push(*name);
470                    let qname: QualifiedName = qsegs.into();
471                    let fiber = self.modal_fiber_ob(obj)?;
472                    instance.add_generator(qname, fiber);
473                }
474                FiberTyV_::Record(sub_fields) => {
475                    if let NameSegment::Uuid(uuid) = name {
476                        namespace.set_label(*uuid, *label);
477                    }
478                    // Labels of the sub-instance's own generators live in an
479                    // inner namespace keyed by the import's name, mirroring
480                    // the structure of the flattened qualified names.
481                    let mut sub_ns = Namespace::new_for_uuid();
482                    let mut sub_prefix = prefix.to_vec();
483                    sub_prefix.push(*name);
484                    self.extract_modal_record(instance, &mut sub_ns, &sub_prefix, sub_fields)?;
485                    namespace.add_inner(*name, sub_ns);
486                }
487                FiberTyV_::Id(_, _, _) => {}
488            }
489        }
490        for (_, (_, field_ty)) in fields.iter() {
491            if let FiberTyV_::Id(eq_ty, lhs, rhs) = &**field_ty {
492                let ob_type = self.modal_equation_ob_type(eq_ty)?;
493                let lhs_t = self.modal_instance_term(&*instance, lhs, &ob_type, prefix)?;
494                let rhs_t = self.modal_instance_term(&*instance, rhs, &ob_type, prefix)?;
495                instance.add_equation(lhs_t, rhs_t);
496            }
497        }
498        Ok(())
499    }
500
501    /// Converts the codomain object a generator lies over into a [`ModalOb`].
502    fn modal_fiber_ob(&self, obj: &BaseTmV) -> Result<modal::ModalOb, String> {
503        let (ob, _) = self.make_ob_synth_type(obj).ok_or_else(|| {
504            "instance generator lies over an object this doctrine cannot yet extract".to_string()
505        })?;
506        ob.try_into()
507            .map_err(|_| "expected a modal object as a generator's fiber".to_string())
508    }
509
510    /// The object type an equation lives over (its `Over` fiber type).
511    fn modal_equation_ob_type(&self, eq_ty: &FiberTyV) -> Result<ObType, String> {
512        let FiberTyV_::Over(obj) = &**eq_ty else {
513            return Err("instance equation is not over an object".into());
514        };
515        let (_, ob_type) = self
516            .make_ob_synth_type(obj)
517            .ok_or_else(|| "cannot determine the type of an instance equation".to_string())?;
518        Ok(ob_type)
519    }
520
521    /// Converts a fiber term into a [`ModalInstanceTerm`].
522    fn modal_instance_term<Kind: DblTheoryKind>(
523        &self,
524        instance: &modal::ModalDblModelInstance<Kind>,
525        tm: &FiberTmV,
526        expected: &ObType,
527        prefix: &[NameSegment],
528    ) -> Result<modal::ModalInstanceTerm, String> {
529        let (mor, base) = self.modal_mor_base(instance, tm, expected, prefix)?;
530        Ok(modal::ModalInstanceTerm { mor, base })
531    }
532
533    /// The heart of modal term extraction: converts a fiber term into a
534    /// morphism applied to a base of generators, threading the expected object
535    /// type down so list modalities can be recovered.
536    ///
537    /// The returned morphism is an identity exactly when the term is a pure
538    /// base (generators/lists with no applied morphisms), keeping terms in the
539    /// flat normal form of [`ModalInstanceTerm`]: composition and list-tupling
540    /// of morphisms encountered inside list elements are pushed into the
541    /// morphism (via `Composite`/`List`) rather than nesting applications.
542    fn modal_mor_base<Kind: DblTheoryKind>(
543        &self,
544        instance: &modal::ModalDblModelInstance<Kind>,
545        tm: &FiberTmV,
546        expected: &ObType,
547        prefix: &[NameSegment],
548    ) -> Result<(modal::ModalMor, modal::ModalInstanceBase), String> {
549        use modal::{ModalInstanceBase, ModalMor, ModalOb, Modality, MorListData};
550        match &**tm {
551            FiberTmV_::Var(_, _, _) | FiberTmV_::Proj(_, _, _) => {
552                let mut segs = prefix.to_vec();
553                segs.extend(fiber_full_name(tm)?);
554                let qname: QualifiedName = segs.into();
555                let fiber = instance
556                    .fiber_of(&qname)
557                    .ok_or_else(|| format!("instance term mentions unknown generator {qname}"))?;
558                let id = instance.model().id(fiber.clone());
559                Ok((id, ModalInstanceBase::Generator(qname)))
560            }
561            FiberTmV_::List(elems) => {
562                let (modality, el_type) = expected
563                    .clone()
564                    .mode_app()
565                    .ok_or_else(|| "expected a modal list type for a list term".to_string())?;
566                let Modality::List(list_ty) = modality else {
567                    return Err("expected a list modality for a list term".into());
568                };
569                let mut mors = Vec::with_capacity(elems.len());
570                let mut bases = Vec::with_capacity(elems.len());
571                for elem in elems {
572                    let (m, b) = self.modal_mor_base(instance, elem, &el_type, prefix)?;
573                    mors.push(m);
574                    bases.push(b);
575                }
576                let base = ModalInstanceBase::List(list_ty, bases);
577                // If every element is a pure base, the whole list is too, and
578                // the morphism is the identity on the list object.
579                let identity_obs: Option<Vec<&ModalOb>> =
580                    mors.iter().map(modal::modal_mor_as_identity).collect();
581                if let Some(objs) = identity_obs {
582                    let list_ob = ModalOb::List(list_ty, objs.into_iter().cloned().collect());
583                    Ok((instance.model().id(list_ob), base))
584                } else {
585                    let data = match list_ty {
586                        modal::List::Plain => MorListData::Plain(),
587                        modal::List::Symmetric => {
588                            MorListData::Symmetric(SkelColumn::new((0..mors.len()).collect()))
589                        }
590                        other => {
591                            return Err(format!(
592                                "instance terms do not yet support the {other:?} list modality"
593                            ));
594                        }
595                    };
596                    Ok((ModalMor::List(data, mors), base))
597                }
598            }
599            FiberTmV_::OverApp(path, _cod, inner) => {
600                let qname: QualifiedName =
601                    path.iter().map(|(seg, _)| *seg).collect::<Vec<_>>().into();
602                let mor = ModalMor::Generator(qname.clone());
603                let mor_type = MorType::Modal(instance.model().mor_generator_type(&qname));
604                let dom_ty = self.theory.src_type(&mor_type);
605                let (inner_mor, base) = self.modal_mor_base(instance, inner, &dom_ty, prefix)?;
606                let full = if modal::modal_mor_as_identity(&inner_mor).is_some() {
607                    mor
608                } else {
609                    instance.model().compose2(inner_mor, mor)
610                };
611                Ok((full, base))
612            }
613            FiberTmV_::ObApp(op, inner) => {
614                let op_name: QualifiedName = [*op].into();
615                // Recurse into the argument at the object operation's domain
616                // type, so a list argument recovers its modality.
617                let ob_op = modal::ModalObOp::generator(op_name.clone());
618                let dom_ty: ObType = instance.model().theory().ob_op_dom(&ob_op).into();
619                let (inner_mor, inner_base) =
620                    self.modal_mor_base(instance, inner, &dom_ty, prefix)?;
621                // If the argument is a pure base (identity morphism), the
622                // object operation lands on data and the morphism stays the
623                // identity on the resulting `App` object. Otherwise the
624                // operation acts functorially on the argument's morphism,
625                // yielding a `HomApp`.
626                let mor = match modal::modal_mor_as_identity(&inner_mor).cloned() {
627                    Some(inner_ob) => {
628                        let app_ob = ModalOb::App(Box::new(inner_ob), op_name.clone());
629                        instance.model().id(app_ob)
630                    }
631                    None => ModalMor::HomApp(Box::new(inner_mor.into()), op_name.clone()),
632                };
633                let base = ModalInstanceBase::ObApp(op_name, Box::new(inner_base));
634                Ok((mor, base))
635            }
636            FiberTmV_::Meta(_) => Err("instance term contains an unresolved metavariable".into()),
637        }
638    }
639}
640
641/// An instance of a model generated by DoubleTT.
642///
643/// Like [`Model`], this boxes the per-doctrine concrete instance types
644/// behind one enum.
645pub enum ModelInstance {
646    /// An instance of a discrete double model.
647    Discrete(DiscreteDblModelInstance),
648    /// An instance of a unital modal double model.
649    ModalUnital(modal::ModalDblModelInstance<Unital>),
650    /// An instance of a non-unital modal double model.
651    ModalNonUnital(modal::ModalDblModelInstance<NonUnital>),
652}
653
654/// Generates a [`ModelInstance`] from an elaborated [`Instance`] declaration.
655///
656/// Walks the instance's fiber [`Record`](FiberTyV_::Record), registering
657/// each generator with its fiber, each equation as a pair of instance
658/// terms, and each sub-instance's contents under the appropriate prefix.
659///
660/// The codomain model is built with a `ModelGenerator`, which is then
661/// reused to type the instance's generators and equation terms (this is how
662/// modal list modalities are recovered).
663pub fn instance_from_def(
664    toplevel: &Toplevel,
665    th: &TheoryDef,
666    inst: &Instance,
667) -> Result<(ModelInstance, Namespace), String> {
668    let mut generator = ModelGenerator::new(toplevel, th);
669    // Seed the namespace with the codomain model's labels, so that names of
670    // codomain objects and morphisms appearing in fibers and terms resolve
671    // alongside the instance's own generators.
672    let mut namespace = generator.generate(&inst.codomain);
673    let FiberTyV_::Record(fields) = &*inst.val else {
674        return Err("expected an instance (a fiber record)".into());
675    };
676    let instance = generator.instance(fields, &mut namespace)?;
677    Ok((instance, namespace))
678}
679
680/// The flat normal form of a single instance term, as produced by
681/// [`normalize_instance_term`]: a composite model morphism applied to a base
682/// of generators. Doctrine-specific because each has its own term type.
683pub enum NormalizedInstanceTerm {
684    /// A term of an instance of a discrete double model.
685    Discrete(DiscreteInstanceTerm),
686    /// A term of an instance of a modal double model (unital or non-unital).
687    Modal(modal::ModalInstanceTerm),
688}
689
690impl NormalizedInstanceTerm {
691    /// Renders the term in its flat normal form `mor @ base`, exposing the
692    /// single (composite) model morphism acting on a base of generators. A
693    /// term whose morphism is the identity (a pure base) is rendered as just
694    /// its base. This is deliberately distinct from the applicative
695    /// reconstruction used in instance summaries: the point of `norm` is to
696    /// *show* the composite, e.g. `t(@tensor [s(x0), s(x0)])` normalizing to
697    /// `(@tensor [s, s] ; t) @ @tensor [x0, x0]`.
698    pub fn render(&self) -> String {
699        match self {
700            NormalizedInstanceTerm::Discrete(t) => match &t.path {
701                Path::Id(_) => format!("{}", t.base),
702                Path::Seq(edges) => {
703                    let parts: Vec<_> = edges.iter().map(|mor| format!("{mor}")).collect();
704                    if parts.len() == 1 {
705                        format!("{} @ {}", parts[0], t.base)
706                    } else {
707                        format!("({}) @ {}", parts.join(" ; "), t.base)
708                    }
709                }
710            },
711            NormalizedInstanceTerm::Modal(t) => {
712                let base = render_modal_base(&t.base);
713                match modal::modal_mor_as_identity(&t.mor) {
714                    Some(_) => base,
715                    None => format!("{} @ {}", render_modal_mor(&t.mor), base),
716                }
717            }
718        }
719    }
720}
721
722/// Renders the base of a flat modal term: generators by name, lists as
723/// `[a, b, …]`, and object operations as `@op <base>`.
724fn render_modal_base(base: &modal::ModalInstanceBase) -> String {
725    match base {
726        modal::ModalInstanceBase::Generator(name) => format!("{name}"),
727        modal::ModalInstanceBase::List(_, bases) => {
728            let inner: Vec<_> = bases.iter().map(render_modal_base).collect();
729            format!("[{}]", inner.join(", "))
730        }
731        modal::ModalInstanceBase::ObApp(op, inner) => {
732            format!("@{op} {}", render_modal_base(inner))
733        }
734    }
735}
736
737/// Renders a model morphism in a flat term. A composite of length ≥ 2 is
738/// shown parenthesized as `(m1 ; m2 ; …)`, applied left-to-right; the
739/// functorial `HomApp` of an object operation shows as `@op <path>`.
740fn render_modal_mor(mor: &modal::ModalMor) -> String {
741    match mor {
742        modal::ModalMor::Generator(name) => format!("{name}"),
743        modal::ModalMor::Composite(path) => {
744            let parts = render_modal_mor_path(path);
745            match parts.len() {
746                0 => "id".to_string(),
747                1 => parts.into_iter().next().unwrap(),
748                _ => format!("({})", parts.join(" ; ")),
749            }
750        }
751        modal::ModalMor::App(path, op) => {
752            format!("{op}({})", render_modal_mor_path(path).join(" ; "))
753        }
754        modal::ModalMor::HomApp(path, op) => {
755            format!("@{op} {}", render_modal_mor_path(path).join(" ; "))
756        }
757        modal::ModalMor::List(_, mors) => {
758            let inner: Vec<_> = mors.iter().map(render_modal_mor).collect();
759            format!("[{}]", inner.join(", "))
760        }
761    }
762}
763
764/// The morphisms along a path, each flat-rendered; empty for an identity path.
765fn render_modal_mor_path(path: &Path<modal::ModalOb, modal::ModalMor>) -> Vec<String> {
766    match path {
767        Path::Id(_) => Vec::new(),
768        Path::Seq(edges) => edges.iter().map(render_modal_mor).collect(),
769    }
770}
771
772/// Normalizes a single already-elaborated fiber term `tm` (lying over the
773/// codomain object `over`) in the context of the instance `inst`, returning
774/// its flat normal form.
775///
776/// This is what backs `norm [inst] <term>`: the term is elaborated against
777/// `inst`'s fiber scope elsewhere (in `text_elab`), then handed here to run
778/// the same extraction that turns an instance body's equations into
779/// [`modal::ModalInstanceTerm`]s — which is where nested morphism applications
780/// get composed into a single (`Composite`/`List`) morphism.
781pub fn normalize_instance_term(
782    toplevel: &Toplevel,
783    th: &TheoryDef,
784    inst: &Instance,
785    tm: &FiberTmV,
786    over: &BaseTmV,
787) -> Result<NormalizedInstanceTerm, String> {
788    let mut generator = ModelGenerator::new(toplevel, th);
789    generator.generate(&inst.codomain);
790    let FiberTyV_::Record(fields) = &*inst.val else {
791        return Err("expected an instance (a fiber record)".into());
792    };
793    // Build the instance first, so every generator the term may mention is
794    // registered before we extract (extraction resolves generators by name).
795    let mut namespace = Namespace::new_for_uuid();
796    match generator.instance(fields, &mut namespace)? {
797        ModelInstance::Discrete(instance) => {
798            let term = fiber_tm_to_discrete_instance_term(&instance, tm, &[])?;
799            Ok(NormalizedInstanceTerm::Discrete(term))
800        }
801        ModelInstance::ModalUnital(instance) => {
802            let (_, ob_type) = generator
803                .make_ob_synth_type(over)
804                .ok_or_else(|| "cannot determine the object the term lies over".to_string())?;
805            let term = generator.modal_instance_term(&instance, tm, &ob_type, &[])?;
806            Ok(NormalizedInstanceTerm::Modal(term))
807        }
808        ModelInstance::ModalNonUnital(instance) => {
809            let (_, ob_type) = generator
810                .make_ob_synth_type(over)
811                .ok_or_else(|| "cannot determine the object the term lies over".to_string())?;
812            let term = generator.modal_instance_term(&instance, tm, &ob_type, &[])?;
813            Ok(NormalizedInstanceTerm::Modal(term))
814        }
815    }
816}
817
818/// Register the contents of an instance (a fiber record) into the model
819/// instance under construction, recursing into sub-instance imports under
820/// their prefix.
821///
822/// Two passes so that every generator — including those of sub-instances
823/// — is registered before any equation that may mention it: first the
824/// [`Over`](FiberTyV_::Over) generator fields and [`Record`](FiberTyV_::Record)
825/// sub-instances, then the [`Id`](FiberTyV_::Id) equation fields.
826fn extract_instance_record(
827    instance: &mut DiscreteDblModelInstance,
828    namespace: &mut Namespace,
829    prefix: &[NameSegment],
830    fields: &Row<FiberTyV>,
831) -> Result<(), String> {
832    for (name, (label, field_ty)) in fields.iter() {
833        match &**field_ty {
834            FiberTyV_::Over(obj) => {
835                if let NameSegment::Uuid(uuid) = name {
836                    namespace.set_label(*uuid, *label);
837                }
838                let mut qsegs = prefix.to_vec();
839                qsegs.push(*name);
840                let qname: QualifiedName = qsegs.into();
841                // The fiber is the codomain object the generator lies over.
842                // For a plain generator that is a projection `self.V`, whose
843                // qualified name is `V`. Modal objects (lists, tensors) are
844                // not yet supported by discrete model generation.
845                let BaseTmV_::Neu(n, _) = &**obj else {
846                    return Err("model generation does not yet support generators over a modal \
847                         object (list/tensor)"
848                        .into());
849                };
850                instance.add_generator(qname, n.to_qualified_name());
851            }
852            FiberTyV_::Record(sub_fields) => {
853                if let NameSegment::Uuid(uuid) = name {
854                    namespace.set_label(*uuid, *label);
855                }
856                // Labels of the sub-instance's own generators live in an
857                // inner namespace keyed by the import's name, mirroring the
858                // structure of the flattened qualified names.
859                let mut sub_ns = Namespace::new_for_uuid();
860                let mut sub_prefix = prefix.to_vec();
861                sub_prefix.push(*name);
862                extract_instance_record(instance, &mut sub_ns, &sub_prefix, sub_fields)?;
863                namespace.add_inner(*name, sub_ns);
864            }
865            FiberTyV_::Id(_, _, _) => {}
866        }
867    }
868    for (_, (_, field_ty)) in fields.iter() {
869        if let FiberTyV_::Id(_, lhs, rhs) = &**field_ty {
870            let lhs_t = fiber_tm_to_discrete_instance_term(instance, lhs, prefix)?;
871            let rhs_t = fiber_tm_to_discrete_instance_term(instance, rhs, prefix)?;
872            instance.add_equation(lhs_t, rhs_t);
873        }
874    }
875    Ok(())
876}
877
878/// Convert a fiber term into a [`DiscreteInstanceTerm`], prefixing each
879/// generator name with the path into any enclosing sub-instances.
880///
881/// Nested [`OverApp`](FiberTmV_::OverApp)s are flattened into a single
882/// morphism path applied to the generator at the leaf, matching the flat
883/// canonical shape of [`DiscreteInstanceTerm`].
884fn fiber_tm_to_discrete_instance_term(
885    instance: &DiscreteDblModelInstance,
886    tm: &FiberTmV,
887    prefix: &[NameSegment],
888) -> Result<DiscreteInstanceTerm, String> {
889    // Walk outer-to-inner, recording each applied morphism.
890    let mut mors_outer_first: Vec<QualifiedName> = Vec::new();
891    let mut cur = tm;
892    let base: QualifiedName = loop {
893        match &**cur {
894            FiberTmV_::OverApp(mor_path, _, inner) => {
895                let name: QualifiedName =
896                    mor_path.iter().map(|(seg, _)| *seg).collect::<Vec<_>>().into();
897                mors_outer_first.push(name);
898                cur = inner;
899            }
900            _ => {
901                let mut segs = prefix.to_vec();
902                segs.extend(fiber_full_name(cur)?);
903                break segs.into();
904            }
905        }
906    };
907    // Path order is innermost-first (apply first goes first).
908    mors_outer_first.reverse();
909    let path = match Path::from_vec(mors_outer_first) {
910        Some(p) => p,
911        None => {
912            let fiber = instance
913                .fiber_of(&base)
914                .ok_or_else(|| format!("instance term mentions unknown generator {base}"))?;
915            Path::Id(fiber.clone())
916        }
917    };
918    Ok(DiscreteInstanceTerm { path, base })
919}
920
921/// Read off the full name of a fiber term that is a generator or a chain
922/// of projections out of a sub-instance import (e.g. `we.e`): the leading
923/// variable contributes a segment (it is itself a generator/import name),
924/// followed by each projected field.
925fn fiber_full_name(tm: &FiberTmV) -> Result<Vec<NameSegment>, String> {
926    let mut segments = Vec::new();
927    let mut cur = tm;
928    loop {
929        match &**cur {
930            FiberTmV_::Var(_, name, _) => {
931                segments.push(*name);
932                break;
933            }
934            FiberTmV_::Proj(inner, f, _) => {
935                segments.push(*f);
936                cur = inner;
937            }
938            _ => return Err("expected a fiber generator or projection".into()),
939        }
940    }
941    segments.reverse();
942    Ok(segments)
943}
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948    use crate::tt::text_elab::{TT_PARSE_CONFIG, TopElabResult, TopElaborator};
949    use crate::tt::theory::std_theories;
950
951    fn elaborate_to_toplevel(src: &str) -> Toplevel {
952        let reporter = Reporter::new();
953        let mut toplevel = Toplevel::new(std_theories());
954        let _ = TT_PARSE_CONFIG.with_parsed_top(src, reporter.clone(), |topntns| {
955            let mut topelab = TopElaborator::new(reporter.clone());
956            for topntn in topntns.iter() {
957                if let Some(TopElabResult::Declaration(name, decl)) =
958                    topelab.elab(&toplevel, topntn)
959                {
960                    toplevel.declarations.insert(name, decl);
961                }
962            }
963            Some(())
964        });
965        assert!(!reporter.errored(), "elaboration produced errors");
966        toplevel
967    }
968
969    #[test]
970    fn instance_over_weighted_graph() {
971        let src = r#"
972set_theory ThSchema
973
974model WeightedGraph := [
975  V : Entity,
976  E : Entity,
977  Weight : AttrType,
978  src : (Hom Entity)[E, V],
979  tgt : (Hom Entity)[E, V],
980  weight : Attr[E, Weight]
981]
982
983instance I : WeightedGraph := [
984    V := [v],
985    E := [e],
986    src(e) := v
987]
988"#;
989        let toplevel = elaborate_to_toplevel(src);
990        let def = match toplevel.declarations.get(&name_seg("I")) {
991            Some(TopDecl::Instance(i)) => i.clone(),
992            _ => panic!("expected I to be an instance declaration"),
993        };
994        let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap();
995        let ModelInstance::Discrete(instance) = instance else {
996            panic!("expected a discrete instance");
997        };
998
999        let e_qname: QualifiedName = vec![name_seg("e")].into();
1000        let e_fiber: QualifiedName = vec![name_seg("E")].into();
1001        assert_eq!(instance.fiber_of(&e_qname), Some(&e_fiber));
1002        assert_eq!(instance.equations().count(), 1);
1003    }
1004
1005    /// An instance carrying fiber equations, imported as a sub-instance.
1006    ///
1007    /// `Loop` is itself a fiber record carrying an `Id`-typed field per
1008    /// fiber equation alongside its `e`/`v` generators, and extraction of
1009    /// the importer must recover both copies of the loop's structure.
1010    #[test]
1011    fn import_of_instance_with_fiber_equations() {
1012        let src = r#"
1013set_theory ThSchema
1014
1015model WeightedGraph := [
1016  V : Entity,
1017  E : Entity,
1018  Weight : AttrType,
1019  src : (Hom Entity)[E, V],
1020  tgt : (Hom Entity)[E, V],
1021  weight : Attr[E, Weight]
1022]
1023
1024instance Loop : WeightedGraph := [
1025    V := [v],
1026    E := [e],
1027    src(e) := v,
1028    tgt(e) := v
1029]
1030
1031instance UseLoop : WeightedGraph := [
1032    l : Loop
1033]
1034"#;
1035        let toplevel = elaborate_to_toplevel(src);
1036
1037        // `Loop` is a fiber record exposing its two fiber equations as
1038        // `Id`-typed fields alongside the `e`/`v` generators.
1039        let loop_def = match toplevel.declarations.get(&name_seg("Loop")) {
1040            Some(TopDecl::Instance(i)) => i.clone(),
1041            _ => panic!("expected Loop to be an instance declaration"),
1042        };
1043        let FiberTyV_::Record(r) = &*loop_def.val else {
1044            panic!("Loop should be a fiber record");
1045        };
1046        assert!(r.get(name_seg("e")).is_some(), "generator field e");
1047        assert!(r.get(name_seg("v")).is_some(), "generator field v");
1048        assert!(r.get(name_seg("_eq0")).is_some(), "first fiber equation");
1049        assert!(r.get(name_seg("_eq1")).is_some(), "second fiber equation");
1050
1051        // The importer still extracts both generators and the imported
1052        // copy's two equations.
1053        let use_def = match toplevel.declarations.get(&name_seg("UseLoop")) {
1054            Some(TopDecl::Instance(i)) => i.clone(),
1055            _ => panic!("expected UseLoop to be an instance declaration"),
1056        };
1057        let (instance, _ns) =
1058            instance_from_def(&toplevel, &use_def.theory.definition, &use_def).unwrap();
1059        let ModelInstance::Discrete(instance) = instance else {
1060            panic!("expected a discrete instance");
1061        };
1062        let le_qname: QualifiedName = vec![name_seg("l"), name_seg("e")].into();
1063        let e_fiber: QualifiedName = vec![name_seg("E")].into();
1064        assert_eq!(instance.fiber_of(&le_qname), Some(&e_fiber));
1065        assert_eq!(instance.equations().count(), 2);
1066    }
1067
1068    /// A modal (multicategory) instance: generators lie over plain objects and
1069    /// equations are list-domain morphism applications.
1070    #[test]
1071    fn instance_over_multicategory_monoid() {
1072        let src = r#"
1073set_theory ThMulticategory
1074
1075model SigMonoid := [
1076  M : Object,
1077  op : Multihom[[M, M], M],
1078  unit : Multihom[[], M]
1079]
1080
1081instance Z2 : SigMonoid := [
1082    M := [x],
1083    op([x,x]) := unit([]),
1084    op([x,unit([])]) := x
1085]
1086"#;
1087        let toplevel = elaborate_to_toplevel(src);
1088        let def = match toplevel.declarations.get(&name_seg("Z2")) {
1089            Some(TopDecl::Instance(i)) => i.clone(),
1090            _ => panic!("expected Z2 to be an instance declaration"),
1091        };
1092        let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap();
1093        let ModelInstance::ModalUnital(instance) = instance else {
1094            panic!("expected a unital modal instance");
1095        };
1096
1097        let x_qname: QualifiedName = vec![name_seg("x")].into();
1098        let m_fiber = modal::ModalOb::Generator(vec![name_seg("M")].into());
1099        assert_eq!(instance.fiber_of(&x_qname), Some(&m_fiber));
1100        assert_eq!(instance.equations().count(), 2);
1101    }
1102
1103    /// A symmetric-monoidal instance: equation terms feed generators through
1104    /// the `@tensor` object operation into unary homs between product objects.
1105    #[test]
1106    fn instance_over_symmetric_monoidal() {
1107        let src = r#"
1108set_theory ThSymMonoidalCategory
1109
1110model AB := [
1111  A : Object,
1112  B : Object,
1113  f : (Hom Object)[@tensor [A, B], A]
1114]
1115
1116instance i : AB := [
1117    A := [a],
1118    B := [b],
1119    f(@tensor [a, b]) := a
1120]
1121"#;
1122        let toplevel = elaborate_to_toplevel(src);
1123        let def = match toplevel.declarations.get(&name_seg("i")) {
1124            Some(TopDecl::Instance(i)) => i.clone(),
1125            _ => panic!("expected i to be an instance declaration"),
1126        };
1127        let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap();
1128        let ModelInstance::ModalUnital(instance) = instance else {
1129            panic!("expected a unital modal instance");
1130        };
1131
1132        let a_qname: QualifiedName = vec![name_seg("a")].into();
1133        let a_fiber = modal::ModalOb::Generator(vec![name_seg("A")].into());
1134        assert_eq!(instance.fiber_of(&a_qname), Some(&a_fiber));
1135        assert_eq!(instance.equations().count(), 1);
1136    }
1137
1138    /// A symmetric-monoidal instance whose equation feeds a *morphism*
1139    /// application through `@tensor`, exercising the functorial action of the
1140    /// object operation (a `HomApp`) rather than a pure object-op base.
1141    #[test]
1142    fn instance_over_symmetric_monoidal_functorial() {
1143        let src = r#"
1144set_theory ThSymMonoidalCategory
1145
1146model Chain := [
1147  X : Object,
1148  Y : Object,
1149  s : (Hom Object)[X, Y],
1150  t : (Hom Object)[@tensor [Y, Y], X]
1151]
1152
1153instance chain : Chain := [
1154    X := [x0],
1155    t(@tensor [s(x0), s(x0)]) := x0
1156]
1157"#;
1158        let toplevel = elaborate_to_toplevel(src);
1159        let def = match toplevel.declarations.get(&name_seg("chain")) {
1160            Some(TopDecl::Instance(i)) => i.clone(),
1161            _ => panic!("expected chain to be an instance declaration"),
1162        };
1163        let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap();
1164        let ModelInstance::ModalUnital(instance) = instance else {
1165            panic!("expected a unital modal instance");
1166        };
1167        assert_eq!(instance.equations().count(), 1);
1168
1169        // The left-hand side's morphism is a HomApp: the object operation
1170        // `tensor` acting on the list morphism `[s, s]`.
1171        let (lhs, _) = instance.equations().next().unwrap();
1172        assert!(
1173            matches!(&lhs.mor, modal::ModalMor::Composite(_)),
1174            "expected the applied morphism to compose `t` after the tensor's HomApp"
1175        );
1176    }
1177}