From 54e4d02ae2364a89ebcbec2c3d83a0d06b668d09 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 29 Apr 2026 14:24:43 -0700 Subject: [PATCH 01/53] @over types --- packages/catlog/src/tt/context.rs | 17 +++++++++ packages/catlog/src/tt/eval.rs | 34 +++++++++++++++++ packages/catlog/src/tt/modelgen.rs | 1 + packages/catlog/src/tt/stx.rs | 17 +++++++++ packages/catlog/src/tt/text_elab.rs | 58 ++++++++++++++++++++++++++++- packages/catlog/src/tt/toplevel.rs | 26 ++++++++++++- packages/catlog/src/tt/val.rs | 10 +++++ 7 files changed, 160 insertions(+), 3 deletions(-) diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 0e313d478..6b6b50d4c 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -75,4 +75,21 @@ impl Context { .find(|(_, v)| v.name == name) .map(|(i, v)| (i.into(), v.label, v.ty.clone())) } + + /// Lookup a variable by label. + /// + /// Used to find variables bound under a reserved label (e.g. `@diag`) + /// where the caller wants to recover the variable's name rather than + /// look it up by name. + pub fn lookup_by_label( + &self, + label: LabelSegment, + ) -> Option<(BwdIdx, VarName, Option)> { + self.scope + .iter() + .rev() + .enumerate() + .find(|(_, v)| v.label == label) + .map(|(i, v)| (i.into(), v.name, v.ty.clone())) + } } diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index bce90d26f..8eba2112e 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -66,6 +66,7 @@ impl<'a> Evaluator<'a> { } TyS_::Unit => TyV::unit(), TyS_::Meta(mv) => TyV::meta(*mv), + TyS_::Over(name, path) => TyV::over(*name, path.clone()), } } @@ -192,6 +193,7 @@ impl<'a> Evaluator<'a> { } TyV_::Unit => TyS::unit(), TyV_::Meta(mv) => TyS::meta(*mv), + TyV_::Over(name, path) => TyS::over(*name, path.clone()), } } @@ -255,6 +257,7 @@ impl<'a> Evaluator<'a> { TyV_::Id(_, _, _) => Ok(()), TyV_::Unit => Ok(()), TyV_::Meta(_) => Ok(()), + TyV_::Over(_, _) => Ok(()), } } @@ -321,6 +324,7 @@ impl<'a> Evaluator<'a> { TyV_::Id(_, _, _) => TmV::tt(), // Extensional equality at a 100% discount! TyV_::Unit => TmV::tt(), TyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), + TyV_::Over(_, _) => TmV::neu(n.clone(), ty.clone()), } } @@ -423,6 +427,8 @@ impl<'a> Evaluator<'a> { } } + // TODO: refactor to use [`Evaluator::path_ty`] for the descent and + // keep only the subtype check here. fn can_specialize( &self, ty: &TyV, @@ -455,6 +461,34 @@ impl<'a> Evaluator<'a> { } } + /// Walk `path` from the value `val` of record type `ty`, returning + /// the type of the field at the end of the path. + /// + /// An empty path returns `ty` unchanged. Each segment requires the + /// current type to be a record containing the named field. + pub fn path_ty( + &self, + ty: &TyV, + val: &TmV, + path: &[(FieldName, LabelSegment)], + ) -> Result { + let mut ty = ty.clone(); + let mut val = val.clone(); + for &(name, label) in path { + let TyV_::Record(r) = &*ty.clone() else { + return Err(format!("expected a record type at .{label}")); + }; + if !r.fields.has(name) { + return Err(format!("no such field .{label}")); + } + let next_ty = self.field_ty(&ty, &val, name); + let next_val = self.proj(&val, name, label); + ty = next_ty; + val = next_val; + } + Ok(ty) + } + /// Try to specialize the record `r` with the subtype `ty` at `path`. /// /// Precondition: `path` is non-empty. diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 3b5bcb3b0..9128f49a3 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -398,6 +398,7 @@ impl<'a> ModelGenerator<'a> { } TyV_::Unit => None, TyV_::Meta(_) => None, + TyV_::Over(_, _) => None, } } } diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index e01fc7886..2187283d7 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -94,6 +94,15 @@ pub enum TyS_ { /// Currently, this is only used for handling elaboration errors, we might /// add more unification/holes later. Meta(MetaVar), + + /// The type of terms of a fiber over a generator of a diagram's + /// codomain model. + /// + /// The first component names a top-level diagram declaration; the + /// second is a path into that diagram's theory record, identifying + /// an object generator. Example surface syntax: `e : @over .E`, + /// where `E` is a field of the enclosing diagram's theory. + Over(TopVarName, Vec<(FieldName, LabelSegment)>), } /// Syntax for total types, dereferences to [TyS_]. @@ -152,6 +161,11 @@ impl TyS { pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TyS_::Meta(mv))) } + + /// Smart constructor for [TyS], [TyS_::Over] case. + pub fn over(diag_name: TopVarName, path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(TyS_::Over(diag_name, path))) + } } impl ToDoc for TyS { @@ -176,6 +190,9 @@ impl ToDoc for TyS { ), TyS_::Unit => t("Unit"), TyS_::Meta(mv) => t(format!("?{}", mv.id)), + TyS_::Over(diag_name, path) => { + t(format!("@over({}){}", diag_name, path_to_string(path))) + } } } } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 64532fc95..dc78d3bf6 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -24,7 +24,7 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( ("==", Prec::nonassoc(30)), ], &[":", ":=", "&", "Unit", "Hom", "*", "=="], - &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory"], + &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory", "diagram"], ); /// The result of elaborating a top-level statement. @@ -129,6 +129,29 @@ impl TopElaborator { TopDecl::Type(Type::new(theory.clone(), ty_s, ty_v)), )) } + "diagram" => { + let theory = self.get_theory(tn.loc)?; + let mut elab = self.elaborator(&theory, toplevel); + let (name, args_n, annotn, valn) = self.annotated_def(tn.body).or_else(|| { + self.error( + tn.loc, + "unknown syntax for diagram declaration, expected : := ", + ) + })?; + if args_n.is_some() { + return self.error(tn.loc, "diagrams cannot have arguments"); + } + let (_, ret_ty_v) = elab.ty(annotn); + // Bind the diagram's name in scope under the reserved + // label `@diag` so that `@over .E` inside the body can + // recover both the diagram's name and its codomain type. + elab.intro(name, label_seg("@diag"), Some(ret_ty_v.clone())); + let (val_s, val_v) = elab.ty(valn); + Some(TopElabResult::Declaration( + name, + TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), + )) + } "def" => { let theory = self.get_theory(tn.loc)?; let (name, args_n, ty_n, tm_n) = self.annotated_def(tn.body).or_else(|| { @@ -379,6 +402,9 @@ impl<'a> Elaborator<'a> { )) } } + TopDecl::Diag(_) => { + self.ty_error(format!("{name} refers to a diagram, not a type")) + } TopDecl::Def(_) | TopDecl::DefConst(_) => { self.ty_error(format!("{name} refers to a term not a type")) } @@ -387,7 +413,6 @@ impl<'a> Elaborator<'a> { self.ty_error(format!("no such type {name} defined")) } } - fn morphism_ty(&mut self, n: &FNtn) -> Option<(MorType, ObType, ObType)> { let elab = self.enter(n.loc()); let theory = elab.theory(); @@ -459,6 +484,32 @@ impl<'a> Elaborator<'a> { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); (TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v)) } + App1(L(_, Prim("over")), p_n) => { + let Some(path) = elab.path(p_n) else { + return elab.ty_hole(); + }; + // Recover the enclosing diagram from the reserved `@diag` + // binding pushed by the `diagram` toplevel arm. + let Some((_, diag_name, model_ty)) = + elab.ctx.lookup_by_label(label_seg("@diag")) + else { + return elab.ty_error("@over is only allowed inside a diagram declaration"); + }; + let Some(model_ty) = model_ty else { + return elab.ty_error("enclosing diagram has no known codomain type"); + }; + let evaluator = elab.evaluator(); + let (self_n, _) = evaluator.bind_self(model_ty.clone()); + let self_val = evaluator.eta_neu(&self_n, &model_ty); + let resolved = match evaluator.path_ty(&model_ty, &self_val, &path) { + Ok(t) => t, + Err(msg) => return elab.ty_error(msg), + }; + let TyV_::Object(_) = &*resolved else { + return elab.ty_error("path does not refer to an object generator"); + }; + (TyS::over(diag_name, path.clone()), TyV::over(diag_name, path)) + } App1(mt_n, L(_, Tuple(domcod_n))) => { let [dom_n, cod_n] = domcod_n.as_slice() else { return elab.ty_error("expected two arguments for morphism type"); @@ -568,6 +619,9 @@ impl<'a> Elaborator<'a> { TopDecl::Type(_) => self.syn_error(format!("{name} refers type, not term")), TopDecl::DefConst(d) => (TmS::topvar(name), d.val.clone(), d.ty.clone()), TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")), + TopDecl::Diag(_) => { + self.syn_error(format!("{name} refers to a diagram, not a term")) + } } } else { self.syn_error(format!("no such variable {name}")) diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index e9f447bcf..6c96febb9 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -1,6 +1,7 @@ //! Data structures for managing toplevel declarations in the type theory. //! -//! Specifically, notebooks will produce [TopDecl::Type] declarations. +//! Specifically, notebooks will produce [TopDecl::Type] declarations, or +//! maybe [TopDecl::Diag] declartions. use derive_more::Constructor; @@ -16,6 +17,8 @@ pub enum TopDecl { DefConst(DefConst), /// See [Def]. Def(Def), + /// See [Diag] + Diag(Diag), } /// A toplevel declaration of a type. @@ -64,6 +67,19 @@ pub struct Def { pub body: TmS, } +/// A toplevel declaration of a diagram. +#[derive(Constructor, Clone)] +pub struct Diag { + /// The theory that the diagram is defined in. + pub theory: Theory, + /// The model G that this diagram is an instance of. + pub model: TyV, + /// The body: a record TyS whose fields have @over-typed entries, + /// presenting both the domain generators and the mapping. + pub body_stx: TyS, + /// Evaluated body — useful for downstream consumers that want the TyV directly. + pub body_val: TyV, +} impl TopDecl { /// Unwraps the type for a toplevel-declaration of a type, or panics. /// @@ -94,6 +110,14 @@ impl TopDecl { _ => panic!("top-level should be a term judgment"), } } + + /// Unwraps the diagram for a toplevel diagram declaration, or panics. + pub fn unwrap_diag(self) -> Diag { + match self { + TopDecl::Diag(d) => d, + _ => panic!("top-level should be a diagram declaration"), + } + } } /// Storage for toplevel declarations. diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 3caa62e11..b00a4a554 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -98,6 +98,11 @@ pub enum TyV_ { Unit, /// A metavariable, also see [TyS_::Meta]. Meta(MetaVar), + /// The type of terms of a fiber over a generator of a diagram's + /// codomain model. + /// + /// See [TyS_::Over] for the syntactic counterpart. + Over(TopVarName, Vec<(FieldName, LabelSegment)>), } /// Value for total types, dereferences to [TyV_]. @@ -177,6 +182,11 @@ impl TyV { pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TyV_::Meta(mv))) } + + /// Smart constructor for [TyV], [TyV_::Over] case. + pub fn over(diag_name: TopVarName, path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(TyV_::Over(diag_name, path))) + } } /// Inner enum for [TmN]. From 77b56dbf03afab3a3b3cf2a9db011a62fe3034ce Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 29 Apr 2026 15:04:25 -0700 Subject: [PATCH 02/53] Instance without specializations declares --- packages/catlog/src/tt/context.rs | 50 +++++++++++++++++-------- packages/catlog/src/tt/notebook_elab.rs | 4 +- packages/catlog/src/tt/text_elab.rs | 38 +++++++++++++------ 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 6b6b50d4c..8a60f19c7 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -4,6 +4,21 @@ use derive_more::Constructor; use crate::tt::{prelude::*, val::*}; +/// What kind of binding a context entry represents. +/// +/// Most bindings are ordinary `Term` bindings. `Diagram` bindings are pushed +/// by the `diagram` toplevel arm so that `@over .E` can recover the +/// enclosing diagram's name and codomain type without exposing the diagram +/// to ordinary term lookup (where it would masquerade as a term of the +/// codomain type). +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum VarKind { + /// An ordinary term-level binding. + Term, + /// A binding for a diagram declaration; not resolvable by [`Context::lookup`]. + Diagram, +} + /// Each variable in context is associated with a label and a type. /// /// Multiple variables with the same name can show up in context; in this case @@ -19,6 +34,8 @@ pub struct VarInContext { /// We allow the type to be null as a hack for the `self` variable before we /// know the type of the `self` variable. pub ty: Option, + /// What kind of binding this is. + pub kind: VarKind, } /// The variable context during elaboration. @@ -61,35 +78,38 @@ impl Context { self.scope.truncate(c.scope); } - /// Add a new variable to scope (note: does not add it to the environment). + /// Add a new term-kind variable to scope (note: does not add it to the environment). pub fn push_scope(&mut self, name: VarName, label: LabelSegment, ty: Option) { - self.scope.push(VarInContext::new(name, label, ty)) + self.scope.push(VarInContext::new(name, label, ty, VarKind::Term)) } - /// Lookup a variable by name. + /// Add a new diagram-kind variable to scope. + pub fn push_diagram(&mut self, name: VarName, label: LabelSegment, ty: TyV) { + self.scope.push(VarInContext::new(name, label, Some(ty), VarKind::Diagram)) + } + + /// Lookup a term-kind variable by name. + /// + /// Diagram-kind entries are skipped, so a `Var(I)` referring to an + /// enclosing `diagram I := ...` will not resolve here. pub fn lookup(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, Option)> { self.scope .iter() .rev() .enumerate() - .find(|(_, v)| v.name == name) + .find(|(_, v)| v.kind == VarKind::Term && v.name == name) .map(|(i, v)| (i.into(), v.label, v.ty.clone())) } - /// Lookup a variable by label. + /// Find the most recent diagram-kind binding in scope, if any. /// - /// Used to find variables bound under a reserved label (e.g. `@diag`) - /// where the caller wants to recover the variable's name rather than - /// look it up by name. - pub fn lookup_by_label( - &self, - label: LabelSegment, - ) -> Option<(BwdIdx, VarName, Option)> { + /// Returns the diagram's name and codomain type. Used by the `@over .E` + /// type elaborator. + pub fn lookup_diagram(&self) -> Option<(VarName, TyV)> { self.scope .iter() .rev() - .enumerate() - .find(|(_, v)| v.label == label) - .map(|(i, v)| (i.into(), v.name, v.ty.clone())) + .find(|v| v.kind == VarKind::Diagram) + .map(|v| (v.name, v.ty.clone().expect("diagram binding must have a type"))) } } diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index f84623bb8..349579c3d 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -78,7 +78,7 @@ impl<'a> Elaborator<'a> { v }; self.ctx.env = self.ctx.env.snoc(v.clone()); - self.ctx.scope.push(VarInContext::new(name, label, ty)); + self.ctx.scope.push(VarInContext::new(name, label, ty, VarKind::Term)); v } @@ -452,7 +452,7 @@ impl<'a> Elaborator<'a> { nb::ModelJudgment::Equation(eqn_decl) => self.equation_cell(eqn_decl), }; field_ty_vs.push((name, (label, ty_v.clone()))); - self.ctx.scope.push(VarInContext::new(name, label, Some(ty_v.clone()))); + self.ctx.scope.push(VarInContext::new(name, label, Some(ty_v.clone()), VarKind::Term)); self.ctx.env = self.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index dc78d3bf6..6aad0a5b9 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -142,10 +142,15 @@ impl TopElaborator { return self.error(tn.loc, "diagrams cannot have arguments"); } let (_, ret_ty_v) = elab.ty(annotn); - // Bind the diagram's name in scope under the reserved - // label `@diag` so that `@over .E` inside the body can - // recover both the diagram's name and its codomain type. - elab.intro(name, label_seg("@diag"), Some(ret_ty_v.clone())); + // Bind the diagram's name as a diagram-kind context entry + // so that `@over .E` inside the body can recover both the + // diagram's name and its codomain type, while ordinary + // term lookups cannot see it. + let diag_label = match name { + NameSegment::Text(s) => label_seg(s), + NameSegment::Uuid(_) => label_seg("diag"), + }; + elab.intro_diagram(name, diag_label, ret_ty_v.clone()); let (val_s, val_v) = elab.ty(valn); Some(TopElabResult::Declaration( name, @@ -375,6 +380,15 @@ impl<'a> Elaborator<'a> { v } + /// Like [`Self::intro`], but pushes a diagram-kind binding so it is + /// invisible to ordinary term lookup. + fn intro_diagram(&mut self, name: VarName, label: LabelSegment, ty: TyV) { + let v = TmV::neu(TmN::var(self.ctx.scope.len().into(), name, label), ty.clone()); + let v = self.evaluator().eta(&v, Some(&ty)); + self.ctx.env = self.ctx.env.snoc(v); + self.ctx.push_diagram(name, label, ty); + } + fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, TyS, TyV)> { let mut elab = self.enter(n.loc()); match n.ast0() { @@ -484,20 +498,20 @@ impl<'a> Elaborator<'a> { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); (TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v)) } + // `@Instance(X)` is a structural alias for `X`, used in diagram + // annotations. Treating it as an alias keeps the type system + // simple; `I` is kept out of term lookup by the diagram-kind + // binding pushed in the `diagram` toplevel arm. + App1(L(_, Prim("Instance")), x_n) => elab.ty(x_n), App1(L(_, Prim("over")), p_n) => { let Some(path) = elab.path(p_n) else { return elab.ty_hole(); }; - // Recover the enclosing diagram from the reserved `@diag` - // binding pushed by the `diagram` toplevel arm. - let Some((_, diag_name, model_ty)) = - elab.ctx.lookup_by_label(label_seg("@diag")) - else { + // Recover the enclosing diagram from the most recent + // diagram-kind binding pushed by the `diagram` toplevel arm. + let Some((diag_name, model_ty)) = elab.ctx.lookup_diagram() else { return elab.ty_error("@over is only allowed inside a diagram declaration"); }; - let Some(model_ty) = model_ty else { - return elab.ty_error("enclosing diagram has no known codomain type"); - }; let evaluator = elab.evaluator(); let (self_n, _) = evaluator.bind_self(model_ty.clone()); let self_val = evaluator.eta_neu(&self_n, &model_ty); From 27c738a478aa0c66a43f1ba65e3c321bc29c6a98 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 4 May 2026 18:42:17 -0700 Subject: [PATCH 03/53] Diagrams in discrete theories declaring and validating, generating DiscreteDblModelDiagrams, behaving as dopfs --- packages/catlog/src/tt/batch.rs | 114 ++++++++++++++- packages/catlog/src/tt/eval.rs | 21 ++- packages/catlog/src/tt/modelgen.rs | 209 +++++++++++++++++++++++++++- packages/catlog/src/tt/stx.rs | 24 ++-- packages/catlog/src/tt/text_elab.rs | 158 ++++++++++++++++++++- packages/catlog/src/tt/val.rs | 13 +- 6 files changed, 505 insertions(+), 34 deletions(-) diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 42be88292..c333ab36e 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -11,8 +11,18 @@ use scopeguard::guard; use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; -use super::{text_elab::*, theory::std_theories, toplevel::*}; -use crate::zero::NameSegment; +use super::{ + modelgen::{Model, diagram_from_diag}, + text_elab::*, + theory::std_theories, + toplevel::*, +}; +use crate::dbl::discrete::{DiscreteDblModelDiagram, InvalidDiscreteDblModelDiagram}; +use crate::dbl::model::{FpDblModel, MutDblModel}; +use crate::dbl::model_diagram::DblModelDiagram; +use crate::one::category::FgCategory; +use crate::one::path::Path; +use crate::zero::{Mapping, NameSegment, QualifiedName}; declare_error!(TOP_ERROR, "top", "an error at the top-level"); @@ -64,6 +74,69 @@ impl BatchOutput { } } + fn diagram_summary(&self, diagram: &DiscreteDblModelDiagram) { + if let BatchOutput::Snapshot(out) = self { + let DblModelDiagram(mapping, domain) = diagram; + let mut out = out.borrow_mut(); + let ob_gens: Vec<_> = domain.ob_generators().collect(); + let mor_gens: Vec<_> = domain.mor_generators().collect(); + if ob_gens.is_empty() && mor_gens.is_empty() { + writeln!(out, "#/ diagram has no domain generators").unwrap(); + return; + } + writeln!(out, "#/ diagram domain:").unwrap(); + for g in &ob_gens { + let ot = domain.ob_generator_type(g); + match mapping.0.ob_generator_map.apply_to_ref(g) { + Some(target) => writeln!(out, "#/ {g} : {ot} -> {target}").unwrap(), + None => writeln!(out, "#/ {g} : {ot}").unwrap(), + } + } + for g in &mor_gens { + let mt = format_path(&domain.mor_generator_type(g)); + let dom_str = domain + .get_dom(g) + .map(|o| format!("{o}")) + .unwrap_or_else(|| "?".into()); + let cod_str = domain + .get_cod(g) + .map(|o| format!("{o}")) + .unwrap_or_else(|| "?".into()); + let target_str = mapping + .0 + .mor_generator_map + .apply_to_ref(g) + .map(|p| format!(" -> {}", format_path(&p))) + .unwrap_or_default(); + writeln!(out, "#/ {g} : {mt}[{dom_str}, {cod_str}]{target_str}").unwrap(); + } + } + } + + fn diagram_error(&self, msg: &str) { + if let BatchOutput::Snapshot(out) = self { + writeln!(out.borrow_mut(), "#/ diagram generation failed: {msg}").unwrap(); + } + } + + fn validation_result>( + &self, + errs: I, + ) { + if let BatchOutput::Snapshot(out) = self { + let mut out = out.borrow_mut(); + let mut iter = errs.into_iter().peekable(); + if iter.peek().is_none() { + writeln!(out, "#/ diagram validates against codomain").unwrap(); + } else { + writeln!(out, "#/ diagram validation failed:").unwrap(); + for e in iter { + writeln!(out, "#/ {e:?}").unwrap(); + } + } + } + } + fn got_result(&self, result: &str) { match self { BatchOutput::Snapshot(out) => { @@ -179,8 +252,34 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result { + let is_diag = matches!(top_decl, TopDecl::Diag(_)); toplevel.declarations.insert(name_segment, top_decl); output.declared(name_segment); + if is_diag + && let Some(TopDecl::Diag(diag)) = + toplevel.declarations.get(&name_segment) + { + match diagram_from_diag( + &toplevel, + &diag.theory.definition, + diag, + ) { + Ok((model_diag, _)) => { + output.diagram_summary(&model_diag); + let (cod, _) = Model::from_ty( + &toplevel, + &diag.theory.definition, + &diag.model, + ); + if let Some(cod) = cod.as_discrete() { + output.validation_result( + model_diag.iter_invalid_in(&cod), + ); + } + } + Err(msg) => output.diagram_error(&msg), + } + } } TopElabResult::Output(s) => { output.got_result(&s); @@ -242,3 +341,14 @@ fn snapshot_examples() { } assert!(succeeded); } + +/// Render a [`QualifiedPath`] for snapshot output. +fn format_path(p: &Path) -> String { + match p { + Path::Id(v) => format!("Hom({v})"), + Path::Seq(es) => { + let parts: Vec = es.iter().map(|e| format!("{e}")).collect(); + parts.join(".") + } + } +} diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 8eba2112e..af27e23fc 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -49,7 +49,11 @@ impl<'a> Evaluator<'a> { /// to self.env. pub fn eval_ty(&self, ty: &TyS) -> TyV { match &**ty { - TyS_::TopVar(tv) => self.toplevel.declarations.get(tv).unwrap().clone().unwrap_ty().val, + TyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { + TopDecl::Type(t) => t.val.clone(), + TopDecl::Diag(d) => d.body_val.clone(), + _ => panic!("top-level {tv} should be a type or diagram declaration"), + }, TyS_::Object(ot) => TyV::object(ot.clone()), TyS_::Morphism(pt, dom, cod) => { TyV::morphism(pt.clone(), self.eval_tm(dom), self.eval_tm(cod)) @@ -66,7 +70,7 @@ impl<'a> Evaluator<'a> { } TyS_::Unit => TyV::unit(), TyS_::Meta(mv) => TyV::meta(*mv), - TyS_::Over(name, path) => TyV::over(*name, path.clone()), + TyS_::Over(path) => TyV::over(path.clone()), } } @@ -193,7 +197,7 @@ impl<'a> Evaluator<'a> { } TyV_::Unit => TyS::unit(), TyV_::Meta(mv) => TyS::meta(*mv), - TyV_::Over(name, path) => TyS::over(*name, path.clone()), + TyV_::Over(path) => TyS::over(path.clone()), } } @@ -257,7 +261,7 @@ impl<'a> Evaluator<'a> { TyV_::Id(_, _, _) => Ok(()), TyV_::Unit => Ok(()), TyV_::Meta(_) => Ok(()), - TyV_::Over(_, _) => Ok(()), + TyV_::Over(_) => Ok(()), } } @@ -302,6 +306,13 @@ impl<'a> Evaluator<'a> { (TyV_::Sing(ty1, _), _) => self.convertible_ty(ty1, ty2), (_, TyV_::Sing(ty2, _)) => self.convertible_ty(ty1, ty2), (TyV_::Unit, TyV_::Unit) => Ok(()), + (TyV_::Over(p1), TyV_::Over(p2)) => { + if p1 == p2 { + Ok(()) + } else { + Err(t("over-types refer to different paths in the codomain")) + } + } _ => Err(t("tried to convert between types of different type constructors")), } } @@ -324,7 +335,7 @@ impl<'a> Evaluator<'a> { TyV_::Id(_, _, _) => TmV::tt(), // Extensional equality at a 100% discount! TyV_::Unit => TmV::tt(), TyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), - TyV_::Over(_, _) => TmV::neu(n.clone(), ty.clone()), + TyV_::Over(_) => TmV::neu(n.clone(), ty.clone()), } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 9128f49a3..9be670f9c 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -8,10 +8,11 @@ use super::{eval::*, prelude::*, text_elab, theory::*, toplevel::*, val::*}; use crate::dbl::{ discrete, discrete_tabulator, modal, model::{DblModel, DblModelPrinter, MutDblModel}, + model_diagram::DblModelDiagram, theory::{DblTheory, DblTheoryKind, NonUnital, Unital}, }; use crate::one::{ - Category, + Category, QualifiedPath, path::{Path, PathEq}, }; use crate::zero::{Namespace, QualifiedName}; @@ -398,7 +399,211 @@ impl<'a> ModelGenerator<'a> { } TyV_::Unit => None, TyV_::Meta(_) => None, - TyV_::Over(_, _) => None, + TyV_::Over(_) => None, } } } + +/// Generates a [`DiscreteDblModelDiagram`] from an elaborated [`Diag`]. +/// +/// Restricted to discrete double theories for the moment; other theory +/// kinds will need their own diagram types in catlog before this can be +/// promoted to an enum mirroring [`Model`]. +pub fn diagram_from_diag( + toplevel: &Toplevel, + th: &TheoryDef, + diag: &Diag, +) -> Result<(discrete::DiscreteDblModelDiagram, Namespace), String> { + let TheoryDef::Discrete(theory) = th else { + return Err("diagram generation only supports discrete double theories".into()); + }; + let mut generator = DiagramGenerator { + eval: Evaluator::empty(toplevel), + codomain_ty: diag.model.clone(), + domain: discrete::DiscreteDblModel::new(theory.clone()), + mapping: discrete::DiscreteDblModelMapping::default(), + }; + let namespace = generator.generate(&diag.body_val)?; + let DiagramGenerator { domain, mapping, .. } = generator; + Ok((DblModelDiagram(mapping, domain), namespace)) +} + +struct DiagramGenerator<'a> { + eval: Evaluator<'a>, + codomain_ty: TyV, + domain: discrete::DiscreteDblModel, + mapping: discrete::DiscreteDblModelMapping, +} + +impl DiagramGenerator<'_> { + fn generate(&mut self, body_ty: &TyV) -> Result { + let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); + self.eval = eval_with_self; + let self_val = self.eval.eta_neu(&self_n, body_ty); + Ok(self.extract(vec![], &self_val, body_ty)?.unwrap_or_else(Namespace::new_for_uuid)) + } + + fn extract( + &mut self, + prefix: Vec, + val: &TmV, + ty: &TyV, + ) -> Result, String> { + match &**ty { + TyV_::Record(r) => { + let mut namespace = Namespace::new_for_uuid(); + for (name, (label, _)) in r.fields.iter() { + let mut child_prefix = prefix.clone(); + child_prefix.push(*name); + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let field_tm = self.eval.proj(val, *name, *label); + let field_ty = self.eval.field_ty(ty, val, *name); + if let Some(inner) = self.extract(child_prefix, &field_tm, &field_ty)? { + namespace.add_inner(*name, inner); + } + } + Ok(Some(namespace)) + } + TyV_::Over(path) => { + // Resolve the path against the codomain to find the + // catlog object type for this generator. + let (cod_self_n, cod_eval) = self.eval.bind_self(self.codomain_ty.clone()); + let cod_val = cod_eval.eta_neu(&cod_self_n, &self.codomain_ty); + let resolved = cod_eval.path_ty(&self.codomain_ty, &cod_val, path)?; + let TyV_::Object(ot) = &*resolved else { + return Err( + "@over path does not refer to an object generator in the codomain".into(), + ); + }; + let ot: QualifiedName = ot.clone().try_into().map_err(|_| { + "@over codomain object type is not valid for a discrete theory".to_string() + })?; + let qname: QualifiedName = prefix.into(); + self.domain.add_ob(qname.clone(), ot); + let target: QualifiedName = + path.iter().map(|(seg, _)| *seg).collect::>().into(); + self.mapping.assign_ob(qname, target); + Ok(None) + } + TyV_::Morphism(mt, dom, cod) => { + // Augmentation produces lift morphisms whose dom and cod + // are projections from the body's self, resolving to the + // qualified names of generators (declared or specialized). + let dom_name = neutral_qualified_name(dom).ok_or_else(|| { + "morphism domain does not resolve to a generator name".to_string() + })?; + let cod_name = neutral_qualified_name(cod).ok_or_else(|| { + "morphism codomain does not resolve to a generator name".to_string() + })?; + let mt_inner: QualifiedPath = mt.clone().try_into().map_err(|_| { + "morphism type is not valid for a discrete theory".to_string() + })?; + let qname: QualifiedName = prefix.clone().into(); + self.domain.add_mor(qname.clone(), dom_name, cod_name, mt_inner); + // Mapping target: extract the codomain morphism's name + // from the synthetic lift name `~f(e)`. For other shapes + // (e.g., a future user-declared morphism in a body), fall + // back to the field name itself. + if let Some(last) = prefix.last() + && let Some(cod_mor) = parse_lift_morphism_name(last) + { + let target = Path::single(QualifiedName::single(cod_mor)); + self.mapping.assign_mor(qname, target); + } + Ok(None) + } + TyV_::Object(_) + | TyV_::Sing(_, _) + | TyV_::Id(_, _, _) + | TyV_::Unit + | TyV_::Meta(_) => Ok(None), + } + } +} + +/// Read off a [`QualifiedName`] from a value that's a neutral chain of +/// projections from a self-variable (regardless of which level the self +/// is bound at). Returns `None` if the value isn't of that shape. +fn neutral_qualified_name(tm: &TmV) -> Option { + let TmV_::Neu(n, _) = &**tm else { + return None; + }; + Some(n.to_qualified_name()) +} + +/// Parse a synthetic lift-morphism field name of the form `~f(e)` and +/// return the codomain morphism's name `f`. Returns `None` if the name +/// isn't of that shape. +fn parse_lift_morphism_name(name: &NameSegment) -> Option { + let NameSegment::Text(s) = name else { + return None; + }; + let s = s.as_str(); + let rest = s.strip_prefix('~')?; + let paren = rest.find('(')?; + Some(name_seg(ustr(&rest[..paren]))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dbl::model::FpDblModel; + use crate::tt::text_elab::{TT_PARSE_CONFIG, TopElabResult, TopElaborator}; + use crate::tt::theory::std_theories; + use crate::zero::Mapping; + + fn elaborate_to_toplevel(src: &str) -> Toplevel { + let reporter = Reporter::new(); + let mut toplevel = Toplevel::new(std_theories()); + let _ = TT_PARSE_CONFIG.with_parsed_top(src, reporter.clone(), |topntns| { + let mut topelab = TopElaborator::new(reporter.clone()); + for topntn in topntns.iter() { + if let Some(TopElabResult::Declaration(name, decl)) = + topelab.elab(&toplevel, topntn) + { + toplevel.declarations.insert(name, decl); + } + } + Some(()) + }); + assert!(!reporter.errored(), "elaboration produced errors"); + toplevel + } + + #[test] + fn diagram_over_weighted_graph() { + let src = r#" +set_theory ThSchema + +type WeightedGraph := [ + V : Entity, + E : Entity, + Weight : AttrType, + src : (Hom Entity)[E, V], + tgt : (Hom Entity)[E, V], + weight : Attr[E, Weight] +] + +diagram I : @Instance(WeightedGraph) := [ + e : @over .E, +] +"#; + let toplevel = elaborate_to_toplevel(src); + let diag = match toplevel.declarations.get(&name_seg("I")) { + Some(TopDecl::Diag(d)) => d.clone(), + _ => panic!("expected I to be a diagram declaration"), + }; + let (DblModelDiagram(mapping, domain), _ns) = + diagram_from_diag(&toplevel, &diag.theory.definition, &diag).unwrap(); + + let e_qname: QualifiedName = vec![name_seg("e")].into(); + let entity_ot: QualifiedName = vec![name_seg("Entity")].into(); + let e_target: QualifiedName = vec![name_seg("E")].into(); + + assert!(domain.has_ob(&e_qname)); + assert_eq!(domain.ob_generator_type(&e_qname), entity_ot); + assert_eq!(mapping.0.ob_generator_map.apply_to_ref(&e_qname), Some(e_target)); + } +} diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 2187283d7..5bb3dbdf8 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -95,14 +95,16 @@ pub enum TyS_ { /// add more unification/holes later. Meta(MetaVar), - /// The type of terms of a fiber over a generator of a diagram's - /// codomain model. + /// The type of terms in a fiber over an object generator of some + /// diagram's codomain model. /// - /// The first component names a top-level diagram declaration; the - /// second is a path into that diagram's theory record, identifying - /// an object generator. Example surface syntax: `e : @over .E`, - /// where `E` is a field of the enclosing diagram's theory. - Over(TopVarName, Vec<(FieldName, LabelSegment)>), + /// The path identifies the object generator in the codomain. Diagram + /// identity is contextual rather than part of the type: any diagram + /// in scope contributes its terms to this type, so two `@over .V` + /// types from different diagram declarations are convertible. The + /// elaborator validates the path against the enclosing diagram's + /// codomain at construction time. Example surface syntax: `e : @over .E`. + Over(Vec<(FieldName, LabelSegment)>), } /// Syntax for total types, dereferences to [TyS_]. @@ -163,8 +165,8 @@ impl TyS { } /// Smart constructor for [TyS], [TyS_::Over] case. - pub fn over(diag_name: TopVarName, path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(TyS_::Over(diag_name, path))) + pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(TyS_::Over(path))) } } @@ -190,9 +192,7 @@ impl ToDoc for TyS { ), TyS_::Unit => t("Unit"), TyS_::Meta(mv) => t(format!("?{}", mv.id)), - TyS_::Over(diag_name, path) => { - t(format!("@over({}){}", diag_name, path_to_string(path))) - } + TyS_::Over(path) => t(format!("@over{}", path_to_string(path))), } } } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 6aad0a5b9..db6eb7d76 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -151,7 +151,13 @@ impl TopElaborator { NameSegment::Uuid(_) => label_seg("diag"), }; elab.intro_diagram(name, diag_label, ret_ty_v.clone()); - let (val_s, val_v) = elab.ty(valn); + let (val_s, _val_v) = elab.ty(valn); + // Augment the body with synthetic lift-target fields forced + // by the discrete-opfibration condition: for each declared + // `e : @over .X` and codomain morphism `f : X → Y`, add a + // synthetic field `f(e) : @over .Y`. + let val_s = augment_with_lifts(&val_s, &ret_ty_v); + let val_v = elab.evaluator().eval_ty(&val_s); Some(TopElabResult::Declaration( name, TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), @@ -416,8 +422,18 @@ impl<'a> Elaborator<'a> { )) } } - TopDecl::Diag(_) => { - self.ty_error(format!("{name} refers to a diagram, not a type")) + TopDecl::Diag(d) => { + if d.theory == self.theory { + // Using a diagram name as a type yields the diagram's + // body record, allowing `we : I & [...]`-style + // specializations to walk paths through I's fields. + (TyS::topvar(name), d.body_val.clone()) + } else { + self.ty_error(format!( + "{name} refers to a diagram in theory {}, expected theory {}", + d.theory, self.theory + )) + } } TopDecl::Def(_) | TopDecl::DefConst(_) => { self.ty_error(format!("{name} refers to a term not a type")) @@ -466,6 +482,23 @@ impl<'a> Elaborator<'a> { p.push((name_seg(*f), label_seg(*f))); Some(p) } + // `.f(x)` — applied path segment, resolved to a single synthetic + // field whose name is the canonical string `"f(x)"`. This makes + // the lift-target objects forced by the discrete-opfibration + // condition addressable by ordinary path-walking. + App1(head_n, L(_, Var(x))) => match head_n.ast0() { + Field(f) => { + let s = ustr(&format!("{f}({x})")); + Some(vec![(name_seg(s), label_seg(s))]) + } + App1(p_n, L(_, Field(f))) => { + let mut p = elab.path(p_n)?; + let s = ustr(&format!("{f}({x})")); + p.push((name_seg(s), label_seg(s))); + Some(p) + } + _ => elab.error("unexpected notation for path"), + }, _ => elab.error("unexpected notation for path"), } } @@ -507,9 +540,11 @@ impl<'a> Elaborator<'a> { let Some(path) = elab.path(p_n) else { return elab.ty_hole(); }; - // Recover the enclosing diagram from the most recent - // diagram-kind binding pushed by the `diagram` toplevel arm. - let Some((diag_name, model_ty)) = elab.ctx.lookup_diagram() else { + // The enclosing diagram-kind binding gives the codomain + // against which we validate the path. The diagram's + // identity is not stored in the resulting type — `@over` + // names a fiber-type that's shared across diagrams. + let Some((_, model_ty)) = elab.ctx.lookup_diagram() else { return elab.ty_error("@over is only allowed inside a diagram declaration"); }; let evaluator = elab.evaluator(); @@ -522,7 +557,7 @@ impl<'a> Elaborator<'a> { let TyV_::Object(_) = &*resolved else { return elab.ty_error("path does not refer to an object generator"); }; - (TyS::over(diag_name, path.clone()), TyV::over(diag_name, path)) + (TyS::over(path.clone()), TyV::over(path)) } App1(mt_n, L(_, Tuple(domcod_n))) => { let [dom_n, cod_n] = domcod_n.as_slice() else { @@ -842,6 +877,115 @@ impl<'a> Elaborator<'a> { } } +/// Augment a diagram's body with synthetic lift-target fields. +/// +/// For each declared `e : @over .X` field, and each codomain morphism +/// `f : X → Y`, add a synthetic field named `"f(e)" : @over .Y`. Iterates +/// to a fixpoint, with a depth bound to guard against cyclic theories. +/// +/// The synthetic fields exist only in the type-theoretic body so that +/// path-walking and specialization (e.g., `we : I & [.src(e) := v1]`) +/// can resolve `.src(e)` as an ordinary field. Modelgen filters them +/// out by name when extracting the domain model. +fn augment_with_lifts(body_s: &TyS, codomain: &TyV) -> TyS { + const MAX_DEPTH: usize = 16; + let TyS_::Record(initial) = &**body_s else { + return body_s.clone(); + }; + let TyV_::Record(cod_r) = &**codomain else { + return body_s.clone(); + }; + + // Collect (mor_name, mt, dom_path, cod_path) for each codomain + // morphism field whose source/target reduce to a single chain of + // fields. + type LiftPath = Vec<(NameSegment, LabelSegment)>; + type CodMor = (NameSegment, MorType, LiftPath, LiftPath); + let mut cod_morphisms: Vec = Vec::new(); + for (mor_name, (_, mor_ty_s)) in cod_r.fields.iter() { + if let TyS_::Morphism(mt, dom_s, cod_s) = &**mor_ty_s + && let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) + { + cod_morphisms.push((*mor_name, mt.clone(), dom_path, cod_path)); + } + } + + let mut row = initial.clone(); + for _ in 0..MAX_DEPTH { + let snapshot = row.clone(); + let mut added = false; + for (field_name, (field_label, field_ty)) in snapshot.iter() { + let TyS_::Over(path) = &**field_ty else { + continue; + }; + for (mor_name, mt, dom_path, cod_path) in &cod_morphisms { + if dom_path != path { + continue; + } + // Synthetic object field `f(e) : @over .Y`, the unique + // lift target forced by the discrete-opfibration condition. + let synth = ustr(&format!("{mor_name}({field_name})")); + let synth_seg = name_seg(synth); + let synth_label = label_seg(synth); + if !row.has(synth_seg) { + row.insert(synth_seg, synth_label, TyS::over(cod_path.clone())); + added = true; + } + // Synthetic morphism field `~f(e) : Morphism(mt, e, f(e))`, + // the lift morphism itself. The `~` prefix evokes the + // overline notation for lifts and disambiguates per source + // object so multiple declared `@over .X` fields can each + // get their own lift; modelgen strips the wrapper to + // recover the codomain morphism's name. + let lift_name = ustr(&format!("~{mor_name}({field_name})")); + let lift_seg = name_seg(lift_name); + let lift_label = label_seg(lift_name); + if !row.has(lift_seg) { + let self_var = TmS::var( + BwdIdx::from(0), + name_seg("self"), + label_seg("self"), + ); + let dom_term = TmS::proj(self_var.clone(), *field_name, *field_label); + let cod_term = TmS::proj(self_var, synth_seg, synth_label); + row.insert( + lift_seg, + lift_label, + TyS::morphism(mt.clone(), dom_term, cod_term), + ); + added = true; + } + } + } + if !added { + return TyS::record(row); + } + } + // Hit the depth bound — return what we have. Cyclic theories will + // produce an under-augmented body; this is a known limitation. + TyS::record(row) +} + +/// Read off a path of `(name, label)` segments from a term that is a chain +/// of projections rooted in a variable. +/// +/// The leading variable is treated as the implicit root (e.g., the `self` +/// of an enclosing record) and contributes no segment. So `Var(self)` +/// returns `[]` and `Proj(Var(self), E, E_label)` returns `[(E, E_label)]`, +/// matching the path representation produced by the surface `.E` syntax. +/// Returns `None` if the term has any other shape. +fn tms_to_path(tm: &TmS) -> Option> { + match &**tm { + TmS_::Var(_, _, _) => Some(vec![]), + TmS_::Proj(inner, name, label) => { + let mut p = tms_to_path(inner)?; + p.push((*name, *label)); + Some(p) + } + _ => None, + } +} + // NOTE: Most tests for the text elaborator are in the `examples` dir. #[cfg(test)] mod tests { diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index b00a4a554..428289b43 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -98,11 +98,12 @@ pub enum TyV_ { Unit, /// A metavariable, also see [TyS_::Meta]. Meta(MetaVar), - /// The type of terms of a fiber over a generator of a diagram's - /// codomain model. + /// The type of terms in a fiber over an object generator of some + /// diagram's codomain model. /// - /// See [TyS_::Over] for the syntactic counterpart. - Over(TopVarName, Vec<(FieldName, LabelSegment)>), + /// See [TyS_::Over] for the syntactic counterpart and explanation + /// of why diagram identity is not part of this type. + Over(Vec<(FieldName, LabelSegment)>), } /// Value for total types, dereferences to [TyV_]. @@ -184,8 +185,8 @@ impl TyV { } /// Smart constructor for [TyV], [TyV_::Over] case. - pub fn over(diag_name: TopVarName, path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(TyV_::Over(diag_name, path))) + pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(TyV_::Over(path))) } } From 9d1aeb60480e2d570b7d940ba671738d44505ea5 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 4 May 2026 18:47:42 -0700 Subject: [PATCH 04/53] Add the test files --- .../examples/tt/text/test_instances.dbltt | 28 ++++++++ .../tt/text/test_instances.dbltt.snapshot | 71 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 packages/catlog/examples/tt/text/test_instances.dbltt create mode 100644 packages/catlog/examples/tt/text/test_instances.dbltt.snapshot diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt new file mode 100644 index 000000000..9f9857a9e --- /dev/null +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -0,0 +1,28 @@ +set_theory ThSchema + +type WeightedGraph := [ + V : Entity, + E : Entity, + Weight : AttrType, + src : (Hom Entity)[E, V], + tgt : (Hom Entity)[E, V], + weight : Attr[E, Weight] +] + +diagram I : @Instance(WeightedGraph) := [ + e : @over .E, +] + +#/ Should there be some sugar for this situation, where I'm basically just renaming default-named guys? +diagram I2 : @Instance(WeightedGraph) := [ + v1 : @over .V, + v2 : @over .V, + w : @over .Weight, + we : I & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], + wf : I & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] +] + +diagram I3 : @Instance(WeightedGraph) := [ + e1 : @over .E, + e2 : @over .E +] \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot new file mode 100644 index 000000000..8a5c474e7 --- /dev/null +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -0,0 +1,71 @@ +set_theory ThSchema +#/ result: set theory to ThSchema + +type WeightedGraph := [ + V : Entity, + E : Entity, + Weight : AttrType, + src : (Hom Entity)[E, V], + tgt : (Hom Entity)[E, V], + weight : Attr[E, Weight] +] +#/ declared: WeightedGraph + +diagram I : @Instance(WeightedGraph) := [ + e : @over .E, +] +#/ declared: I +#/ diagram domain: +#/ e : Entity -> E +#/ src(e) : Entity -> V +#/ tgt(e) : Entity -> V +#/ weight(e) : AttrType -> Weight +#/ ~src(e) : Hom(Entity)[e, src(e)] -> src +#/ ~tgt(e) : Hom(Entity)[e, tgt(e)] -> tgt +#/ ~weight(e) : Attr[e, weight(e)] -> weight +#/ diagram validates against codomain + +diagram I2 : @Instance(WeightedGraph) := [ + v1 : @over .V, + v2 : @over .V, + w : @over .Weight, + we : I & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], + wf : I & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] +] +#/ declared: I2 +#/ diagram domain: +#/ v1 : Entity -> V +#/ v2 : Entity -> V +#/ w : AttrType -> Weight +#/ we.e : Entity -> E +#/ wf.e : Entity -> E +#/ we.~src(e) : Hom(Entity)[we.e, v1] -> src +#/ we.~tgt(e) : Hom(Entity)[we.e, v2] -> tgt +#/ we.~weight(e) : Attr[we.e, w] -> weight +#/ wf.~src(e) : Hom(Entity)[wf.e, v2] -> src +#/ wf.~tgt(e) : Hom(Entity)[wf.e, v1] -> tgt +#/ wf.~weight(e) : Attr[wf.e, w] -> weight +#/ diagram validates against codomain + +diagram I3 : @Instance(WeightedGraph) := [ + e1 : @over .E, + e2 : @over .E +] +#/ declared: I3 +#/ diagram domain: +#/ e1 : Entity -> E +#/ e2 : Entity -> E +#/ src(e1) : Entity -> V +#/ tgt(e1) : Entity -> V +#/ weight(e1) : AttrType -> Weight +#/ src(e2) : Entity -> V +#/ tgt(e2) : Entity -> V +#/ weight(e2) : AttrType -> Weight +#/ ~src(e1) : Hom(Entity)[e1, src(e1)] -> src +#/ ~tgt(e1) : Hom(Entity)[e1, tgt(e1)] -> tgt +#/ ~weight(e1) : Attr[e1, weight(e1)] -> weight +#/ ~src(e2) : Hom(Entity)[e2, src(e2)] -> src +#/ ~tgt(e2) : Hom(Entity)[e2, tgt(e2)] -> tgt +#/ ~weight(e2) : Attr[e2, weight(e2)] -> weight +#/ diagram validates against codomain + From 1b92d0c9af8c4c3e54a5d6d8763f013455db3c86 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 6 May 2026 15:35:59 -0700 Subject: [PATCH 05/53] missing commit --- packages/catlog/src/tt/batch.rs | 23 ++++++----------------- packages/catlog/src/tt/modelgen.rs | 21 +++++++++++---------- packages/catlog/src/tt/notebook_elab.rs | 4 +++- packages/catlog/src/tt/text_elab.rs | 12 ++---------- 4 files changed, 22 insertions(+), 38 deletions(-) diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index c333ab36e..da490338f 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -94,14 +94,10 @@ impl BatchOutput { } for g in &mor_gens { let mt = format_path(&domain.mor_generator_type(g)); - let dom_str = domain - .get_dom(g) - .map(|o| format!("{o}")) - .unwrap_or_else(|| "?".into()); - let cod_str = domain - .get_cod(g) - .map(|o| format!("{o}")) - .unwrap_or_else(|| "?".into()); + let dom_str = + domain.get_dom(g).map(|o| format!("{o}")).unwrap_or_else(|| "?".into()); + let cod_str = + domain.get_cod(g).map(|o| format!("{o}")).unwrap_or_else(|| "?".into()); let target_str = mapping .0 .mor_generator_map @@ -119,10 +115,7 @@ impl BatchOutput { } } - fn validation_result>( - &self, - errs: I, - ) { + fn validation_result>(&self, errs: I) { if let BatchOutput::Snapshot(out) = self { let mut out = out.borrow_mut(); let mut iter = errs.into_iter().peekable(); @@ -259,11 +252,7 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result { output.diagram_summary(&model_diag); let (cod, _) = Model::from_ty( diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 9be670f9c..17b57c4f2 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -440,7 +440,9 @@ impl DiagramGenerator<'_> { let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); self.eval = eval_with_self; let self_val = self.eval.eta_neu(&self_n, body_ty); - Ok(self.extract(vec![], &self_val, body_ty)?.unwrap_or_else(Namespace::new_for_uuid)) + Ok(self + .extract(vec![], &self_val, body_ty)? + .unwrap_or_else(Namespace::new_for_uuid)) } fn extract( @@ -474,7 +476,7 @@ impl DiagramGenerator<'_> { let resolved = cod_eval.path_ty(&self.codomain_ty, &cod_val, path)?; let TyV_::Object(ot) = &*resolved else { return Err( - "@over path does not refer to an object generator in the codomain".into(), + "@over path does not refer to an object generator in the codomain".into() ); }; let ot: QualifiedName = ot.clone().try_into().map_err(|_| { @@ -497,9 +499,10 @@ impl DiagramGenerator<'_> { let cod_name = neutral_qualified_name(cod).ok_or_else(|| { "morphism codomain does not resolve to a generator name".to_string() })?; - let mt_inner: QualifiedPath = mt.clone().try_into().map_err(|_| { - "morphism type is not valid for a discrete theory".to_string() - })?; + let mt_inner: QualifiedPath = mt + .clone() + .try_into() + .map_err(|_| "morphism type is not valid for a discrete theory".to_string())?; let qname: QualifiedName = prefix.clone().into(); self.domain.add_mor(qname.clone(), dom_name, cod_name, mt_inner); // Mapping target: extract the codomain morphism's name @@ -514,11 +517,9 @@ impl DiagramGenerator<'_> { } Ok(None) } - TyV_::Object(_) - | TyV_::Sing(_, _) - | TyV_::Id(_, _, _) - | TyV_::Unit - | TyV_::Meta(_) => Ok(None), + TyV_::Object(_) | TyV_::Sing(_, _) | TyV_::Id(_, _, _) | TyV_::Unit | TyV_::Meta(_) => { + Ok(None) + } } } } diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 349579c3d..7fbe38408 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -452,7 +452,9 @@ impl<'a> Elaborator<'a> { nb::ModelJudgment::Equation(eqn_decl) => self.equation_cell(eqn_decl), }; field_ty_vs.push((name, (label, ty_v.clone()))); - self.ctx.scope.push(VarInContext::new(name, label, Some(ty_v.clone()), VarKind::Term)); + self.ctx + .scope + .push(VarInContext::new(name, label, Some(ty_v.clone()), VarKind::Term)); self.ctx.env = self.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index db6eb7d76..01104e72c 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -941,18 +941,10 @@ fn augment_with_lifts(body_s: &TyS, codomain: &TyV) -> TyS { let lift_seg = name_seg(lift_name); let lift_label = label_seg(lift_name); if !row.has(lift_seg) { - let self_var = TmS::var( - BwdIdx::from(0), - name_seg("self"), - label_seg("self"), - ); + let self_var = TmS::var(BwdIdx::from(0), name_seg("self"), label_seg("self")); let dom_term = TmS::proj(self_var.clone(), *field_name, *field_label); let cod_term = TmS::proj(self_var, synth_seg, synth_label); - row.insert( - lift_seg, - lift_label, - TyS::morphism(mt.clone(), dom_term, cod_term), - ); + row.insert(lift_seg, lift_label, TyS::morphism(mt.clone(), dom_term, cod_term)); added = true; } } From 42a9b6232f852a61389e44d074bba3af87c3ab9b Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 6 May 2026 15:37:21 -0700 Subject: [PATCH 06/53] clippy --- packages/catlog/src/tt/toplevel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 6c96febb9..03808a16f 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -17,7 +17,7 @@ pub enum TopDecl { DefConst(DefConst), /// See [Def]. Def(Def), - /// See [Diag] + /// See [Diag]. Diag(Diag), } From 8f0a6f92609b2e3467101f362b5d2c12e794e257 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 7 May 2026 14:19:06 -0700 Subject: [PATCH 07/53] docs fix --- packages/catlog/src/tt/modelgen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 17b57c4f2..f55cdec9f 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -404,7 +404,7 @@ impl<'a> ModelGenerator<'a> { } } -/// Generates a [`DiscreteDblModelDiagram`] from an elaborated [`Diag`]. +/// Generates a [`discrete::DiscreteDblModelDiagram`] from an elaborated [`Diag`]. /// /// Restricted to discrete double theories for the moment; other theory /// kinds will need their own diagram types in catlog before this can be From d0cf43b59a1d4e676752cd041726dcdcbee1082b Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 11 May 2026 16:14:00 -0700 Subject: [PATCH 08/53] change a variable --- packages/catlog/examples/tt/text/test_instances.dbltt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 9f9857a9e..4cc55fcd9 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -9,17 +9,17 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -diagram I : @Instance(WeightedGraph) := [ +diagram Edge : @Instance(WeightedGraph) := [ e : @over .E, ] #/ Should there be some sugar for this situation, where I'm basically just renaming default-named guys? -diagram I2 : @Instance(WeightedGraph) := [ +diagram I : @Instance(WeightedGraph) := [ v1 : @over .V, v2 : @over .V, w : @over .Weight, - we : I & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], - wf : I & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] + we : Edge & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], + wf : Edge & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] ] diagram I3 : @Instance(WeightedGraph) := [ From f4b812c98061ff22fa7f646569833bd29c3750e8 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 3 Jun 2026 18:25:23 -0700 Subject: [PATCH 09/53] Switching specializations to equality types in diagrams --- packages/catlog/Cargo.toml | 2 +- .../examples/tt/text/test_instances.dbltt | 12 ++- .../tt/text/test_instances.dbltt.snapshot | 18 ++-- packages/catlog/src/tt/modelgen.rs | 92 +++++++++++++++++-- packages/catlog/src/tt/text_elab.rs | 39 +++++++- 5 files changed, 143 insertions(+), 20 deletions(-) diff --git a/packages/catlog/Cargo.toml b/packages/catlog/Cargo.toml index dc0f9219c..99b5fbf61 100644 --- a/packages/catlog/Cargo.toml +++ b/packages/catlog/Cargo.toml @@ -36,7 +36,7 @@ tattle = "0.4.3" thiserror = "1" tsify = { version = "0.5.6", features = ["js"], optional = true } ustr = "1" -uuid = { version = "1.18" } +uuid = { version = "1.18", features = ["v4"] } wasm-bindgen = { version = "0.2.100", optional = true } catcolab-document-types = { version = "0.1.0", path = "../document-types" } sea-query = { version = "0.32.7", optional = true } diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 4cc55fcd9..628c844ba 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -13,13 +13,19 @@ diagram Edge : @Instance(WeightedGraph) := [ e : @over .E, ] -#/ Should there be some sugar for this situation, where I'm basically just renaming default-named guys? +#/ Needs some sugar, maybe analogous to in the notebook diagram I : @Instance(WeightedGraph) := [ v1 : @over .V, v2 : @over .V, w : @over .Weight, - we : Edge & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], - wf : Edge & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] + we : Edge, + wf : Edge, + _ : (we.src(e) == v1), + _ : (we.tgt(e) == v2), + _ : (wf.src(e) == v2), + _ : (wf.tgt(e) == v1), + _ : (we.weight(e) == w), + _ : (wf.weight(e) == w) ] diagram I3 : @Instance(WeightedGraph) := [ diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 8a5c474e7..2f2c88434 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -11,10 +11,10 @@ type WeightedGraph := [ ] #/ declared: WeightedGraph -diagram I : @Instance(WeightedGraph) := [ +diagram Edge : @Instance(WeightedGraph) := [ e : @over .E, ] -#/ declared: I +#/ declared: Edge #/ diagram domain: #/ e : Entity -> E #/ src(e) : Entity -> V @@ -25,14 +25,20 @@ diagram I : @Instance(WeightedGraph) := [ #/ ~weight(e) : Attr[e, weight(e)] -> weight #/ diagram validates against codomain -diagram I2 : @Instance(WeightedGraph) := [ +diagram I : @Instance(WeightedGraph) := [ v1 : @over .V, v2 : @over .V, w : @over .Weight, - we : I & [.src(e) := v1, .tgt(e) := v2, .weight(e) := w], - wf : I & [.src(e) := v2, .tgt(e) := v1, .weight(e) := w] + we : Edge, + wf : Edge, + _ : (we.src(e) == v1), + _ : (we.tgt(e) == v2), + _ : (wf.src(e) == v2), + _ : (wf.tgt(e) == v1), + _ : (we.weight(e) == w), + _ : (wf.weight(e) == w) ] -#/ declared: I2 +#/ declared: I #/ diagram domain: #/ v1 : Entity -> V #/ v2 : Entity -> V diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index f55cdec9f..2753c138e 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -1,5 +1,7 @@ //! Generate catlog models from DoubleTT types. +use std::collections::HashMap; + use all_the_same::all_the_same; use derive_more::{From, TryInto}; use tattle::display::SourceInfo; @@ -422,6 +424,7 @@ pub fn diagram_from_diag( codomain_ty: diag.model.clone(), domain: discrete::DiscreteDblModel::new(theory.clone()), mapping: discrete::DiscreteDblModelMapping::default(), + ob_aliases: HashMap::new(), }; let namespace = generator.generate(&diag.body_val)?; let DiagramGenerator { domain, mapping, .. } = generator; @@ -433,6 +436,16 @@ struct DiagramGenerator<'a> { codomain_ty: TyV, domain: discrete::DiscreteDblModel, mapping: discrete::DiscreteDblModelMapping, + /// Names of domain object generators that should be elided in favor + /// of a canonical alias. Populated by [`Self::collect_ob_aliases`] + /// before [`Self::extract`] runs; the [`TyV_::Over`] arm skips aliased + /// names, and the [`TyV_::Morphism`] arm rewrites dom/cod names through + /// the map. This is the interim mechanism by which `Id`-equalities + /// whose carrier is an `@over` type fold their two sides into a + /// single domain generator: `DiscreteDblModel`'s only equation + /// facility is for morphism paths, so object equations are realized + /// here by name substitution instead. + ob_aliases: HashMap, } impl DiagramGenerator<'_> { @@ -440,11 +453,69 @@ impl DiagramGenerator<'_> { let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); self.eval = eval_with_self; let self_val = self.eval.eta_neu(&self_n, body_ty); + self.collect_ob_aliases(&self_val, body_ty); + self.normalize_ob_aliases(); Ok(self .extract(vec![], &self_val, body_ty)? .unwrap_or_else(Namespace::new_for_uuid)) } + /// Walks the body and records every `Id`-equality whose carrier is + /// `@over` as a name substitution from the longer qualified name + /// (ties broken lex) to the shorter. The elaborator's well-typedness + /// check on the `Id` already ensures the two sides have the same + /// underlying object type, so the resulting domain stays well-typed. + fn collect_ob_aliases(&mut self, val: &TmV, ty: &TyV) { + match &**ty { + TyV_::Record(r) => { + for (name, (label, _)) in r.fields.iter() { + let field_tm = self.eval.proj(val, *name, *label); + let field_ty = self.eval.field_ty(ty, val, *name); + self.collect_ob_aliases(&field_tm, &field_ty); + } + } + TyV_::Id(carrier, lhs, rhs) if matches!(&**carrier, TyV_::Over(_)) => { + if let (TmV_::Neu(ln, _), TmV_::Neu(rn, _)) = (&**lhs, &**rhs) { + let ln = ln.to_qualified_name(); + let rn = rn.to_qualified_name(); + if ln == rn { + return; + } + let (from, to) = if (ln.as_slice().len(), &ln) > (rn.as_slice().len(), &rn) { + (ln, rn) + } else { + (rn, ln) + }; + self.ob_aliases.insert(from, to); + } + } + _ => {} + } + } + + /// Collapses multi-hop chains in `ob_aliases` so each key maps directly + /// to its terminal canonical name. Bails on cycles (which can only + /// arise from contradictory user constraints). + fn normalize_ob_aliases(&mut self) { + let keys: Vec = self.ob_aliases.keys().cloned().collect(); + for key in keys { + let mut cur = self.ob_aliases[&key].clone(); + let mut seen = std::collections::HashSet::new(); + seen.insert(key.clone()); + while let Some(next) = self.ob_aliases.get(&cur).cloned() { + if !seen.insert(cur.clone()) { + break; + } + cur = next; + } + self.ob_aliases.insert(key, cur); + } + } + + fn canonical_ob_name(&self, name: QualifiedName) -> QualifiedName { + self.ob_aliases.get(&name).cloned().unwrap_or(name) + } + fn extract( &mut self, prefix: Vec, @@ -483,6 +554,13 @@ impl DiagramGenerator<'_> { "@over codomain object type is not valid for a discrete theory".to_string() })?; let qname: QualifiedName = prefix.into(); + // Folded into a canonical alias by an `Id`-over-`@over` + // equation elsewhere in the body — skip both the domain + // generator and the mapping entry. The canonical name's + // own `@over` declaration handles them. + if self.ob_aliases.contains_key(&qname) { + return Ok(None); + } self.domain.add_ob(qname.clone(), ot); let target: QualifiedName = path.iter().map(|(seg, _)| *seg).collect::>().into(); @@ -493,12 +571,14 @@ impl DiagramGenerator<'_> { // Augmentation produces lift morphisms whose dom and cod // are projections from the body's self, resolving to the // qualified names of generators (declared or specialized). - let dom_name = neutral_qualified_name(dom).ok_or_else(|| { - "morphism domain does not resolve to a generator name".to_string() - })?; - let cod_name = neutral_qualified_name(cod).ok_or_else(|| { - "morphism codomain does not resolve to a generator name".to_string() - })?; + let dom_name = + self.canonical_ob_name(neutral_qualified_name(dom).ok_or_else(|| { + "morphism domain does not resolve to a generator name".to_string() + })?); + let cod_name = + self.canonical_ob_name(neutral_qualified_name(cod).ok_or_else(|| { + "morphism codomain does not resolve to a generator name".to_string() + })?); let mt_inner: QualifiedPath = mt .clone() .try_into() diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 01104e72c..c17d0a482 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -579,7 +579,15 @@ impl<'a> Elaborator<'a> { elab.loc = Some(field_n.loc()); let Some((name, label, ty_n)) = (match field_n.ast0() { App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { - Some((name_seg(*name), label_seg(*name), ty_n)) + // `_` is a fresh anonymous binder — give it a + // UUID name so distinct `_` fields don't collide + // in the record row. + let name_seg = if *name == "_" { + NameSegment::Uuid(uuid::Uuid::new_v4()) + } else { + name_seg(*name) + }; + Some((name_seg, label_seg(*name), ty_n)) } _ => elab.error("expected fields in the form : "), }) else { @@ -633,10 +641,11 @@ impl<'a> Elaborator<'a> { App2(L(_, Keyword("==")), tm1_n, tm2_n) => { let (tm1_s, tm1_v, tm1_ty) = elab.syn(tm1_n); let (tm2_s, tm2_v, tm2_ty) = elab.syn(tm2_n); - let TyV_::Morphism(_, _, _) = &*tm1_ty else { + if !matches!(&*tm1_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { elab.loc = Some(tm1_n.loc()); - return elab.ty_error("Equality types are only supported for morphisms"); - }; + return elab + .ty_error("Equality types are only supported for morphisms and @over"); + } if let Err(e) = elab.evaluator().convertible_ty(&tm1_ty, &tm2_ty) { let eval = elab.evaluator(); return elab.ty_error(format!( @@ -698,6 +707,28 @@ impl<'a> Elaborator<'a> { elab.evaluator().field_ty(&ty_v, &tm_v, f), ) } + // `tm.f(x)` — applied projection, resolved to a projection by + // the single synthetic field whose name is the canonical string + // `"f(x)"`. Mirrors the same trick in [`Self::path`] so that the + // lift-target fields inserted by `augment_with_lifts` are + // projectable as ordinary terms (e.g. `we.src(e)`). + App1(L(_, App1(tm_n, L(_, Field(f)))), L(_, Var(x))) => { + let (tm_s, tm_v, ty_v) = elab.syn(tm_n); + let TyV_::Record(r) = &*ty_v else { + return elab.syn_error("can only project from record type"); + }; + let s = ustr(&format!("{f}({x})")); + let label = label_seg(s); + let f = name_seg(s); + if !r.fields.has(f) { + return elab.syn_error(format!("no such field {f}")); + } + ( + TmS::proj(tm_s, f, label), + elab.evaluator().proj(&tm_v, f, label), + elab.evaluator().field_ty(&ty_v, &tm_v, f), + ) + } App1(L(_, Prim("id")), ob_n) => { let (ob_s, ob_v, ob_t) = elab.syn(ob_n); let TyV_::Object(ob_type) = &*ob_t else { From ee63b2a9adeafc0fad05d69a290d9d757eba2a79 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 4 Jun 2026 13:25:08 -0700 Subject: [PATCH 10/53] getting rid of the silly augment_with_lifts hack ahead of proper instance datatype --- .../tt/text/test_instances.dbltt.snapshot | 24 --- packages/catlog/src/tt/eval.rs | 17 ++ packages/catlog/src/tt/modelgen.rs | 142 +------------- packages/catlog/src/tt/stx.rs | 37 ++++ packages/catlog/src/tt/text_elab.rs | 178 ++++++------------ packages/catlog/src/tt/val.rs | 14 ++ 6 files changed, 135 insertions(+), 277 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 2f2c88434..924708600 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -17,12 +17,6 @@ diagram Edge : @Instance(WeightedGraph) := [ #/ declared: Edge #/ diagram domain: #/ e : Entity -> E -#/ src(e) : Entity -> V -#/ tgt(e) : Entity -> V -#/ weight(e) : AttrType -> Weight -#/ ~src(e) : Hom(Entity)[e, src(e)] -> src -#/ ~tgt(e) : Hom(Entity)[e, tgt(e)] -> tgt -#/ ~weight(e) : Attr[e, weight(e)] -> weight #/ diagram validates against codomain diagram I : @Instance(WeightedGraph) := [ @@ -45,12 +39,6 @@ diagram I : @Instance(WeightedGraph) := [ #/ w : AttrType -> Weight #/ we.e : Entity -> E #/ wf.e : Entity -> E -#/ we.~src(e) : Hom(Entity)[we.e, v1] -> src -#/ we.~tgt(e) : Hom(Entity)[we.e, v2] -> tgt -#/ we.~weight(e) : Attr[we.e, w] -> weight -#/ wf.~src(e) : Hom(Entity)[wf.e, v2] -> src -#/ wf.~tgt(e) : Hom(Entity)[wf.e, v1] -> tgt -#/ wf.~weight(e) : Attr[wf.e, w] -> weight #/ diagram validates against codomain diagram I3 : @Instance(WeightedGraph) := [ @@ -61,17 +49,5 @@ diagram I3 : @Instance(WeightedGraph) := [ #/ diagram domain: #/ e1 : Entity -> E #/ e2 : Entity -> E -#/ src(e1) : Entity -> V -#/ tgt(e1) : Entity -> V -#/ weight(e1) : AttrType -> Weight -#/ src(e2) : Entity -> V -#/ tgt(e2) : Entity -> V -#/ weight(e2) : AttrType -> Weight -#/ ~src(e1) : Hom(Entity)[e1, src(e1)] -> src -#/ ~tgt(e1) : Hom(Entity)[e1, tgt(e1)] -> tgt -#/ ~weight(e1) : Attr[e1, weight(e1)] -> weight -#/ ~src(e2) : Hom(Entity)[e2, src(e2)] -> src -#/ ~tgt(e2) : Hom(Entity)[e2, tgt(e2)] -> tgt -#/ ~weight(e2) : Attr[e2, weight(e2)] -> weight #/ diagram validates against codomain diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index af27e23fc..a2a45a390 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -97,6 +97,9 @@ impl<'a> Evaluator<'a> { TmS_::Compose(f, g) => TmV::compose(self.eval_tm(f), self.eval_tm(g)), TmS_::ObApp(name, x) => TmV::app(*name, self.eval_tm(x)), TmS_::List(elems) => TmV::list(elems.iter().map(|tm| self.eval_tm(tm)).collect()), + TmS_::OverApp(mor, mor_label, tgt_path, inner) => { + TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eval_tm(inner)) + } TmS_::Meta(mv) => TmV::meta(*mv), } } @@ -218,6 +221,9 @@ impl<'a> Evaluator<'a> { match &**tm { TmV_::Neu(n, _) => self.quote_neu(n), TmV_::App(name, x) => TmS::ob_app(*name, self.quote_tm(x)), + TmV_::OverApp(mor, mor_label, tgt_path, inner) => { + TmS::over_app(*mor, *mor_label, tgt_path.clone(), self.quote_tm(inner)) + } TmV_::List(elems) => TmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()), TmV_::Cons(fields) => TmS::cons(fields.map(|tm| self.quote_tm(tm))), TmV_::Tt => TmS::tt(), @@ -344,6 +350,9 @@ impl<'a> Evaluator<'a> { match &**v { TmV_::Neu(tm_n, ty_v) => self.eta_neu(tm_n, ty_v), TmV_::App(name, x) => TmV::app(*name, self.eta(x, None)), + TmV_::OverApp(mor, mor_label, tgt_path, inner) => { + TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eta(inner, None)) + } TmV_::List(elems) => TmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()), TmV_::Cons(row) => { if let Some(ty) = ty { @@ -430,6 +439,14 @@ impl<'a> Evaluator<'a> { (TmV_::Tab(mor1), TmV_::Tab(mor2)) => { self.equal_tm_helper(mor1, mor2, strict1, strict2) } + (TmV_::OverApp(mor1, _, _, inner1), TmV_::OverApp(mor2, _, _, inner2)) => { + if mor1 != mor2 { + return Err(t(format!( + "OverApp morphisms {mor1} and {mor2} are not equal" + ))); + } + self.equal_tm_helper(inner1, inner2, strict1, strict2) + } _ => Err(t(format!( "failed to match terms {} and {}", self.quote_tm(tm1), diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 2753c138e..108897970 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -1,7 +1,5 @@ //! Generate catlog models from DoubleTT types. -use std::collections::HashMap; - use all_the_same::all_the_same; use derive_more::{From, TryInto}; use tattle::display::SourceInfo; @@ -14,7 +12,7 @@ use crate::dbl::{ theory::{DblTheory, DblTheoryKind, NonUnital, Unital}, }; use crate::one::{ - Category, QualifiedPath, + Category, path::{Path, PathEq}, }; use crate::zero::{Namespace, QualifiedName}; @@ -424,7 +422,6 @@ pub fn diagram_from_diag( codomain_ty: diag.model.clone(), domain: discrete::DiscreteDblModel::new(theory.clone()), mapping: discrete::DiscreteDblModelMapping::default(), - ob_aliases: HashMap::new(), }; let namespace = generator.generate(&diag.body_val)?; let DiagramGenerator { domain, mapping, .. } = generator; @@ -436,16 +433,6 @@ struct DiagramGenerator<'a> { codomain_ty: TyV, domain: discrete::DiscreteDblModel, mapping: discrete::DiscreteDblModelMapping, - /// Names of domain object generators that should be elided in favor - /// of a canonical alias. Populated by [`Self::collect_ob_aliases`] - /// before [`Self::extract`] runs; the [`TyV_::Over`] arm skips aliased - /// names, and the [`TyV_::Morphism`] arm rewrites dom/cod names through - /// the map. This is the interim mechanism by which `Id`-equalities - /// whose carrier is an `@over` type fold their two sides into a - /// single domain generator: `DiscreteDblModel`'s only equation - /// facility is for morphism paths, so object equations are realized - /// here by name substitution instead. - ob_aliases: HashMap, } impl DiagramGenerator<'_> { @@ -453,69 +440,11 @@ impl DiagramGenerator<'_> { let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); self.eval = eval_with_self; let self_val = self.eval.eta_neu(&self_n, body_ty); - self.collect_ob_aliases(&self_val, body_ty); - self.normalize_ob_aliases(); Ok(self .extract(vec![], &self_val, body_ty)? .unwrap_or_else(Namespace::new_for_uuid)) } - /// Walks the body and records every `Id`-equality whose carrier is - /// `@over` as a name substitution from the longer qualified name - /// (ties broken lex) to the shorter. The elaborator's well-typedness - /// check on the `Id` already ensures the two sides have the same - /// underlying object type, so the resulting domain stays well-typed. - fn collect_ob_aliases(&mut self, val: &TmV, ty: &TyV) { - match &**ty { - TyV_::Record(r) => { - for (name, (label, _)) in r.fields.iter() { - let field_tm = self.eval.proj(val, *name, *label); - let field_ty = self.eval.field_ty(ty, val, *name); - self.collect_ob_aliases(&field_tm, &field_ty); - } - } - TyV_::Id(carrier, lhs, rhs) if matches!(&**carrier, TyV_::Over(_)) => { - if let (TmV_::Neu(ln, _), TmV_::Neu(rn, _)) = (&**lhs, &**rhs) { - let ln = ln.to_qualified_name(); - let rn = rn.to_qualified_name(); - if ln == rn { - return; - } - let (from, to) = if (ln.as_slice().len(), &ln) > (rn.as_slice().len(), &rn) { - (ln, rn) - } else { - (rn, ln) - }; - self.ob_aliases.insert(from, to); - } - } - _ => {} - } - } - - /// Collapses multi-hop chains in `ob_aliases` so each key maps directly - /// to its terminal canonical name. Bails on cycles (which can only - /// arise from contradictory user constraints). - fn normalize_ob_aliases(&mut self) { - let keys: Vec = self.ob_aliases.keys().cloned().collect(); - for key in keys { - let mut cur = self.ob_aliases[&key].clone(); - let mut seen = std::collections::HashSet::new(); - seen.insert(key.clone()); - while let Some(next) = self.ob_aliases.get(&cur).cloned() { - if !seen.insert(cur.clone()) { - break; - } - cur = next; - } - self.ob_aliases.insert(key, cur); - } - } - - fn canonical_ob_name(&self, name: QualifiedName) -> QualifiedName { - self.ob_aliases.get(&name).cloned().unwrap_or(name) - } - fn extract( &mut self, prefix: Vec, @@ -554,79 +483,22 @@ impl DiagramGenerator<'_> { "@over codomain object type is not valid for a discrete theory".to_string() })?; let qname: QualifiedName = prefix.into(); - // Folded into a canonical alias by an `Id`-over-`@over` - // equation elsewhere in the body — skip both the domain - // generator and the mapping entry. The canonical name's - // own `@over` declaration handles them. - if self.ob_aliases.contains_key(&qname) { - return Ok(None); - } self.domain.add_ob(qname.clone(), ot); let target: QualifiedName = path.iter().map(|(seg, _)| *seg).collect::>().into(); self.mapping.assign_ob(qname, target); Ok(None) } - TyV_::Morphism(mt, dom, cod) => { - // Augmentation produces lift morphisms whose dom and cod - // are projections from the body's self, resolving to the - // qualified names of generators (declared or specialized). - let dom_name = - self.canonical_ob_name(neutral_qualified_name(dom).ok_or_else(|| { - "morphism domain does not resolve to a generator name".to_string() - })?); - let cod_name = - self.canonical_ob_name(neutral_qualified_name(cod).ok_or_else(|| { - "morphism codomain does not resolve to a generator name".to_string() - })?); - let mt_inner: QualifiedPath = mt - .clone() - .try_into() - .map_err(|_| "morphism type is not valid for a discrete theory".to_string())?; - let qname: QualifiedName = prefix.clone().into(); - self.domain.add_mor(qname.clone(), dom_name, cod_name, mt_inner); - // Mapping target: extract the codomain morphism's name - // from the synthetic lift name `~f(e)`. For other shapes - // (e.g., a future user-declared morphism in a body), fall - // back to the field name itself. - if let Some(last) = prefix.last() - && let Some(cod_mor) = parse_lift_morphism_name(last) - { - let target = Path::single(QualifiedName::single(cod_mor)); - self.mapping.assign_mor(qname, target); - } - Ok(None) - } - TyV_::Object(_) | TyV_::Sing(_, _) | TyV_::Id(_, _, _) | TyV_::Unit | TyV_::Meta(_) => { - Ok(None) - } + TyV_::Object(_) + | TyV_::Morphism(_, _, _) + | TyV_::Sing(_, _) + | TyV_::Id(_, _, _) + | TyV_::Unit + | TyV_::Meta(_) => Ok(None), } } } -/// Read off a [`QualifiedName`] from a value that's a neutral chain of -/// projections from a self-variable (regardless of which level the self -/// is bound at). Returns `None` if the value isn't of that shape. -fn neutral_qualified_name(tm: &TmV) -> Option { - let TmV_::Neu(n, _) = &**tm else { - return None; - }; - Some(n.to_qualified_name()) -} - -/// Parse a synthetic lift-morphism field name of the form `~f(e)` and -/// return the codomain morphism's name `f`. Returns `None` if the name -/// isn't of that shape. -fn parse_lift_morphism_name(name: &NameSegment) -> Option { - let NameSegment::Text(s) = name else { - return None; - }; - let s = s.as_str(); - let rest = s.strip_prefix('~')?; - let paren = rest.find('(')?; - Some(name_seg(ustr(&rest[..paren]))) -} - #[cfg(test)] mod tests { use super::*; diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 5bb3dbdf8..73062ae5f 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -240,6 +240,30 @@ pub enum TmS_ { ObApp(VarName, TmS), /// List of objects. List(Vec), + /// Application of a codomain morphism to an [`@over`-typed](TyS_::Over) + /// term. Only well-formed inside a diagram declaration. + /// + /// Arguments, in order: + /// 1. `mor` — name of the codomain morphism being applied + /// (e.g. `src` in `we.src(e)`). + /// 2. `mor_label` — display label for that name. + /// 3. `tgt_path` — the codomain object-path that `mor` lands at. + /// Stored on the node so [`Evaluator::eval_tm`] can recover the + /// result type `@over ` without consulting the codomain. + /// 4. `inner` — the `@over`-typed argument (e.g. the elaboration of + /// `we.e`, whose type is `@over ` where the codomain + /// morphism `mor : `). + /// + /// Example: in + /// ```text + /// diagram I : @Instance(WeightedGraph) := [ + /// we : Edge, // Edge := [e : @over .E] + /// _ : (we.src(e) == v1), + /// ] + /// ``` + /// the LHS `we.src(e)` elaborates to + /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of type `@over .V`. + OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmS), /// A metavar. /// /// This only appears when we have an error in elaboration. @@ -310,6 +334,16 @@ impl TmS { Self(Rc::new(TmS_::List(elems))) } + /// Smart constructor for [TmS], [TmS_::OverApp] case. + pub fn over_app( + mor: FieldName, + mor_label: LabelSegment, + tgt_path: Vec<(FieldName, LabelSegment)>, + inner: TmS, + ) -> Self { + Self(Rc::new(TmS_::OverApp(mor, mor_label, tgt_path, inner))) + } + /// Smart constructor for [TmS], [TmS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TmS_::Meta(mv))) @@ -333,6 +367,9 @@ impl ToDoc for TmS { TmS_::Compose(f, g) => binop(t("·"), f.to_doc(), g.to_doc()), TmS_::ObApp(name, x) => unop(t(format!("@{name}")), x.to_doc()), TmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), + TmS_::OverApp(_, mor_label, _, inner) => { + inner.to_doc() + t(format!(".{mor_label}")) + } TmS_::Tt => t("tt"), TmS_::Meta(mv) => t(format!("?{}", mv.id)), } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index c17d0a482..cb5ea3af1 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -151,13 +151,7 @@ impl TopElaborator { NameSegment::Uuid(_) => label_seg("diag"), }; elab.intro_diagram(name, diag_label, ret_ty_v.clone()); - let (val_s, _val_v) = elab.ty(valn); - // Augment the body with synthetic lift-target fields forced - // by the discrete-opfibration condition: for each declared - // `e : @over .X` and codomain morphism `f : X → Y`, add a - // synthetic field `f(e) : @over .Y`. - let val_s = augment_with_lifts(&val_s, &ret_ty_v); - let val_v = elab.evaluator().eval_ty(&val_s); + let (val_s, val_v) = elab.ty(valn); Some(TopElabResult::Declaration( name, TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), @@ -482,23 +476,6 @@ impl<'a> Elaborator<'a> { p.push((name_seg(*f), label_seg(*f))); Some(p) } - // `.f(x)` — applied path segment, resolved to a single synthetic - // field whose name is the canonical string `"f(x)"`. This makes - // the lift-target objects forced by the discrete-opfibration - // condition addressable by ordinary path-walking. - App1(head_n, L(_, Var(x))) => match head_n.ast0() { - Field(f) => { - let s = ustr(&format!("{f}({x})")); - Some(vec![(name_seg(s), label_seg(s))]) - } - App1(p_n, L(_, Field(f))) => { - let mut p = elab.path(p_n)?; - let s = ustr(&format!("{f}({x})")); - p.push((name_seg(s), label_seg(s))); - Some(p) - } - _ => elab.error("unexpected notation for path"), - }, _ => elab.error("unexpected notation for path"), } } @@ -707,26 +684,72 @@ impl<'a> Elaborator<'a> { elab.evaluator().field_ty(&ty_v, &tm_v, f), ) } - // `tm.f(x)` — applied projection, resolved to a projection by - // the single synthetic field whose name is the canonical string - // `"f(x)"`. Mirrors the same trick in [`Self::path`] so that the - // lift-target fields inserted by `augment_with_lifts` are - // projectable as ordinary terms (e.g. `we.src(e)`). + // `tm.f(x)` — applied projection. Inside a diagram, this is the + // surface syntax for applying a codomain morphism `f` to a field + // `x` of `tm` whose type is `@over `. The discrete- + // opfibration condition forces a unique lift target in the fiber + // over `f`'s codomain, and `tm.f(x)` denotes that lift target. + // + // Elaborates to a [`TmS_::OverApp`] node carrying the codomain + // morphism name and its target path. No synthetic field is + // inserted into the diagram body. App1(L(_, App1(tm_n, L(_, Field(f)))), L(_, Var(x))) => { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); let TyV_::Record(r) = &*ty_v else { return elab.syn_error("can only project from record type"); }; - let s = ustr(&format!("{f}({x})")); - let label = label_seg(s); - let f = name_seg(s); - if !r.fields.has(f) { - return elab.syn_error(format!("no such field {f}")); + let x_label = label_seg(*x); + let x_name = name_seg(*x); + if !r.fields.has(x_name) { + return elab.syn_error(format!("no such field {x_name}")); + } + let inner_ty = elab.evaluator().field_ty(&ty_v, &tm_v, x_name); + let TyV_::Over(src_path) = &*inner_ty else { + let quoted = elab.evaluator().quote_ty(&inner_ty); + return elab + .syn_error(format!("field {x_name} has type {quoted}, expected an @over type")); + }; + let inner_s = TmS::proj(tm_s, x_name, x_label); + let inner_v = elab.evaluator().proj(&tm_v, x_name, x_label); + let f_label = label_seg(*f); + let f_name = name_seg(*f); + // Look up `f` in the diagram's codomain as a morphism field + // whose source path equals `src_path`, and read off its + // target path. + let Some((_, model_ty)) = elab.ctx.lookup_diagram() else { + return elab.syn_error( + "applied codomain morphism is only allowed inside a diagram declaration", + ); + }; + let TyV_::Record(cod_r) = &*model_ty else { + return elab.syn_error("diagram codomain is not a record type"); + }; + let Some(mor_ty_s) = cod_r.fields.get(f_name) else { + return elab.syn_error(format!("no such codomain morphism {f_name}")); + }; + let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + return elab.syn_error(format!( + "codomain field {f_name} is not a morphism" + )); + }; + let (Some(dom_path), Some(cod_path)) = + (tms_to_path(dom_s), tms_to_path(cod_s)) + else { + return elab.syn_error(format!( + "codomain morphism {f_name} has non-path dom/cod; \ + applied-projection syntax requires both to be paths" + )); + }; + if dom_path != *src_path { + return elab.syn_error(format!( + "codomain morphism {f_name} has source path differing from the \ + @over path of {x_name}" + )); } ( - TmS::proj(tm_s, f, label), - elab.evaluator().proj(&tm_v, f, label), - elab.evaluator().field_ty(&ty_v, &tm_v, f), + TmS::over_app(f_name, f_label, cod_path.clone(), inner_s), + TmV::over_app(f_name, f_label, cod_path.clone(), inner_v), + TyV::over(cod_path), ) } App1(L(_, Prim("id")), ob_n) => { @@ -908,87 +931,6 @@ impl<'a> Elaborator<'a> { } } -/// Augment a diagram's body with synthetic lift-target fields. -/// -/// For each declared `e : @over .X` field, and each codomain morphism -/// `f : X → Y`, add a synthetic field named `"f(e)" : @over .Y`. Iterates -/// to a fixpoint, with a depth bound to guard against cyclic theories. -/// -/// The synthetic fields exist only in the type-theoretic body so that -/// path-walking and specialization (e.g., `we : I & [.src(e) := v1]`) -/// can resolve `.src(e)` as an ordinary field. Modelgen filters them -/// out by name when extracting the domain model. -fn augment_with_lifts(body_s: &TyS, codomain: &TyV) -> TyS { - const MAX_DEPTH: usize = 16; - let TyS_::Record(initial) = &**body_s else { - return body_s.clone(); - }; - let TyV_::Record(cod_r) = &**codomain else { - return body_s.clone(); - }; - - // Collect (mor_name, mt, dom_path, cod_path) for each codomain - // morphism field whose source/target reduce to a single chain of - // fields. - type LiftPath = Vec<(NameSegment, LabelSegment)>; - type CodMor = (NameSegment, MorType, LiftPath, LiftPath); - let mut cod_morphisms: Vec = Vec::new(); - for (mor_name, (_, mor_ty_s)) in cod_r.fields.iter() { - if let TyS_::Morphism(mt, dom_s, cod_s) = &**mor_ty_s - && let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) - { - cod_morphisms.push((*mor_name, mt.clone(), dom_path, cod_path)); - } - } - - let mut row = initial.clone(); - for _ in 0..MAX_DEPTH { - let snapshot = row.clone(); - let mut added = false; - for (field_name, (field_label, field_ty)) in snapshot.iter() { - let TyS_::Over(path) = &**field_ty else { - continue; - }; - for (mor_name, mt, dom_path, cod_path) in &cod_morphisms { - if dom_path != path { - continue; - } - // Synthetic object field `f(e) : @over .Y`, the unique - // lift target forced by the discrete-opfibration condition. - let synth = ustr(&format!("{mor_name}({field_name})")); - let synth_seg = name_seg(synth); - let synth_label = label_seg(synth); - if !row.has(synth_seg) { - row.insert(synth_seg, synth_label, TyS::over(cod_path.clone())); - added = true; - } - // Synthetic morphism field `~f(e) : Morphism(mt, e, f(e))`, - // the lift morphism itself. The `~` prefix evokes the - // overline notation for lifts and disambiguates per source - // object so multiple declared `@over .X` fields can each - // get their own lift; modelgen strips the wrapper to - // recover the codomain morphism's name. - let lift_name = ustr(&format!("~{mor_name}({field_name})")); - let lift_seg = name_seg(lift_name); - let lift_label = label_seg(lift_name); - if !row.has(lift_seg) { - let self_var = TmS::var(BwdIdx::from(0), name_seg("self"), label_seg("self")); - let dom_term = TmS::proj(self_var.clone(), *field_name, *field_label); - let cod_term = TmS::proj(self_var, synth_seg, synth_label); - row.insert(lift_seg, lift_label, TyS::morphism(mt.clone(), dom_term, cod_term)); - added = true; - } - } - } - if !added { - return TyS::record(row); - } - } - // Hit the depth bound — return what we have. Cyclic theories will - // produce an under-augmented body; this is a known limitation. - TyS::record(row) -} - /// Read off a path of `(name, label)` segments from a term that is a chain /// of projections rooted in a variable. /// diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 428289b43..f08e152a5 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -236,6 +236,10 @@ pub enum TmV_ { Neu(TmN, TyV), /// Application of an object operation in the theory. App(VarName, TmV), + /// Application of a codomain morphism to an [`@over`-typed](TyV_::Over) + /// term. See [`TmS_::OverApp`] for the syntactic counterpart and + /// argument-by-argument documentation. + OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), /// Lists of objects. List(Vec), /// Records. @@ -268,6 +272,16 @@ impl TmV { TmV(Rc::new(TmV_::App(name, x))) } + /// Smart constructor for [TmV], [TmV_::OverApp] case. + pub fn over_app( + mor: FieldName, + mor_label: LabelSegment, + tgt_path: Vec<(FieldName, LabelSegment)>, + inner: TmV, + ) -> Self { + TmV(Rc::new(TmV_::OverApp(mor, mor_label, tgt_path, inner))) + } + /// Smart constructor for [TmV], [TmV_::List] case. pub fn list(elems: Vec) -> Self { TmV(Rc::new(TmV_::List(elems))) From 7e1e44ac67d31c2b6f08945075a112d5d87acd96 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 5 Jun 2026 11:36:05 -0700 Subject: [PATCH 11/53] Generate a dbl::instance from an @Instance --- .../tt/text/test_instances.dbltt.snapshot | 18 +++ packages/catlog/src/dbl/discrete/mod.rs | 2 + packages/catlog/src/dbl/mod.rs | 1 + packages/catlog/src/tt/batch.rs | 56 ++++++++- packages/catlog/src/tt/modelgen.rs | 118 +++++++++++++++++- 5 files changed, 192 insertions(+), 3 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 924708600..10a04c3e5 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -18,6 +18,8 @@ diagram Edge : @Instance(WeightedGraph) := [ #/ diagram domain: #/ e : Entity -> E #/ diagram validates against codomain +#/ instance generators: +#/ e : E diagram I : @Instance(WeightedGraph) := [ v1 : @over .V, @@ -40,6 +42,19 @@ diagram I : @Instance(WeightedGraph) := [ #/ we.e : Entity -> E #/ wf.e : Entity -> E #/ diagram validates against codomain +#/ instance generators: +#/ v1 : V +#/ v2 : V +#/ w : Weight +#/ we.e : E +#/ wf.e : E +#/ instance equations: +#/ src(we.e) == v1 +#/ tgt(we.e) == v2 +#/ src(wf.e) == v2 +#/ tgt(wf.e) == v1 +#/ weight(we.e) == w +#/ weight(wf.e) == w diagram I3 : @Instance(WeightedGraph) := [ e1 : @over .E, @@ -50,4 +65,7 @@ diagram I3 : @Instance(WeightedGraph) := [ #/ e1 : Entity -> E #/ e2 : Entity -> E #/ diagram validates against codomain +#/ instance generators: +#/ e1 : E +#/ e2 : E diff --git a/packages/catlog/src/dbl/discrete/mod.rs b/packages/catlog/src/dbl/discrete/mod.rs index 10636e856..4dd02d3f2 100644 --- a/packages/catlog/src/dbl/discrete/mod.rs +++ b/packages/catlog/src/dbl/discrete/mod.rs @@ -2,10 +2,12 @@ pub mod model; pub mod model_diagram; +pub mod model_instance; pub mod model_morphism; pub mod theory; pub use model::*; pub use model_diagram::*; +pub use model_instance::*; pub use model_morphism::*; pub use theory::*; diff --git a/packages/catlog/src/dbl/mod.rs b/packages/catlog/src/dbl/mod.rs index 9bcde45ed..f4b17f42b 100644 --- a/packages/catlog/src/dbl/mod.rs +++ b/packages/catlog/src/dbl/mod.rs @@ -52,6 +52,7 @@ pub mod tree; pub mod model; pub mod model_diagram; +pub mod model_instance; pub mod model_morphism; pub mod theory; diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index da490338f..60bb9732c 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -12,12 +12,15 @@ use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; use super::{ - modelgen::{Model, diagram_from_diag}, + modelgen::{Model, diagram_from_diag, instance_from_diag}, text_elab::*, theory::std_theories, toplevel::*, }; -use crate::dbl::discrete::{DiscreteDblModelDiagram, InvalidDiscreteDblModelDiagram}; +use crate::dbl::discrete::{ + DiscreteDblModelDiagram, DiscreteDblModelInstance, DiscreteInstanceTerm, + InvalidDiscreteDblModelDiagram, +}; use crate::dbl::model::{FpDblModel, MutDblModel}; use crate::dbl::model_diagram::DblModelDiagram; use crate::one::category::FgCategory; @@ -115,6 +118,42 @@ impl BatchOutput { } } + fn instance_summary(&self, instance: &DiscreteDblModelInstance) { + if let BatchOutput::Snapshot(out) = self { + let mut out = out.borrow_mut(); + let gens: Vec<_> = instance.generators().collect(); + let eqns: Vec<_> = instance.equations().collect(); + if gens.is_empty() && eqns.is_empty() { + writeln!(out, "#/ instance has no generators or equations").unwrap(); + return; + } + if !gens.is_empty() { + writeln!(out, "#/ instance generators:").unwrap(); + for (name, fiber) in &gens { + writeln!(out, "#/ {name} : {fiber}").unwrap(); + } + } + if !eqns.is_empty() { + writeln!(out, "#/ instance equations:").unwrap(); + for (lhs, rhs) in &eqns { + writeln!( + out, + "#/ {} == {}", + format_instance_term(lhs), + format_instance_term(rhs) + ) + .unwrap(); + } + } + } + } + + fn instance_error(&self, msg: &str) { + if let BatchOutput::Snapshot(out) = self { + writeln!(out.borrow_mut(), "#/ instance generation failed: {msg}").unwrap(); + } + } + fn validation_result>(&self, errs: I) { if let BatchOutput::Snapshot(out) = self { let mut out = out.borrow_mut(); @@ -268,6 +307,10 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result output.diagram_error(&msg), } + match instance_from_diag(&toplevel, &diag.theory.definition, diag) { + Ok((instance, _)) => output.instance_summary(&instance), + Err(msg) => output.instance_error(&msg), + } } } TopElabResult::Output(s) => { @@ -341,3 +384,12 @@ fn format_path(p: &Path) -> String { } } } + +fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { + match tm { + DiscreteInstanceTerm::Generator(name) => format!("{name}"), + DiscreteInstanceTerm::Apply(mor, arg) => { + format!("{}({})", format_path(mor), format_instance_term(arg)) + } + } +} diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 108897970..51c3553fd 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -4,9 +4,12 @@ use all_the_same::all_the_same; use derive_more::{From, TryInto}; use tattle::display::SourceInfo; +use std::rc::Rc; + use super::{eval::*, prelude::*, text_elab, theory::*, toplevel::*, val::*}; use crate::dbl::{ - discrete, discrete_tabulator, modal, + discrete::{self, DiscreteDblModelInstance, DiscreteInstanceTerm}, + discrete_tabulator, modal, model::{DblModel, DblModelPrinter, MutDblModel}, model_diagram::DblModelDiagram, theory::{DblTheory, DblTheoryKind, NonUnital, Unital}, @@ -499,6 +502,119 @@ impl DiagramGenerator<'_> { } } +/// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Diag`]. +/// +/// Walks the diagram body, adding one instance generator per `@over .X` +/// field (with fiber resolved against the codomain) and one equation per +/// `_ : (lhs == rhs)` field whose carrier is an `@over` type. No synthetic +/// lift-target objects or lift morphisms are materialized. +/// +/// Restricted to discrete double theories for the moment. +pub fn instance_from_diag( + toplevel: &Toplevel, + th: &TheoryDef, + diag: &Diag, +) -> Result<(DiscreteDblModelInstance, Namespace), String> { + let TheoryDef::Discrete(_) = th else { + return Err("instance generation only supports discrete double theories".into()); + }; + let (cod_model, _) = Model::from_ty(toplevel, th, &diag.model); + let cod_model = cod_model + .as_discrete() + .ok_or_else(|| "expected a discrete codomain model".to_string())?; + let mut generator = InstanceGenerator { + eval: Evaluator::empty(toplevel), + codomain_ty: diag.model.clone(), + instance: DiscreteDblModelInstance::new(Rc::new(cod_model)), + }; + let namespace = generator.generate(&diag.body_val)?; + Ok((generator.instance, namespace)) +} + +struct InstanceGenerator<'a> { + eval: Evaluator<'a>, + codomain_ty: TyV, + instance: DiscreteDblModelInstance, +} + +impl InstanceGenerator<'_> { + fn generate(&mut self, body_ty: &TyV) -> Result { + let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); + self.eval = eval_with_self; + let self_val = self.eval.eta_neu(&self_n, body_ty); + Ok(self + .extract(vec![], &self_val, body_ty)? + .unwrap_or_else(Namespace::new_for_uuid)) + } + + fn extract( + &mut self, + prefix: Vec, + val: &TmV, + ty: &TyV, + ) -> Result, String> { + match &**ty { + TyV_::Record(r) => { + let mut namespace = Namespace::new_for_uuid(); + for (name, (label, _)) in r.fields.iter() { + let mut child_prefix = prefix.clone(); + child_prefix.push(*name); + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let field_tm = self.eval.proj(val, *name, *label); + let field_ty = self.eval.field_ty(ty, val, *name); + if let Some(inner) = self.extract(child_prefix, &field_tm, &field_ty)? { + namespace.add_inner(*name, inner); + } + } + Ok(Some(namespace)) + } + TyV_::Over(path) => { + // Add an instance generator over the resolved codomain object. + let (cod_self_n, cod_eval) = self.eval.bind_self(self.codomain_ty.clone()); + let cod_val = cod_eval.eta_neu(&cod_self_n, &self.codomain_ty); + let resolved = cod_eval.path_ty(&self.codomain_ty, &cod_val, path)?; + let TyV_::Object(_) = &*resolved else { + return Err( + "@over path does not refer to an object generator in the codomain".into() + ); + }; + let qname: QualifiedName = prefix.into(); + let fiber: QualifiedName = + path.iter().map(|(seg, _)| *seg).collect::>().into(); + self.instance.add_generator(qname, fiber); + Ok(None) + } + TyV_::Id(carrier, lhs, rhs) if matches!(&**carrier, TyV_::Over(_)) => { + let lhs_t = tm_to_discrete_instance_term(lhs)?; + let rhs_t = tm_to_discrete_instance_term(rhs)?; + self.instance.add_equation(lhs_t, rhs_t); + Ok(None) + } + _ => Ok(None), + } + } +} + +/// Convert an `@over`-typed term value into a [`DiscreteInstanceTerm`]. +/// +/// Handles bare neutrals (projections of declared generator fields, which +/// become [`DiscreteInstanceTerm::Generator`]) and nested +/// [`TmV_::OverApp`] applications. The morphism is a single codomain field +/// name, wrapped as a singleton [`Path`]. +fn tm_to_discrete_instance_term(tm: &TmV) -> Result { + match &**tm { + TmV_::Neu(n, _) => Ok(DiscreteInstanceTerm::Generator(n.to_qualified_name())), + TmV_::OverApp(mor_name, _, _, inner) => { + let mor_path = Path::single(QualifiedName::single(*mor_name)); + let inner_t = tm_to_discrete_instance_term(inner)?; + Ok(DiscreteInstanceTerm::Apply(mor_path, Box::new(inner_t))) + } + _ => Err("term is not a generator or codomain-morphism application".into()), + } +} + #[cfg(test)] mod tests { use super::*; From 504b4c3b786e6ab9740f5615964c209e565acd0c Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 5 Jun 2026 11:51:55 -0700 Subject: [PATCH 12/53] Remove diagram elab pathway --- .../tt/text/test_instances.dbltt.snapshot | 14 --- .../text/test_modal_theories.dbltt.snapshot | 12 +- .../tt/text/test_tabulators.dbltt.snapshot | 50 ++++++-- packages/catlog/src/tt/batch.rs | 84 +------------ packages/catlog/src/tt/modelgen.rs | 115 ++---------------- packages/catlog/src/tt/text_elab.rs | 78 +++++++----- 6 files changed, 109 insertions(+), 244 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 10a04c3e5..8ec65108f 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -15,9 +15,6 @@ diagram Edge : @Instance(WeightedGraph) := [ e : @over .E, ] #/ declared: Edge -#/ diagram domain: -#/ e : Entity -> E -#/ diagram validates against codomain #/ instance generators: #/ e : E @@ -35,13 +32,6 @@ diagram I : @Instance(WeightedGraph) := [ _ : (wf.weight(e) == w) ] #/ declared: I -#/ diagram domain: -#/ v1 : Entity -> V -#/ v2 : Entity -> V -#/ w : AttrType -> Weight -#/ we.e : Entity -> E -#/ wf.e : Entity -> E -#/ diagram validates against codomain #/ instance generators: #/ v1 : V #/ v2 : V @@ -61,10 +51,6 @@ diagram I3 : @Instance(WeightedGraph) := [ e2 : @over .E ] #/ declared: I3 -#/ diagram domain: -#/ e1 : Entity -> E -#/ e2 : Entity -> E -#/ diagram validates against codomain #/ instance generators: #/ e1 : E #/ e2 : E diff --git a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot index 2e33aadfe..c09c39d6c 100644 --- a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot @@ -76,9 +76,13 @@ type BadOpApp := [ ] #/ declared: BadOpApp #/ expected errors: -#/ error[elab]: synthesized type Object does not match expected type List.Symmetric Object: -#/ object types Object and List.Symmetric Object are not equal -#/ --> examples/tt/text/test_modal_theories.dbltt:44:29 +#/ error[elab]: unexpected notation for term +#/ --> examples/tt/text/test_modal_theories.dbltt:44:21 #/ 44| f : (Hom Object)[@tensor X, X], -#/ 44| ^ +#/ 44| ^^^^^^^^^ +#/ error[elab]: synthesized type ?1 does not match expected type Object: +#/ tried to convert between types of different type constructors +#/ --> examples/tt/text/test_modal_theories.dbltt:44:21 +#/ 44| f : (Hom Object)[@tensor X, X], +#/ 44| ^^^^^^^^^ diff --git a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot index b33a77fa5..1940ef0fc 100644 --- a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot @@ -10,15 +10,24 @@ type SIR := [ _ : Link[I,@tab inf] ] #/ declared: SIR +#/ unexpected errors: +#/ error[elab]: unexpected notation for term +#/ --> examples/tt/text/test_tabulators.dbltt:9:16 +#/ 9| _ : Link[I,@tab inf] +#/ 9| ^^^^^^^^ +#/ error[elab]: synthesized type ?1 does not match expected type Tab Hom Object: +#/ tried to convert between types of different type constructors +#/ --> examples/tt/text/test_tabulators.dbltt:9:16 +#/ 9| _ : Link[I,@tab inf] +#/ 9| ^^^^^^^^ generate SIR -#/ result: model generated by 3 objects and 3 morphisms +#/ result: model generated by 3 objects and 2 morphisms #/ S : Object #/ I : Object #/ R : Object #/ inf : S -> I : Hom Object #/ rec : I -> R : Hom Object -#/ _ : I -> inf : Link type Endo := [ A : Object, @@ -52,6 +61,16 @@ type WalkingLink := [ l : Link[z,@tab f] ] #/ declared: WalkingLink +#/ unexpected errors: +#/ error[elab]: unexpected notation for term +#/ --> examples/tt/text/test_tabulators.dbltt:32:16 +#/ 32| l : Link[z,@tab f] +#/ 32| ^^^^^^ +#/ error[elab]: synthesized type ?1 does not match expected type Tab Hom Object: +#/ tried to convert between types of different type constructors +#/ --> examples/tt/text/test_tabulators.dbltt:32:16 +#/ 32| l : Link[z,@tab f] +#/ 32| ^^^^^^ type SI := [ S : Object, @@ -61,14 +80,19 @@ type SI := [ #/ declared: SI generate SI -#/ result: model generated by 2 objects and 2 morphisms +#/ result: model generated by 2 objects and 1 morphism #/ S : Object #/ I : Object #/ inf.f : S -> I : Hom Object -#/ inf.l : I -> inf.f : Link chk [sf : SI] (sf.inf.l : Link[sf.I,@tab sf.inf.f]) -#/ result: sf.inf.l +#/ result: ?0 +#/ unexpected errors: +#/ error[elab]: synthesized type (Link)[sf.I, ?2] does not match expected type (Link)[sf.I, (@tab sf.inf.f)]: +#/ could not convert codomains: failed to match terms ?2 and (@tab sf.inf.f) +#/ --> examples/tt/text/test_tabulators.dbltt:43:16 +#/ 43| chk [sf : SI] (sf.inf.l : Link[sf.I,@tab sf.inf.f]) +#/ 43| ^^^^^^^^ chk [sf : SI] (sf.inf : WalkingLink) #/ result: sf.inf @@ -84,7 +108,7 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) #/ y : Object, #/ z : Object, #/ f : (Hom Object)[self.x, self.y], -#/ l : (Link)[self.z, (@tab self.f)]] +#/ l : (Link)[self.z, ?2]] #/ & #/ [.x : @sing sf.I]: #/ Neutrals sf.S and sf.I are not equal. @@ -94,6 +118,12 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l #/ declared: act_on_link +#/ unexpected errors: +#/ error[elab]: synthesized type (Link)[d.S, ?2] does not match expected type (Link)[d.S, (@tab d.inf.f)]: +#/ could not convert codomains: failed to match terms ?2 and (@tab d.inf.f) +#/ --> examples/tt/text/test_tabulators.dbltt:52:53 +#/ 52| def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l +#/ 52| ^^^^^^^^^^^^^^^^^ type Triangle := [ x : Object, @@ -117,7 +147,13 @@ def delete_y[T : Triangle] : SI := [ ] ] #/ declared: delete_y +#/ unexpected errors: +#/ error[elab]: synthesized type (Link)[T.z, (@tab T.f · T.g)] does not match expected type (Link)[T.z, ?2]: +#/ could not convert codomains: failed to match terms (@tab T.f · T.g) and ?2 +#/ --> examples/tt/text/test_tabulators.dbltt:71:14 +#/ 71| l := T.l +#/ 71| ^^^ syn [T : Triangle] delete_y[T].inf.l -#/ result: delete_y[T].inf.l : (Link)[T.z, (@tab T.f · T.g)] +#/ result: delete_y[T].inf.l : (Link)[T.z, ?2] diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 60bb9732c..7adae5904 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -12,20 +12,14 @@ use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; use super::{ - modelgen::{Model, diagram_from_diag, instance_from_diag}, + modelgen::instance_from_diag, text_elab::*, theory::std_theories, toplevel::*, }; -use crate::dbl::discrete::{ - DiscreteDblModelDiagram, DiscreteDblModelInstance, DiscreteInstanceTerm, - InvalidDiscreteDblModelDiagram, -}; -use crate::dbl::model::{FpDblModel, MutDblModel}; -use crate::dbl::model_diagram::DblModelDiagram; -use crate::one::category::FgCategory; +use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; use crate::one::path::Path; -use crate::zero::{Mapping, NameSegment, QualifiedName}; +use crate::zero::{NameSegment, QualifiedName}; declare_error!(TOP_ERROR, "top", "an error at the top-level"); @@ -77,47 +71,6 @@ impl BatchOutput { } } - fn diagram_summary(&self, diagram: &DiscreteDblModelDiagram) { - if let BatchOutput::Snapshot(out) = self { - let DblModelDiagram(mapping, domain) = diagram; - let mut out = out.borrow_mut(); - let ob_gens: Vec<_> = domain.ob_generators().collect(); - let mor_gens: Vec<_> = domain.mor_generators().collect(); - if ob_gens.is_empty() && mor_gens.is_empty() { - writeln!(out, "#/ diagram has no domain generators").unwrap(); - return; - } - writeln!(out, "#/ diagram domain:").unwrap(); - for g in &ob_gens { - let ot = domain.ob_generator_type(g); - match mapping.0.ob_generator_map.apply_to_ref(g) { - Some(target) => writeln!(out, "#/ {g} : {ot} -> {target}").unwrap(), - None => writeln!(out, "#/ {g} : {ot}").unwrap(), - } - } - for g in &mor_gens { - let mt = format_path(&domain.mor_generator_type(g)); - let dom_str = - domain.get_dom(g).map(|o| format!("{o}")).unwrap_or_else(|| "?".into()); - let cod_str = - domain.get_cod(g).map(|o| format!("{o}")).unwrap_or_else(|| "?".into()); - let target_str = mapping - .0 - .mor_generator_map - .apply_to_ref(g) - .map(|p| format!(" -> {}", format_path(&p))) - .unwrap_or_default(); - writeln!(out, "#/ {g} : {mt}[{dom_str}, {cod_str}]{target_str}").unwrap(); - } - } - } - - fn diagram_error(&self, msg: &str) { - if let BatchOutput::Snapshot(out) = self { - writeln!(out.borrow_mut(), "#/ diagram generation failed: {msg}").unwrap(); - } - } - fn instance_summary(&self, instance: &DiscreteDblModelInstance) { if let BatchOutput::Snapshot(out) = self { let mut out = out.borrow_mut(); @@ -154,21 +107,6 @@ impl BatchOutput { } } - fn validation_result>(&self, errs: I) { - if let BatchOutput::Snapshot(out) = self { - let mut out = out.borrow_mut(); - let mut iter = errs.into_iter().peekable(); - if iter.peek().is_none() { - writeln!(out, "#/ diagram validates against codomain").unwrap(); - } else { - writeln!(out, "#/ diagram validation failed:").unwrap(); - for e in iter { - writeln!(out, "#/ {e:?}").unwrap(); - } - } - } - } - fn got_result(&self, result: &str) { match self { BatchOutput::Snapshot(out) => { @@ -291,22 +229,6 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result { - output.diagram_summary(&model_diag); - let (cod, _) = Model::from_ty( - &toplevel, - &diag.theory.definition, - &diag.model, - ); - if let Some(cod) = cod.as_discrete() { - output.validation_result( - model_diag.iter_invalid_in(&cod), - ); - } - } - Err(msg) => output.diagram_error(&msg), - } match instance_from_diag(&toplevel, &diag.theory.definition, diag) { Ok((instance, _)) => output.instance_summary(&instance), Err(msg) => output.instance_error(&msg), diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 51c3553fd..4b3d0155b 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -11,7 +11,6 @@ use crate::dbl::{ discrete::{self, DiscreteDblModelInstance, DiscreteInstanceTerm}, discrete_tabulator, modal, model::{DblModel, DblModelPrinter, MutDblModel}, - model_diagram::DblModelDiagram, theory::{DblTheory, DblTheoryKind, NonUnital, Unital}, }; use crate::one::{ @@ -407,101 +406,6 @@ impl<'a> ModelGenerator<'a> { } } -/// Generates a [`discrete::DiscreteDblModelDiagram`] from an elaborated [`Diag`]. -/// -/// Restricted to discrete double theories for the moment; other theory -/// kinds will need their own diagram types in catlog before this can be -/// promoted to an enum mirroring [`Model`]. -pub fn diagram_from_diag( - toplevel: &Toplevel, - th: &TheoryDef, - diag: &Diag, -) -> Result<(discrete::DiscreteDblModelDiagram, Namespace), String> { - let TheoryDef::Discrete(theory) = th else { - return Err("diagram generation only supports discrete double theories".into()); - }; - let mut generator = DiagramGenerator { - eval: Evaluator::empty(toplevel), - codomain_ty: diag.model.clone(), - domain: discrete::DiscreteDblModel::new(theory.clone()), - mapping: discrete::DiscreteDblModelMapping::default(), - }; - let namespace = generator.generate(&diag.body_val)?; - let DiagramGenerator { domain, mapping, .. } = generator; - Ok((DblModelDiagram(mapping, domain), namespace)) -} - -struct DiagramGenerator<'a> { - eval: Evaluator<'a>, - codomain_ty: TyV, - domain: discrete::DiscreteDblModel, - mapping: discrete::DiscreteDblModelMapping, -} - -impl DiagramGenerator<'_> { - fn generate(&mut self, body_ty: &TyV) -> Result { - let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); - self.eval = eval_with_self; - let self_val = self.eval.eta_neu(&self_n, body_ty); - Ok(self - .extract(vec![], &self_val, body_ty)? - .unwrap_or_else(Namespace::new_for_uuid)) - } - - fn extract( - &mut self, - prefix: Vec, - val: &TmV, - ty: &TyV, - ) -> Result, String> { - match &**ty { - TyV_::Record(r) => { - let mut namespace = Namespace::new_for_uuid(); - for (name, (label, _)) in r.fields.iter() { - let mut child_prefix = prefix.clone(); - child_prefix.push(*name); - if let NameSegment::Uuid(uuid) = name { - namespace.set_label(*uuid, *label); - } - let field_tm = self.eval.proj(val, *name, *label); - let field_ty = self.eval.field_ty(ty, val, *name); - if let Some(inner) = self.extract(child_prefix, &field_tm, &field_ty)? { - namespace.add_inner(*name, inner); - } - } - Ok(Some(namespace)) - } - TyV_::Over(path) => { - // Resolve the path against the codomain to find the - // catlog object type for this generator. - let (cod_self_n, cod_eval) = self.eval.bind_self(self.codomain_ty.clone()); - let cod_val = cod_eval.eta_neu(&cod_self_n, &self.codomain_ty); - let resolved = cod_eval.path_ty(&self.codomain_ty, &cod_val, path)?; - let TyV_::Object(ot) = &*resolved else { - return Err( - "@over path does not refer to an object generator in the codomain".into() - ); - }; - let ot: QualifiedName = ot.clone().try_into().map_err(|_| { - "@over codomain object type is not valid for a discrete theory".to_string() - })?; - let qname: QualifiedName = prefix.into(); - self.domain.add_ob(qname.clone(), ot); - let target: QualifiedName = - path.iter().map(|(seg, _)| *seg).collect::>().into(); - self.mapping.assign_ob(qname, target); - Ok(None) - } - TyV_::Object(_) - | TyV_::Morphism(_, _, _) - | TyV_::Sing(_, _) - | TyV_::Id(_, _, _) - | TyV_::Unit - | TyV_::Meta(_) => Ok(None), - } - } -} - /// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Diag`]. /// /// Walks the diagram body, adding one instance generator per `@over .X` @@ -618,10 +522,8 @@ fn tm_to_discrete_instance_term(tm: &TmV) -> Result Toplevel { let reporter = Reporter::new(); @@ -642,7 +544,7 @@ mod tests { } #[test] - fn diagram_over_weighted_graph() { + fn instance_over_weighted_graph() { let src = r#" set_theory ThSchema @@ -656,7 +558,9 @@ type WeightedGraph := [ ] diagram I : @Instance(WeightedGraph) := [ + v : @over .V, e : @over .E, + _ : (src(e) == v) ] "#; let toplevel = elaborate_to_toplevel(src); @@ -664,15 +568,12 @@ diagram I : @Instance(WeightedGraph) := [ Some(TopDecl::Diag(d)) => d.clone(), _ => panic!("expected I to be a diagram declaration"), }; - let (DblModelDiagram(mapping, domain), _ns) = - diagram_from_diag(&toplevel, &diag.theory.definition, &diag).unwrap(); + let (instance, _ns) = + instance_from_diag(&toplevel, &diag.theory.definition, &diag).unwrap(); let e_qname: QualifiedName = vec![name_seg("e")].into(); - let entity_ot: QualifiedName = vec![name_seg("Entity")].into(); - let e_target: QualifiedName = vec![name_seg("E")].into(); - - assert!(domain.has_ob(&e_qname)); - assert_eq!(domain.ob_generator_type(&e_qname), entity_ot); - assert_eq!(mapping.0.ob_generator_map.apply_to_ref(&e_qname), Some(e_target)); + let e_fiber: QualifiedName = vec![name_seg("E")].into(); + assert_eq!(instance.fiber_of(&e_qname), Some(&e_fiber)); + assert_eq!(instance.equations().count(), 1); } } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index cb5ea3af1..592d72531 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -684,43 +684,59 @@ impl<'a> Elaborator<'a> { elab.evaluator().field_ty(&ty_v, &tm_v, f), ) } - // `tm.f(x)` — applied projection. Inside a diagram, this is the - // surface syntax for applying a codomain morphism `f` to a field - // `x` of `tm` whose type is `@over `. The discrete- - // opfibration condition forces a unique lift target in the fiber - // over `f`'s codomain, and `tm.f(x)` denotes that lift target. + // Applied codomain-morphism syntax, in either of two shapes: // - // Elaborates to a [`TmS_::OverApp`] node carrying the codomain - // morphism name and its target path. No synthetic field is - // inserted into the diagram body. - App1(L(_, App1(tm_n, L(_, Field(f)))), L(_, Var(x))) => { - let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - let TyV_::Record(r) = &*ty_v else { - return elab.syn_error("can only project from record type"); - }; - let x_label = label_seg(*x); - let x_name = name_seg(*x); - if !r.fields.has(x_name) { - return elab.syn_error(format!("no such field {x_name}")); - } - let inner_ty = elab.evaluator().field_ty(&ty_v, &tm_v, x_name); - let TyV_::Over(src_path) = &*inner_ty else { - let quoted = elab.evaluator().quote_ty(&inner_ty); - return elab - .syn_error(format!("field {x_name} has type {quoted}, expected an @over type")); + // `f(x)` — `x` is a variable of `@over ` in + // scope (e.g. a generator declared by an + // enclosing field), and `f` is the codomain + // morphism name. + // `tm.f(x)` — same idea, but the argument is the field + // `x` of the record-typed receiver `tm`, + // and `f` is still the codomain morphism + // name. The receiver belongs to the + // *argument*, not the morphism — `tm.f(x)` + // is sugar for `f(tm.x)`. + // + // Both shapes elaborate to a [`TmS_::OverApp`] applying `f` + // (a codomain morphism in the enclosing diagram) to the + // resolved argument term. + App1(head_n, L(_, Var(x))) => { + let (f, inner) = match head_n.ast0() { + Var(f) => (f, None), + App1(tm_n, L(_, Field(f))) => (f, Some(tm_n)), + _ => return elab.syn_error("unexpected notation for term"), }; - let inner_s = TmS::proj(tm_s, x_name, x_label); - let inner_v = elab.evaluator().proj(&tm_v, x_name, x_label); - let f_label = label_seg(*f); - let f_name = name_seg(*f); - // Look up `f` in the diagram's codomain as a morphism field - // whose source path equals `src_path`, and read off its - // target path. let Some((_, model_ty)) = elab.ctx.lookup_diagram() else { return elab.syn_error( "applied codomain morphism is only allowed inside a diagram declaration", ); }; + let (inner_s, inner_v, inner_ty) = match inner { + None => elab.lookup_tm(ustr(*x)), + Some(tm_n) => { + let (tm_s, tm_v, ty_v) = elab.syn(tm_n); + let TyV_::Record(r) = &*ty_v else { + return elab.syn_error("can only project from record type"); + }; + let x_label = label_seg(*x); + let x_name = name_seg(*x); + if !r.fields.has(x_name) { + return elab.syn_error(format!("no such field {x_name}")); + } + let ty = elab.evaluator().field_ty(&ty_v, &tm_v, x_name); + let s = TmS::proj(tm_s, x_name, x_label); + let v = elab.evaluator().proj(&tm_v, x_name, x_label); + (s, v, ty) + } + }; + let TyV_::Over(src_path) = &*inner_ty else { + let quoted = elab.evaluator().quote_ty(&inner_ty); + return elab.syn_error(format!( + "argument {x} has type {quoted}, expected an @over type" + )); + }; + let f_label = label_seg(*f); + let f_name = name_seg(*f); let TyV_::Record(cod_r) = &*model_ty else { return elab.syn_error("diagram codomain is not a record type"); }; @@ -743,7 +759,7 @@ impl<'a> Elaborator<'a> { if dom_path != *src_path { return elab.syn_error(format!( "codomain morphism {f_name} has source path differing from the \ - @over path of {x_name}" + @over path of {x}" )); } ( From 42d7dc1ba7502e48eeafec1b39b7c94ecf1c6266 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 11 Jun 2026 15:38:32 -0700 Subject: [PATCH 13/53] should split: forgot to add DblModelInstances before; secondly, got rid of diagram keyword and started interpret instances as terms of types. --- .../examples/tt/text/test_instances.dbltt | 8 +- .../tt/text/test_instances.dbltt.snapshot | 8 +- .../catlog/src/dbl/discrete/model_instance.rs | 30 +++++ packages/catlog/src/dbl/model_instance.rs | 104 +++++++++++++++ packages/catlog/src/tt/context.rs | 45 +------ packages/catlog/src/tt/modelgen.rs | 2 +- packages/catlog/src/tt/notebook_elab.rs | 4 +- packages/catlog/src/tt/text_elab.rs | 122 ++++++++++-------- 8 files changed, 219 insertions(+), 104 deletions(-) create mode 100644 packages/catlog/src/dbl/discrete/model_instance.rs create mode 100644 packages/catlog/src/dbl/model_instance.rs diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 628c844ba..7042225fe 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -9,16 +9,16 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -diagram Edge : @Instance(WeightedGraph) := [ +def Edge : WeightedGraph := [ e : @over .E, ] #/ Needs some sugar, maybe analogous to in the notebook -diagram I : @Instance(WeightedGraph) := [ +def I : WeightedGraph := [ v1 : @over .V, v2 : @over .V, w : @over .Weight, - we : Edge, + we : Edge, wf : Edge, _ : (we.src(e) == v1), _ : (we.tgt(e) == v2), @@ -28,7 +28,7 @@ diagram I : @Instance(WeightedGraph) := [ _ : (wf.weight(e) == w) ] -diagram I3 : @Instance(WeightedGraph) := [ +def I3 : WeightedGraph := [ e1 : @over .E, e2 : @over .E ] \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 8ec65108f..e7531bf9b 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -11,18 +11,18 @@ type WeightedGraph := [ ] #/ declared: WeightedGraph -diagram Edge : @Instance(WeightedGraph) := [ +def Edge : WeightedGraph := [ e : @over .E, ] #/ declared: Edge #/ instance generators: #/ e : E -diagram I : @Instance(WeightedGraph) := [ +def I : WeightedGraph := [ v1 : @over .V, v2 : @over .V, w : @over .Weight, - we : Edge, + we : Edge, wf : Edge, _ : (we.src(e) == v1), _ : (we.tgt(e) == v2), @@ -46,7 +46,7 @@ diagram I : @Instance(WeightedGraph) := [ #/ weight(we.e) == w #/ weight(wf.e) == w -diagram I3 : @Instance(WeightedGraph) := [ +def I3 : WeightedGraph := [ e1 : @over .E, e2 : @over .E ] diff --git a/packages/catlog/src/dbl/discrete/model_instance.rs b/packages/catlog/src/dbl/discrete/model_instance.rs new file mode 100644 index 000000000..a29f2e69e --- /dev/null +++ b/packages/catlog/src/dbl/discrete/model_instance.rs @@ -0,0 +1,30 @@ +//! Instances of models of a discrete double theory. + +use crate::dbl::model_instance::{DblModelInstance, HasInstanceTerm, InstanceTerm}; +use crate::one::QualifiedPath; +use crate::zero::QualifiedName; + +use super::model::DiscreteDblModel; + +/// A term in an instance of a discrete double model. +/// +/// Discrete morphisms have single-object domains, so applications are +/// 1-ary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DiscreteInstanceTerm { + /// A bare instance generator. + Generator(QualifiedName), + /// A morphism of the model applied to one argument term. + Apply(QualifiedPath, Box), +} + +impl InstanceTerm for DiscreteInstanceTerm { + type Mor = QualifiedPath; +} + +impl HasInstanceTerm for DiscreteDblModel { + type Term = DiscreteInstanceTerm; +} + +/// An instance of a model of a discrete double theory. +pub type DiscreteDblModelInstance = DblModelInstance; diff --git a/packages/catlog/src/dbl/model_instance.rs b/packages/catlog/src/dbl/model_instance.rs new file mode 100644 index 000000000..1c67c7934 --- /dev/null +++ b/packages/catlog/src/dbl/model_instance.rs @@ -0,0 +1,104 @@ +//! Instances of models of a double theory. +//! +//! An **instance** of a model presents a fibered structure over the model: +//! a set of generators living over each object of the model, together with +//! equations between terms built from morphisms of the model applied to +//! those generators. Crucially, the equations are stated *abstractly*: the +//! lift targets and lift morphisms forced by the discrete-opfibration +//! condition are *not* materialized as explicit generators. This is what +//! makes the data structure suited to "minimalist" presentations such as +//! the ones produced by the `diagram` form in DoubleTT. +//! +//! The term language is left to each doctrine to define, via the +//! [`InstanceTerm`] trait and the [`HasInstanceTerm`] extension on +//! [`DblModel`]. Discrete doctrines typically need only bare generators +//! and morphism applications; modal doctrines additionally allow list +//! terms to feed list-shaped morphism domains. +//! +//! For the related but distinct notion of a *diagram* in a model — a +//! morphism into the model from a free model — see +//! [`model_diagram`](super::model_diagram). + +use std::rc::Rc; + +use super::model::DblModel; +use crate::one::Category; +use crate::zero::{Column, HashColumn, MutMapping, QualifiedName}; + +/// A term in the language of an instance of some model. +/// +/// Each doctrine implements its own concrete term type. The associated +/// [`Mor`](Self::Mor) type ties the term language to a particular +/// model's morphism type. +pub trait InstanceTerm { + /// Morphism type from the associated model that this term language + /// can apply to its arguments. + type Mor; +} + +/// A [`DblModel`] that has an associated term language for instances. +/// +/// Each doctrine that wants to support [`DblModelInstance`] declares its +/// term type here. +pub trait HasInstanceTerm: DblModel { + /// The kind of term used to express equations in instances of this + /// model. + type Term: InstanceTerm::Mor>; +} + +/// An instance of a model: a set of fibered generators plus equations +/// between terms in the model's instance-term language. +/// +/// Owns the generator-to-fiber assignment and the equations, but does +/// not own the model itself (held behind an [`Rc`], matching how models +/// reference their theories). +pub struct DblModelInstance { + model: Rc, + /// For each instance generator, the model object it lives over. + /// Multiple generators may share a fiber. + fibers: HashColumn, + /// Equations between terms, asserted to hold in this instance. + equations: Vec<(M::Term, M::Term)>, +} + +impl DblModelInstance { + /// Creates an empty instance over the given model. + pub fn new(model: Rc) -> Self { + Self { + model, + fibers: Default::default(), + equations: Vec::new(), + } + } + + /// The model this is an instance of. + pub fn model(&self) -> &Rc { + &self.model + } + + /// Adds a generator living over the given object of the model. + pub fn add_generator(&mut self, name: QualifiedName, fiber: M::Ob) { + self.fibers.set(name, fiber); + } + + /// The model object that `name` lives over, if `name` is a generator + /// of this instance. + pub fn fiber_of(&self, name: &QualifiedName) -> Option<&M::Ob> { + self.fibers.get(name) + } + + /// Iterates over the instance generators and their fibers. + pub fn generators(&self) -> impl Iterator { + self.fibers.iter() + } + + /// Adds an equation between two terms. + pub fn add_equation(&mut self, lhs: M::Term, rhs: M::Term) { + self.equations.push((lhs, rhs)); + } + + /// Iterates over the equations of this instance. + pub fn equations(&self) -> impl Iterator { + self.equations.iter() + } +} diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 8a60f19c7..0e313d478 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -4,21 +4,6 @@ use derive_more::Constructor; use crate::tt::{prelude::*, val::*}; -/// What kind of binding a context entry represents. -/// -/// Most bindings are ordinary `Term` bindings. `Diagram` bindings are pushed -/// by the `diagram` toplevel arm so that `@over .E` can recover the -/// enclosing diagram's name and codomain type without exposing the diagram -/// to ordinary term lookup (where it would masquerade as a term of the -/// codomain type). -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum VarKind { - /// An ordinary term-level binding. - Term, - /// A binding for a diagram declaration; not resolvable by [`Context::lookup`]. - Diagram, -} - /// Each variable in context is associated with a label and a type. /// /// Multiple variables with the same name can show up in context; in this case @@ -34,8 +19,6 @@ pub struct VarInContext { /// We allow the type to be null as a hack for the `self` variable before we /// know the type of the `self` variable. pub ty: Option, - /// What kind of binding this is. - pub kind: VarKind, } /// The variable context during elaboration. @@ -78,38 +61,18 @@ impl Context { self.scope.truncate(c.scope); } - /// Add a new term-kind variable to scope (note: does not add it to the environment). + /// Add a new variable to scope (note: does not add it to the environment). pub fn push_scope(&mut self, name: VarName, label: LabelSegment, ty: Option) { - self.scope.push(VarInContext::new(name, label, ty, VarKind::Term)) + self.scope.push(VarInContext::new(name, label, ty)) } - /// Add a new diagram-kind variable to scope. - pub fn push_diagram(&mut self, name: VarName, label: LabelSegment, ty: TyV) { - self.scope.push(VarInContext::new(name, label, Some(ty), VarKind::Diagram)) - } - - /// Lookup a term-kind variable by name. - /// - /// Diagram-kind entries are skipped, so a `Var(I)` referring to an - /// enclosing `diagram I := ...` will not resolve here. + /// Lookup a variable by name. pub fn lookup(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, Option)> { self.scope .iter() .rev() .enumerate() - .find(|(_, v)| v.kind == VarKind::Term && v.name == name) + .find(|(_, v)| v.name == name) .map(|(i, v)| (i.into(), v.label, v.ty.clone())) } - - /// Find the most recent diagram-kind binding in scope, if any. - /// - /// Returns the diagram's name and codomain type. Used by the `@over .E` - /// type elaborator. - pub fn lookup_diagram(&self) -> Option<(VarName, TyV)> { - self.scope - .iter() - .rev() - .find(|v| v.kind == VarKind::Diagram) - .map(|v| (v.name, v.ty.clone().expect("diagram binding must have a type"))) - } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 4b3d0155b..15553c547 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -557,7 +557,7 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -diagram I : @Instance(WeightedGraph) := [ +def I : WeightedGraph := [ v : @over .V, e : @over .E, _ : (src(e) == v) diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 7fbe38408..fc1267e79 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -78,7 +78,7 @@ impl<'a> Elaborator<'a> { v }; self.ctx.env = self.ctx.env.snoc(v.clone()); - self.ctx.scope.push(VarInContext::new(name, label, ty, VarKind::Term)); + self.ctx.scope.push(VarInContext::new(name, label, ty)); v } @@ -454,7 +454,7 @@ impl<'a> Elaborator<'a> { field_ty_vs.push((name, (label, ty_v.clone()))); self.ctx .scope - .push(VarInContext::new(name, label, Some(ty_v.clone()), VarKind::Term)); + .push(VarInContext::new(name, label, Some(ty_v.clone()))); self.ctx.env = self.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 592d72531..4ce535703 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -24,9 +24,27 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( ("==", Prec::nonassoc(30)), ], &[":", ":=", "&", "Unit", "Hom", "*", "=="], - &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory", "diagram"], + &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory"], ); +/// Transitional dispatch for `def NAME : TYPE := BODY`. +/// +/// Returns true when BODY looks like an instance specification — i.e., +/// a tuple whose clauses use `:` annotations (declaring generators, +/// equations, or sub-instances). Returns false when the body uses +/// `:=` field assignments (the ordinary record-construction `def`). +/// +/// During Pass A the two shapes are disjoint: `:` for instances, +/// `:=` for record construction. Once Pass C migrates the instance +/// surface to `:=`-with-set/mapping-literals, this dispatch will need +/// a different signal. +fn body_is_instance_shaped(n: &FNtn) -> bool { + let Tuple(field_ns) = n.ast0() else { + return false; + }; + field_ns.iter().any(|field_n| matches!(field_n.ast0(), App2(L(_, Keyword(":")), _, _))) +} + /// The result of elaborating a top-level statement. pub enum TopElabResult { /// A new declaration. @@ -129,34 +147,6 @@ impl TopElaborator { TopDecl::Type(Type::new(theory.clone(), ty_s, ty_v)), )) } - "diagram" => { - let theory = self.get_theory(tn.loc)?; - let mut elab = self.elaborator(&theory, toplevel); - let (name, args_n, annotn, valn) = self.annotated_def(tn.body).or_else(|| { - self.error( - tn.loc, - "unknown syntax for diagram declaration, expected : := ", - ) - })?; - if args_n.is_some() { - return self.error(tn.loc, "diagrams cannot have arguments"); - } - let (_, ret_ty_v) = elab.ty(annotn); - // Bind the diagram's name as a diagram-kind context entry - // so that `@over .E` inside the body can recover both the - // diagram's name and its codomain type, while ordinary - // term lookups cannot see it. - let diag_label = match name { - NameSegment::Text(s) => label_seg(s), - NameSegment::Uuid(_) => label_seg("diag"), - }; - elab.intro_diagram(name, diag_label, ret_ty_v.clone()); - let (val_s, val_v) = elab.ty(valn); - Some(TopElabResult::Declaration( - name, - TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), - )) - } "def" => { let theory = self.get_theory(tn.loc)?; let (name, args_n, ty_n, tm_n) = self.annotated_def(tn.body).or_else(|| { @@ -188,12 +178,31 @@ impl TopElaborator { } None => { let mut elab = self.elaborator(&theory, toplevel); - let (_, ty_v) = elab.ty(ty_n); - let (tm_s, tm_v) = elab.chk(&ty_v, tm_n); - Some(TopElabResult::Declaration( - name, - TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ty_v)), - )) + let (_, ret_ty_v) = elab.ty(ty_n); + // An "instance body" is a tuple of `:`-bound clauses + // (rather than `:=`-bound ones). Route those to the + // instance-elaboration path; everything else is an + // ordinary const-term `def`. + if body_is_instance_shaped(tm_n) { + elab.push_instance_codomain(ret_ty_v.clone()); + let (val_s, val_v) = elab.ty(tm_n); + elab.pop_instance_codomain(); + Some(TopElabResult::Declaration( + name, + TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), + )) + } else { + let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); + Some(TopElabResult::Declaration( + name, + TopDecl::DefConst(DefConst::new( + theory.clone(), + tm_s, + tm_v, + ret_ty_v, + )), + )) + } } } } @@ -273,6 +282,11 @@ pub struct Elaborator<'a> { loc: Option, ctx: Context, next_meta: usize, + /// Stack of model types currently being instantiated, used by + /// `@over .X` and the applied-codomain-morphism arms to recover + /// the codomain. The top of the stack is the innermost enclosing + /// instance body. + instance_codomains: Vec, } struct ElaboratorCheckpoint { @@ -292,9 +306,22 @@ impl<'a> Elaborator<'a> { loc: None, ctx: Context::new(), next_meta: 0, + instance_codomains: Vec::new(), } } + fn push_instance_codomain(&mut self, ty: TyV) { + self.instance_codomains.push(ty); + } + + fn pop_instance_codomain(&mut self) { + self.instance_codomains.pop(); + } + + fn current_instance_codomain(&self) -> Option<&TyV> { + self.instance_codomains.last() + } + fn theory(&self) -> &TheoryDef { &self.theory.definition } @@ -380,15 +407,6 @@ impl<'a> Elaborator<'a> { v } - /// Like [`Self::intro`], but pushes a diagram-kind binding so it is - /// invisible to ordinary term lookup. - fn intro_diagram(&mut self, name: VarName, label: LabelSegment, ty: TyV) { - let v = TmV::neu(TmN::var(self.ctx.scope.len().into(), name, label), ty.clone()); - let v = self.evaluator().eta(&v, Some(&ty)); - self.ctx.env = self.ctx.env.snoc(v); - self.ctx.push_diagram(name, label, ty); - } - fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, TyS, TyV)> { let mut elab = self.enter(n.loc()); match n.ast0() { @@ -517,12 +535,12 @@ impl<'a> Elaborator<'a> { let Some(path) = elab.path(p_n) else { return elab.ty_hole(); }; - // The enclosing diagram-kind binding gives the codomain - // against which we validate the path. The diagram's - // identity is not stored in the resulting type — `@over` - // names a fiber-type that's shared across diagrams. - let Some((_, model_ty)) = elab.ctx.lookup_diagram() else { - return elab.ty_error("@over is only allowed inside a diagram declaration"); + // The enclosing instance body gives the codomain against + // which we validate the path. The codomain's identity is + // not stored in the resulting type — `@over` names a + // fiber-type that's shared across instances. + let Some(model_ty) = elab.current_instance_codomain().cloned() else { + return elab.ty_error("@over is only allowed inside an instance body"); }; let evaluator = elab.evaluator(); let (self_n, _) = evaluator.bind_self(model_ty.clone()); @@ -706,9 +724,9 @@ impl<'a> Elaborator<'a> { App1(tm_n, L(_, Field(f))) => (f, Some(tm_n)), _ => return elab.syn_error("unexpected notation for term"), }; - let Some((_, model_ty)) = elab.ctx.lookup_diagram() else { + let Some(model_ty) = elab.current_instance_codomain().cloned() else { return elab.syn_error( - "applied codomain morphism is only allowed inside a diagram declaration", + "applied codomain morphism is only allowed inside an instance body", ); }; let (inner_s, inner_v, inner_ty) = match inner { From 77ab7ea1e60b229e1e411092463b557abc5877ef Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 11 Jun 2026 16:39:13 -0700 Subject: [PATCH 14/53] := morphism assignment and set literals --- .../examples/tt/text/test_instances.dbltt | 23 ++- .../tt/text/test_instances.dbltt.snapshot | 22 ++- .../catlog/src/dbl/discrete/model_instance.rs | 8 +- packages/catlog/src/tt/batch.rs | 15 +- packages/catlog/src/tt/modelgen.rs | 4 +- packages/catlog/src/tt/text_elab.rs | 151 +++++++++++++++--- 6 files changed, 156 insertions(+), 67 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 7042225fe..4fac6b0bb 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -10,25 +10,22 @@ type WeightedGraph := [ ] def Edge : WeightedGraph := [ - e : @over .E, + E := [e] ] -#/ Needs some sugar, maybe analogous to in the notebook def I : WeightedGraph := [ - v1 : @over .V, - v2 : @over .V, - w : @over .Weight, + V := [v1, v2], + Weight := [w], we : Edge, wf : Edge, - _ : (we.src(e) == v1), - _ : (we.tgt(e) == v2), - _ : (wf.src(e) == v2), - _ : (wf.tgt(e) == v1), - _ : (we.weight(e) == w), - _ : (wf.weight(e) == w) + we.src(e) := v1, + we.tgt(e) := v2, + wf.src(e) := v2, + wf.tgt(e) := v1, + we.weight(e) := w, + wf.weight(e) := w ] def I3 : WeightedGraph := [ - e1 : @over .E, - e2 : @over .E + E := [e1, e2] ] \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index e7531bf9b..f7edb5eed 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -12,24 +12,23 @@ type WeightedGraph := [ #/ declared: WeightedGraph def Edge : WeightedGraph := [ - e : @over .E, + E := [e] ] #/ declared: Edge #/ instance generators: #/ e : E def I : WeightedGraph := [ - v1 : @over .V, - v2 : @over .V, - w : @over .Weight, + V := [v1, v2], + Weight := [w], we : Edge, wf : Edge, - _ : (we.src(e) == v1), - _ : (we.tgt(e) == v2), - _ : (wf.src(e) == v2), - _ : (wf.tgt(e) == v1), - _ : (we.weight(e) == w), - _ : (wf.weight(e) == w) + we.src(e) := v1, + we.tgt(e) := v2, + wf.src(e) := v2, + wf.tgt(e) := v1, + we.weight(e) := w, + wf.weight(e) := w ] #/ declared: I #/ instance generators: @@ -47,8 +46,7 @@ def I : WeightedGraph := [ #/ weight(wf.e) == w def I3 : WeightedGraph := [ - e1 : @over .E, - e2 : @over .E + E := [e1, e2] ] #/ declared: I3 #/ instance generators: diff --git a/packages/catlog/src/dbl/discrete/model_instance.rs b/packages/catlog/src/dbl/discrete/model_instance.rs index a29f2e69e..2f2c83d17 100644 --- a/packages/catlog/src/dbl/discrete/model_instance.rs +++ b/packages/catlog/src/dbl/discrete/model_instance.rs @@ -9,13 +9,17 @@ use super::model::DiscreteDblModel; /// A term in an instance of a discrete double model. /// /// Discrete morphisms have single-object domains, so applications are -/// 1-ary. +/// 1-ary. The morphism is identified by its name (a [`QualifiedName`]) +/// rather than a path, since instance terms only ever apply a single +/// model morphism at a time — composition is reflected by the +/// composability of [`Apply`](Self::Apply) nodes themselves, not by +/// path values living inside one. #[derive(Clone, Debug, PartialEq, Eq)] pub enum DiscreteInstanceTerm { /// A bare instance generator. Generator(QualifiedName), /// A morphism of the model applied to one argument term. - Apply(QualifiedPath, Box), + Apply(QualifiedName, Box), } impl InstanceTerm for DiscreteInstanceTerm { diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 7adae5904..b79b1b808 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -18,8 +18,7 @@ use super::{ toplevel::*, }; use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; -use crate::one::path::Path; -use crate::zero::{NameSegment, QualifiedName}; +use crate::zero::NameSegment; declare_error!(TOP_ERROR, "top", "an error at the top-level"); @@ -297,21 +296,11 @@ fn snapshot_examples() { } /// Render a [`QualifiedPath`] for snapshot output. -fn format_path(p: &Path) -> String { - match p { - Path::Id(v) => format!("Hom({v})"), - Path::Seq(es) => { - let parts: Vec = es.iter().map(|e| format!("{e}")).collect(); - parts.join(".") - } - } -} - fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { match tm { DiscreteInstanceTerm::Generator(name) => format!("{name}"), DiscreteInstanceTerm::Apply(mor, arg) => { - format!("{}({})", format_path(mor), format_instance_term(arg)) + format!("{}({})", mor, format_instance_term(arg)) } } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 15553c547..cb0079b28 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -511,9 +511,9 @@ fn tm_to_discrete_instance_term(tm: &TmV) -> Result Ok(DiscreteInstanceTerm::Generator(n.to_qualified_name())), TmV_::OverApp(mor_name, _, _, inner) => { - let mor_path = Path::single(QualifiedName::single(*mor_name)); + let mor_qname = QualifiedName::single(*mor_name); let inner_t = tm_to_discrete_instance_term(inner)?; - Ok(DiscreteInstanceTerm::Apply(mor_path, Box::new(inner_t))) + Ok(DiscreteInstanceTerm::Apply(mor_qname, Box::new(inner_t))) } _ => Err("term is not a generator or codomain-morphism application".into()), } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 4ce535703..c2a1cc83d 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -27,22 +27,20 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory"], ); -/// Transitional dispatch for `def NAME : TYPE := BODY`. -/// -/// Returns true when BODY looks like an instance specification — i.e., -/// a tuple whose clauses use `:` annotations (declaring generators, -/// equations, or sub-instances). Returns false when the body uses -/// `:=` field assignments (the ordinary record-construction `def`). -/// -/// During Pass A the two shapes are disjoint: `:` for instances, -/// `:=` for record construction. Once Pass C migrates the instance -/// surface to `:=`-with-set/mapping-literals, this dispatch will need -/// a different signal. +/// Temporary `def`-time dispatch — see Pass D in `tt/README` (TODO) +/// for the eventual move into `chk`. A body is instance-shaped if it +/// contains any `:` clause, any `mor(arg) := target` clause (LHS is an +/// application), or any `field := [...]` set-literal clause. fn body_is_instance_shaped(n: &FNtn) -> bool { let Tuple(field_ns) = n.ast0() else { return false; }; - field_ns.iter().any(|field_n| matches!(field_n.ast0(), App2(L(_, Keyword(":")), _, _))) + field_ns.iter().any(|field_n| match field_n.ast0() { + App2(L(_, Keyword(":")), _, _) => true, + App2(L(_, Keyword(":=")), L(_, Var(_)), L(_, Tuple(_))) => true, + App2(L(_, Keyword(":=")), L(_, App1(_, _)), _) => true, + _ => false, + }) } /// The result of elaborating a top-level statement. @@ -572,28 +570,131 @@ impl<'a> Elaborator<'a> { let c = elab.checkpoint(); for field_n in field_ns.iter() { elab.loc = Some(field_n.loc()); - let Some((name, label, ty_n)) = (match field_n.ast0() { + // Each clause emits zero or more field-row entries. + // Set-literal clauses introduce one entry per generator + // name; everything else is single-entry. + let entries: Vec<(FieldName, LabelSegment, TyV)> = match field_n.ast0() { + // `name : type` — ordinary typed field declaration + // (generator slot, sub-instance, or annotated + // equation field). App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { // `_` is a fresh anonymous binder — give it a // UUID name so distinct `_` fields don't collide // in the record row. - let name_seg = if *name == "_" { + let n = if *name == "_" { NameSegment::Uuid(uuid::Uuid::new_v4()) } else { name_seg(*name) }; - Some((name_seg, label_seg(*name), ty_n)) + let (_, ty_v) = elab.ty(ty_n); + vec![(n, label_seg(*name), ty_v)] + } + // `field := [name1, name2, ...]` — set-literal + // assignment to an object-typed field of the + // enclosing instance codomain. Each `nameI` + // introduces a fresh generator slot of type + // `@over .`. + App2( + L(_, Keyword(":=")), + L(_, Var(field_name)), + L(_, Tuple(name_ns)), + ) => { + let Some(model_ty) = elab.current_instance_codomain().cloned() else { + elab.error::<()>( + "set-literal field assignment is only allowed inside an \ + instance body", + ); + failed = true; + continue; + }; + let TyV_::Record(cod_r) = &*model_ty else { + elab.error::<()>("instance codomain is not a record type"); + failed = true; + continue; + }; + let f_seg = name_seg(*field_name); + let f_label = label_seg(*field_name); + let Some(field_ty_s) = cod_r.fields.get(f_seg) else { + elab.error::<()>(format!( + "no such codomain field {field_name}" + )); + failed = true; + continue; + }; + if !matches!(&**field_ty_s, TyS_::Object(_)) { + elab.error::<()>(format!( + "set-literal assignment requires field {field_name} to be \ + object-typed" + )); + failed = true; + continue; + }; + let path = vec![(f_seg, f_label)]; + let gen_ty_v = TyV::over(path); + let mut entries = Vec::with_capacity(name_ns.len()); + let mut local_failed = false; + for name_n in name_ns.iter() { + let Var(gen_name) = name_n.ast0() else { + elab.loc = Some(name_n.loc()); + elab.error::<()>( + "set-literal entries must be bare names", + ); + local_failed = true; + break; + }; + entries.push(( + name_seg(*gen_name), + label_seg(*gen_name), + gen_ty_v.clone(), + )); + } + if local_failed { + failed = true; + continue; + } + entries + } + // `mor(arg) := target` — mapping-entry clause: + // sugar for an anonymous Id-typed field witnessing + // the equation `mor(arg) == target`. The LHS + // synthesizes (and validates) via the same `f(x)` + // arm in [`Self::syn`]. + App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { + let (_lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); + if !matches!(&*lhs_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { + elab.loc = Some(lhs_n.loc()); + elab.error::<()>( + "mapping-entry clause `mor(arg) := target` requires the LHS \ + to have a morphism or @over type", + ); + failed = true; + continue; + } + let (_rhs_s, rhs_v) = elab.chk(&lhs_ty, rhs_n); + let id_ty_v = TyV::id(lhs_ty, lhs_v, rhs_v); + vec![( + NameSegment::Uuid(uuid::Uuid::new_v4()), + label_seg("_"), + id_ty_v, + )] + } + _ => { + elab.error::<()>( + "expected fields in the form `name : type`, \ + `field := [names]`, or `mor(arg) := target`", + ); + failed = true; + continue; } - _ => elab.error("expected fields in the form : "), - }) else { - failed = true; - continue; }; - let (_, ty_v) = elab.ty(ty_n); - field_ty_vs.push((name, (label, ty_v.clone()))); - elab.ctx.push_scope(name, label, Some(ty_v.clone())); - elab.ctx.env = - elab.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); + for (name, label, ty_v) in entries { + field_ty_vs.push((name, (label, ty_v.clone()))); + elab.ctx.push_scope(name, label, Some(ty_v.clone())); + elab.ctx.env = elab.ctx.env.snoc(TmV::neu( + TmN::proj(self_var.clone(), name, label), + ty_v, + )); + } } if failed { return elab.ty_hole(); @@ -1032,7 +1133,7 @@ mod tests { let result = Model::from_text(&th, "[ : Entit]"); let expected = expect![[r#" - error[elab]: expected fields in the form : + error[elab]: expected fields in the form `name : type`, `field := [names]`, or `mor(arg) := target` --> :1:3 1| [ : Entit] 1| ^^^^^^^ From 72d94899c48dc104d5f325c6fd779245e212199f Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 10:43:10 -0700 Subject: [PATCH 15/53] Instance Tms, mapping syntax, improved singleton mapping syntax --- .../examples/tt/text/test_instances.dbltt | 22 +- .../tt/text/test_instances.dbltt.snapshot | 36 +- .../text/test_modal_theories.dbltt.snapshot | 12 +- .../tt/text/test_tabulators.dbltt.snapshot | 50 +- packages/catlog/src/tt/eval.rs | 83 ++- packages/catlog/src/tt/modelgen.rs | 166 +++-- packages/catlog/src/tt/stx.rs | 89 ++- packages/catlog/src/tt/text_elab.rs | 697 +++++++++++------- packages/catlog/src/tt/toplevel.rs | 26 +- packages/catlog/src/tt/val.rs | 21 + 10 files changed, 770 insertions(+), 432 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 4fac6b0bb..8e7355fba 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -18,12 +18,22 @@ def I : WeightedGraph := [ Weight := [w], we : Edge, wf : Edge, - we.src(e) := v1, - we.tgt(e) := v2, - wf.src(e) := v2, - wf.tgt(e) := v1, - we.weight(e) := w, - wf.weight(e) := w + src(we.e) := v1, + tgt(we.e) := v2, + src(wf.e) := v2, + tgt(wf.e) := v1, + weight(we.e) := w, + weight(wf.e) := w +] + +def I2 : WeightedGraph := [ + V := [v1, v2], + Weight := [w], + we : Edge, + wf : Edge, + src := [we.e := v1, wf.e := v2], + tgt := [we.e := v2, wf.e := v1], + weight := [we.e := w, wf.e := w] ] def I3 : WeightedGraph := [ diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index f7edb5eed..d1106af5b 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -23,12 +23,12 @@ def I : WeightedGraph := [ Weight := [w], we : Edge, wf : Edge, - we.src(e) := v1, - we.tgt(e) := v2, - wf.src(e) := v2, - wf.tgt(e) := v1, - we.weight(e) := w, - wf.weight(e) := w + src(we.e) := v1, + tgt(we.e) := v2, + src(wf.e) := v2, + tgt(wf.e) := v1, + weight(we.e) := w, + weight(wf.e) := w ] #/ declared: I #/ instance generators: @@ -45,6 +45,30 @@ def I : WeightedGraph := [ #/ weight(we.e) == w #/ weight(wf.e) == w +def I2 : WeightedGraph := [ + V := [v1, v2], + Weight := [w], + we : Edge, + wf : Edge, + src := [we.e := v1, wf.e := v2], + tgt := [we.e := v2, wf.e := v1], + weight := [we.e := w, wf.e := w] +] +#/ declared: I2 +#/ instance generators: +#/ v1 : V +#/ v2 : V +#/ w : Weight +#/ we.e : E +#/ wf.e : E +#/ instance equations: +#/ src(we.e) == v1 +#/ src(wf.e) == v2 +#/ tgt(we.e) == v2 +#/ tgt(wf.e) == v1 +#/ weight(we.e) == w +#/ weight(wf.e) == w + def I3 : WeightedGraph := [ E := [e1, e2] ] diff --git a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot index c09c39d6c..2e33aadfe 100644 --- a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot @@ -76,13 +76,9 @@ type BadOpApp := [ ] #/ declared: BadOpApp #/ expected errors: -#/ error[elab]: unexpected notation for term -#/ --> examples/tt/text/test_modal_theories.dbltt:44:21 +#/ error[elab]: synthesized type Object does not match expected type List.Symmetric Object: +#/ object types Object and List.Symmetric Object are not equal +#/ --> examples/tt/text/test_modal_theories.dbltt:44:29 #/ 44| f : (Hom Object)[@tensor X, X], -#/ 44| ^^^^^^^^^ -#/ error[elab]: synthesized type ?1 does not match expected type Object: -#/ tried to convert between types of different type constructors -#/ --> examples/tt/text/test_modal_theories.dbltt:44:21 -#/ 44| f : (Hom Object)[@tensor X, X], -#/ 44| ^^^^^^^^^ +#/ 44| ^ diff --git a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot index 1940ef0fc..b33a77fa5 100644 --- a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot @@ -10,24 +10,15 @@ type SIR := [ _ : Link[I,@tab inf] ] #/ declared: SIR -#/ unexpected errors: -#/ error[elab]: unexpected notation for term -#/ --> examples/tt/text/test_tabulators.dbltt:9:16 -#/ 9| _ : Link[I,@tab inf] -#/ 9| ^^^^^^^^ -#/ error[elab]: synthesized type ?1 does not match expected type Tab Hom Object: -#/ tried to convert between types of different type constructors -#/ --> examples/tt/text/test_tabulators.dbltt:9:16 -#/ 9| _ : Link[I,@tab inf] -#/ 9| ^^^^^^^^ generate SIR -#/ result: model generated by 3 objects and 2 morphisms +#/ result: model generated by 3 objects and 3 morphisms #/ S : Object #/ I : Object #/ R : Object #/ inf : S -> I : Hom Object #/ rec : I -> R : Hom Object +#/ _ : I -> inf : Link type Endo := [ A : Object, @@ -61,16 +52,6 @@ type WalkingLink := [ l : Link[z,@tab f] ] #/ declared: WalkingLink -#/ unexpected errors: -#/ error[elab]: unexpected notation for term -#/ --> examples/tt/text/test_tabulators.dbltt:32:16 -#/ 32| l : Link[z,@tab f] -#/ 32| ^^^^^^ -#/ error[elab]: synthesized type ?1 does not match expected type Tab Hom Object: -#/ tried to convert between types of different type constructors -#/ --> examples/tt/text/test_tabulators.dbltt:32:16 -#/ 32| l : Link[z,@tab f] -#/ 32| ^^^^^^ type SI := [ S : Object, @@ -80,19 +61,14 @@ type SI := [ #/ declared: SI generate SI -#/ result: model generated by 2 objects and 1 morphism +#/ result: model generated by 2 objects and 2 morphisms #/ S : Object #/ I : Object #/ inf.f : S -> I : Hom Object +#/ inf.l : I -> inf.f : Link chk [sf : SI] (sf.inf.l : Link[sf.I,@tab sf.inf.f]) -#/ result: ?0 -#/ unexpected errors: -#/ error[elab]: synthesized type (Link)[sf.I, ?2] does not match expected type (Link)[sf.I, (@tab sf.inf.f)]: -#/ could not convert codomains: failed to match terms ?2 and (@tab sf.inf.f) -#/ --> examples/tt/text/test_tabulators.dbltt:43:16 -#/ 43| chk [sf : SI] (sf.inf.l : Link[sf.I,@tab sf.inf.f]) -#/ 43| ^^^^^^^^ +#/ result: sf.inf.l chk [sf : SI] (sf.inf : WalkingLink) #/ result: sf.inf @@ -108,7 +84,7 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) #/ y : Object, #/ z : Object, #/ f : (Hom Object)[self.x, self.y], -#/ l : (Link)[self.z, ?2]] +#/ l : (Link)[self.z, (@tab self.f)]] #/ & #/ [.x : @sing sf.I]: #/ Neutrals sf.S and sf.I are not equal. @@ -118,12 +94,6 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l #/ declared: act_on_link -#/ unexpected errors: -#/ error[elab]: synthesized type (Link)[d.S, ?2] does not match expected type (Link)[d.S, (@tab d.inf.f)]: -#/ could not convert codomains: failed to match terms ?2 and (@tab d.inf.f) -#/ --> examples/tt/text/test_tabulators.dbltt:52:53 -#/ 52| def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l -#/ 52| ^^^^^^^^^^^^^^^^^ type Triangle := [ x : Object, @@ -147,13 +117,7 @@ def delete_y[T : Triangle] : SI := [ ] ] #/ declared: delete_y -#/ unexpected errors: -#/ error[elab]: synthesized type (Link)[T.z, (@tab T.f · T.g)] does not match expected type (Link)[T.z, ?2]: -#/ could not convert codomains: failed to match terms (@tab T.f · T.g) and ?2 -#/ --> examples/tt/text/test_tabulators.dbltt:71:14 -#/ 71| l := T.l -#/ 71| ^^^ syn [T : Triangle] delete_y[T].inf.l -#/ result: delete_y[T].inf.l : (Link)[T.z, ?2] +#/ result: delete_y[T].inf.l : (Link)[T.z, (@tab T.f · T.g)] diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index a2a45a390..8efc31503 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -51,7 +51,7 @@ impl<'a> Evaluator<'a> { match &**ty { TyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), - TopDecl::Diag(d) => d.body_val.clone(), + TopDecl::Diag(d) => d.body_ty.clone(), _ => panic!("top-level {tv} should be a type or diagram declaration"), }, TyS_::Object(ot) => TyV::object(ot.clone()), @@ -100,6 +100,19 @@ impl<'a> Evaluator<'a> { TmS_::OverApp(mor, mor_label, tgt_path, inner) => { TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eval_tm(inner)) } + TmS_::Instance(body) => TmV::instance(InstanceBodyV { + generators: body.generators.clone(), + equations: body + .equations + .iter() + .map(|(lhs, rhs)| (self.eval_tm(lhs), self.eval_tm(rhs))) + .collect(), + sub_instances: body + .sub_instances + .iter() + .map(|(name, (label, inner))| (*name, (*label, self.eval_tm(inner)))) + .collect(), + }), TmS_::Meta(mv) => TmV::meta(*mv), } } @@ -151,6 +164,26 @@ impl<'a> Evaluator<'a> { self.bind_neu("self".into(), "self".into(), ty) } + /// Synthesize the record type matching an instance body's + /// structure. Generators become `@over`-typed fields; sub-instances + /// recurse. Equations are elided (they aren't projectable). + pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { + let mut fields: Row = Row::empty(); + for (name, (label, path)) in &body.generators { + fields.insert(*name, *label, TyS::over(path.clone())); + } + for (name, (label, sub_v)) in &body.sub_instances { + let TmV_::Instance(sub_body) = &**sub_v else { + continue; + }; + let sub_ty_v = self.synth_instance_body_ty(sub_body); + let sub_ty_s = self.quote_ty(&sub_ty_v); + fields.insert(*name, *label, sub_ty_s); + } + let r_v = RecordV::new(self.env.clone(), fields, Dtry::empty()); + TyV::record(r_v) + } + /// Produce type syntax from a type value. /// /// This is a *section* of eval, in that `self.eval_ty(self.quote_ty(ty_v)) == ty_v` @@ -224,6 +257,19 @@ impl<'a> Evaluator<'a> { TmV_::OverApp(mor, mor_label, tgt_path, inner) => { TmS::over_app(*mor, *mor_label, tgt_path.clone(), self.quote_tm(inner)) } + TmV_::Instance(body) => TmS::instance(InstanceBodyS { + generators: body.generators.clone(), + equations: body + .equations + .iter() + .map(|(lhs, rhs)| (self.quote_tm(lhs), self.quote_tm(rhs))) + .collect(), + sub_instances: body + .sub_instances + .iter() + .map(|(name, (label, inner))| (*name, (*label, self.quote_tm(inner)))) + .collect(), + }), TmV_::List(elems) => TmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()), TmV_::Cons(fields) => TmS::cons(fields.map(|tm| self.quote_tm(tm))), TmV_::Tt => TmS::tt(), @@ -353,6 +399,9 @@ impl<'a> Evaluator<'a> { TmV_::OverApp(mor, mor_label, tgt_path, inner) => { TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eta(inner, None)) } + // An [`Instance`](TmV_::Instance) is already in normal form + // — its structure isn't subject to η-expansion. + TmV_::Instance(_) => v.clone(), TmV_::List(elems) => TmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()), TmV_::Cons(row) => { if let Some(ty) = ty { @@ -447,6 +496,38 @@ impl<'a> Evaluator<'a> { } self.equal_tm_helper(inner1, inner2, strict1, strict2) } + (TmV_::Instance(b1), TmV_::Instance(b2)) => { + if b1.generators.len() != b2.generators.len() + || b1.equations.len() != b2.equations.len() + || b1.sub_instances.len() != b2.sub_instances.len() + { + return Err(t("instance bodies have differing shapes")); + } + for ((n1, (_, p1)), (n2, (_, p2))) in + b1.generators.iter().zip(b2.generators.iter()) + { + if n1 != n2 || p1 != p2 { + return Err(t(format!("instance generator {n1} differs from {n2}"))); + } + } + for ((lhs1, rhs1), (lhs2, rhs2)) in + b1.equations.iter().zip(b2.equations.iter()) + { + self.equal_tm_helper(lhs1, lhs2, strict1, strict2)?; + self.equal_tm_helper(rhs1, rhs2, strict1, strict2)?; + } + for ((n1, (_, t1)), (n2, (_, t2))) in + b1.sub_instances.iter().zip(b2.sub_instances.iter()) + { + if n1 != n2 { + return Err(t(format!( + "instance sub-instance {n1} differs from {n2}" + ))); + } + self.equal_tm_helper(t1, t2, strict1, strict2)?; + } + Ok(()) + } _ => Err(t(format!( "failed to match terms {} and {}", self.quote_tm(tm1), diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index cb0079b28..22f1c6bc4 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -408,10 +408,10 @@ impl<'a> ModelGenerator<'a> { /// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Diag`]. /// -/// Walks the diagram body, adding one instance generator per `@over .X` -/// field (with fiber resolved against the codomain) and one equation per -/// `_ : (lhs == rhs)` field whose carrier is an `@over` type. No synthetic -/// lift-target objects or lift morphisms are materialized. +/// Walks the instance body's [`TmV_::Instance`] payload, registering +/// each generator with its fiber, each equation as a pair of +/// [`DiscreteInstanceTerm`]s, and each sub-instance's contents under +/// the appropriate prefix. /// /// Restricted to discrete double theories for the moment. pub fn instance_from_diag( @@ -426,99 +426,97 @@ pub fn instance_from_diag( let cod_model = cod_model .as_discrete() .ok_or_else(|| "expected a discrete codomain model".to_string())?; - let mut generator = InstanceGenerator { - eval: Evaluator::empty(toplevel), - codomain_ty: diag.model.clone(), - instance: DiscreteDblModelInstance::new(Rc::new(cod_model)), + let mut instance = DiscreteDblModelInstance::new(Rc::new(cod_model)); + let TmV_::Instance(body) = &*diag.body_val else { + return Err("expected a TmV::Instance body".into()); }; - let namespace = generator.generate(&diag.body_val)?; - Ok((generator.instance, namespace)) + let mut namespace = Namespace::new_for_uuid(); + extract_instance_body(&mut instance, &mut namespace, &[], body)?; + Ok((instance, namespace)) } -struct InstanceGenerator<'a> { - eval: Evaluator<'a>, - codomain_ty: TyV, - instance: DiscreteDblModelInstance, -} - -impl InstanceGenerator<'_> { - fn generate(&mut self, body_ty: &TyV) -> Result { - let (self_n, eval_with_self) = self.eval.bind_self(body_ty.clone()); - self.eval = eval_with_self; - let self_val = self.eval.eta_neu(&self_n, body_ty); - Ok(self - .extract(vec![], &self_val, body_ty)? - .unwrap_or_else(Namespace::new_for_uuid)) - } - - fn extract( - &mut self, - prefix: Vec, - val: &TmV, - ty: &TyV, - ) -> Result, String> { - match &**ty { - TyV_::Record(r) => { - let mut namespace = Namespace::new_for_uuid(); - for (name, (label, _)) in r.fields.iter() { - let mut child_prefix = prefix.clone(); - child_prefix.push(*name); - if let NameSegment::Uuid(uuid) = name { - namespace.set_label(*uuid, *label); - } - let field_tm = self.eval.proj(val, *name, *label); - let field_ty = self.eval.field_ty(ty, val, *name); - if let Some(inner) = self.extract(child_prefix, &field_tm, &field_ty)? { - namespace.add_inner(*name, inner); - } - } - Ok(Some(namespace)) - } - TyV_::Over(path) => { - // Add an instance generator over the resolved codomain object. - let (cod_self_n, cod_eval) = self.eval.bind_self(self.codomain_ty.clone()); - let cod_val = cod_eval.eta_neu(&cod_self_n, &self.codomain_ty); - let resolved = cod_eval.path_ty(&self.codomain_ty, &cod_val, path)?; - let TyV_::Object(_) = &*resolved else { - return Err( - "@over path does not refer to an object generator in the codomain".into() - ); - }; - let qname: QualifiedName = prefix.into(); - let fiber: QualifiedName = - path.iter().map(|(seg, _)| *seg).collect::>().into(); - self.instance.add_generator(qname, fiber); - Ok(None) - } - TyV_::Id(carrier, lhs, rhs) if matches!(&**carrier, TyV_::Over(_)) => { - let lhs_t = tm_to_discrete_instance_term(lhs)?; - let rhs_t = tm_to_discrete_instance_term(rhs)?; - self.instance.add_equation(lhs_t, rhs_t); - Ok(None) - } - _ => Ok(None), +fn extract_instance_body( + instance: &mut DiscreteDblModelInstance, + namespace: &mut Namespace, + prefix: &[NameSegment], + body: &InstanceBodyV, +) -> Result<(), String> { + for (name, (label, path)) in &body.generators { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); } + let mut qsegs = prefix.to_vec(); + qsegs.push(*name); + let qname: QualifiedName = qsegs.into(); + let fiber: QualifiedName = path.iter().map(|(seg, _)| *seg).collect::>().into(); + instance.add_generator(qname, fiber); } + for (lhs, rhs) in &body.equations { + let lhs_t = tm_to_discrete_instance_term_with_prefix(lhs, prefix)?; + let rhs_t = tm_to_discrete_instance_term_with_prefix(rhs, prefix)?; + instance.add_equation(lhs_t, rhs_t); + } + for (name, (label, sub_v)) in &body.sub_instances { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let TmV_::Instance(sub_body) = &**sub_v else { + return Err("sub-instance is not a TmV::Instance".into()); + }; + let mut sub_prefix = prefix.to_vec(); + sub_prefix.push(*name); + extract_instance_body(instance, namespace, &sub_prefix, sub_body)?; + } + Ok(()) } -/// Convert an `@over`-typed term value into a [`DiscreteInstanceTerm`]. -/// -/// Handles bare neutrals (projections of declared generator fields, which -/// become [`DiscreteInstanceTerm::Generator`]) and nested -/// [`TmV_::OverApp`] applications. The morphism is a single codomain field -/// name, wrapped as a singleton [`Path`]. -fn tm_to_discrete_instance_term(tm: &TmV) -> Result { +/// Convert an `@over`-typed term value into a [`DiscreteInstanceTerm`], +/// prefixing each generator name with the path into any enclosing +/// sub-instances. +fn tm_to_discrete_instance_term_with_prefix( + tm: &TmV, + prefix: &[NameSegment], +) -> Result { match &**tm { - TmV_::Neu(n, _) => Ok(DiscreteInstanceTerm::Generator(n.to_qualified_name())), + TmV_::Neu(n, _) => { + let mut segs = prefix.to_vec(); + segs.extend(neu_full_name(n)); + Ok(DiscreteInstanceTerm::Generator(segs.into())) + } TmV_::OverApp(mor_name, _, _, inner) => { let mor_qname = QualifiedName::single(*mor_name); - let inner_t = tm_to_discrete_instance_term(inner)?; + let inner_t = tm_to_discrete_instance_term_with_prefix(inner, prefix)?; Ok(DiscreteInstanceTerm::Apply(mor_qname, Box::new(inner_t))) } _ => Err("term is not a generator or codomain-morphism application".into()), } } +/// Read off the full name of a neutral, including its leading variable +/// name and any subsequent projection segments. Unlike +/// [`TmN::to_qualified_name`] this does *not* treat the leading +/// variable as an implicit root — for instance-body terms the leading +/// variable is itself a generator name (e.g. `v1`) or sub-instance +/// name (e.g. `we`), and must contribute a segment. +fn neu_full_name(n: &TmN) -> Vec { + let mut segments = Vec::new(); + let mut cur = n; + loop { + match &**cur { + TmN_::Var(_, name, _) => { + segments.push(*name); + break; + } + TmN_::Proj(n1, f, _) => { + segments.push(*f); + cur = n1; + } + } + } + segments.reverse(); + segments +} + #[cfg(test)] mod tests { use super::*; @@ -558,9 +556,9 @@ type WeightedGraph := [ ] def I : WeightedGraph := [ - v : @over .V, - e : @over .E, - _ : (src(e) == v) + V := [v], + E := [e], + src(e) := v ] "#; let toplevel = elaborate_to_toplevel(src); diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 73062ae5f..0cd660c84 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -96,14 +96,18 @@ pub enum TyS_ { Meta(MetaVar), /// The type of terms in a fiber over an object generator of some - /// diagram's codomain model. + /// instance's codomain model. Internal-only; no surface syntax — + /// inhabitants are introduced via set-literal clauses `field := + /// [...]` inside an instance body, and applications via the + /// `mor(arg)` and `tm.mor(arg)` syn arms. /// - /// The path identifies the object generator in the codomain. Diagram - /// identity is contextual rather than part of the type: any diagram - /// in scope contributes its terms to this type, so two `@over .V` - /// types from different diagram declarations are convertible. The - /// elaborator validates the path against the enclosing diagram's - /// codomain at construction time. Example surface syntax: `e : @over .E`. + /// The path identifies the object generator in the codomain. + /// Instance identity is contextual rather than part of the type: + /// any instance body in scope contributes its terms to this type, + /// so two fiber-types over the same codomain path from different + /// instance declarations are convertible. The elaborator validates + /// the path against the enclosing instance's codomain at + /// construction time. Over(Vec<(FieldName, LabelSegment)>), } @@ -205,6 +209,19 @@ fn path_to_string(path: &[(FieldName, LabelSegment)]) -> String { out } +fn instance_body_to_doc<'a>(body: &InstanceBodyS) -> D<'a> { + let gens = body.generators.iter().map(|(_, (label, path))| { + binop(t(":"), t(format!("{label}")), t(format!("@over{}", path_to_string(path)))) + }); + let eqns = body.equations.iter().map(|(lhs, rhs)| { + binop(t(":="), lhs.to_doc(), rhs.to_doc()) + }); + let subs = body.sub_instances.iter().map(|(_, (label, inner))| { + binop(t(":"), t(format!("{label}")), inner.to_doc()) + }); + tuple(gens.chain(subs).chain(eqns)) +} + impl fmt::Display for TyS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_doc().group().pretty()) @@ -240,8 +257,8 @@ pub enum TmS_ { ObApp(VarName, TmS), /// List of objects. List(Vec), - /// Application of a codomain morphism to an [`@over`-typed](TyS_::Over) - /// term. Only well-formed inside a diagram declaration. + /// Application of a codomain morphism to a fiber-typed + /// ([`TyS_::Over`]) term. Only well-formed inside an instance body. /// /// Arguments, in order: /// 1. `mor` — name of the codomain morphism being applied @@ -249,27 +266,57 @@ pub enum TmS_ { /// 2. `mor_label` — display label for that name. /// 3. `tgt_path` — the codomain object-path that `mor` lands at. /// Stored on the node so [`Evaluator::eval_tm`] can recover the - /// result type `@over ` without consulting the codomain. - /// 4. `inner` — the `@over`-typed argument (e.g. the elaboration of - /// `we.e`, whose type is `@over ` where the codomain - /// morphism `mor : `). + /// result fiber type without consulting the codomain. + /// 4. `inner` — the fiber-typed argument (e.g. the elaboration of + /// `we.e`, whose type is the fiber over `` where the + /// codomain morphism `mor : `). /// /// Example: in /// ```text - /// diagram I : @Instance(WeightedGraph) := [ - /// we : Edge, // Edge := [e : @over .E] - /// _ : (we.src(e) == v1), + /// def Edge : WeightedGraph := [E := [e]] + /// def I : WeightedGraph := [ + /// V := [v1], + /// we : Edge, + /// we.src(e) := v1, /// ] /// ``` - /// the LHS `we.src(e)` elaborates to - /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of type `@over .V`. + /// the LHS of the mapping clause elaborates to + /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of fiber-type + /// over `.V`. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmS), + /// An instance value: the level-shifted introduction rule for terms + /// of a model (sketch) type. See [`InstanceBodyS`] for the payload. + Instance(InstanceBodyS), /// A metavar. /// /// This only appears when we have an error in elaboration. Meta(MetaVar), } +/// Payload of [`TmS_::Instance`]. +/// +/// Captures the data needed to build a `DblModelInstance` at extraction +/// time, while staying theory-agnostic at the term-syntax level. +/// +/// - `generators` maps each generator's local name (within this +/// instance) to a path into the model identifying which object +/// generator's fiber it lives over. The path mirrors what a +/// surface `name : @over ` clause used to declare. +/// - `equations` is a list of `(lhs, rhs)` pairs over either `@over` +/// or morphism types. Order is preserved but identity isn't +/// semantically significant. +/// - `sub_instances` maps each sub-instance import's local name to a +/// nested instance term. This is what surface `we : Edge` lowers to. +#[derive(Default)] +pub struct InstanceBodyS { + /// Generators introduced by this instance, with their fibers. + pub generators: IndexMap)>, + /// Equation witnesses, asserted to hold in this instance. + pub equations: Vec<(TmS, TmS)>, + /// Sub-instance imports, keyed by import name. + pub sub_instances: IndexMap, +} + /// Syntax for total terms, dereferences to [TmS_]. /// /// See [crate::tt] for an explanation of what total types are, and for an @@ -344,6 +391,11 @@ impl TmS { Self(Rc::new(TmS_::OverApp(mor, mor_label, tgt_path, inner))) } + /// Smart constructor for [TmS], [TmS_::Instance] case. + pub fn instance(body: InstanceBodyS) -> Self { + Self(Rc::new(TmS_::Instance(body))) + } + /// Smart constructor for [TmS], [TmS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TmS_::Meta(mv))) @@ -370,6 +422,7 @@ impl ToDoc for TmS { TmS_::OverApp(_, mor_label, _, inner) => { inner.to_doc() + t(format!(".{mor_label}")) } + TmS_::Instance(body) => instance_body_to_doc(body), TmS_::Tt => t("tt"), TmS_::Meta(mv) => t(format!("?{}", mv.id)), } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index c2a1cc83d..e879bc088 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -27,22 +27,6 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory"], ); -/// Temporary `def`-time dispatch — see Pass D in `tt/README` (TODO) -/// for the eventual move into `chk`. A body is instance-shaped if it -/// contains any `:` clause, any `mor(arg) := target` clause (LHS is an -/// application), or any `field := [...]` set-literal clause. -fn body_is_instance_shaped(n: &FNtn) -> bool { - let Tuple(field_ns) = n.ast0() else { - return false; - }; - field_ns.iter().any(|field_n| match field_n.ast0() { - App2(L(_, Keyword(":")), _, _) => true, - App2(L(_, Keyword(":=")), L(_, Var(_)), L(_, Tuple(_))) => true, - App2(L(_, Keyword(":=")), L(_, App1(_, _)), _) => true, - _ => false, - }) -} - /// The result of elaborating a top-level statement. pub enum TopElabResult { /// A new declaration. @@ -177,21 +161,27 @@ impl TopElaborator { None => { let mut elab = self.elaborator(&theory, toplevel); let (_, ret_ty_v) = elab.ty(ty_n); - // An "instance body" is a tuple of `:`-bound clauses - // (rather than `:=`-bound ones). Route those to the - // instance-elaboration path; everything else is an - // ordinary const-term `def`. - if body_is_instance_shaped(tm_n) { - elab.push_instance_codomain(ret_ty_v.clone()); - let (val_s, val_v) = elab.ty(tm_n); - elab.pop_instance_codomain(); - Some(TopElabResult::Declaration( - name, - TopDecl::Diag(Diag::new(theory.clone(), ret_ty_v, val_s, val_v)), - )) - } else { - let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); - Some(TopElabResult::Declaration( + let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); + // chk dispatches: if the body was instance-shaped, + // the returned TmV is a [`TmV_::Instance`], and we + // wrap as an instance declaration with a + // synthesized record type for sub-instance lookups. + // Otherwise the body is an ordinary const term. + match &*tm_v { + TmV_::Instance(body) => { + let body_ty = elab.evaluator().synth_instance_body_ty(body); + Some(TopElabResult::Declaration( + name, + TopDecl::Diag(Diag::new( + theory.clone(), + ret_ty_v, + tm_s, + tm_v, + body_ty, + )), + )) + } + _ => Some(TopElabResult::Declaration( name, TopDecl::DefConst(DefConst::new( theory.clone(), @@ -199,7 +189,7 @@ impl TopElaborator { tm_v, ret_ty_v, )), - )) + )), } } } @@ -405,6 +395,335 @@ impl<'a> Elaborator<'a> { v } + /// Apply a codomain morphism `f` to an already-elaborated argument + /// of fiber type. Shared by the bare `f(x)` and `f(receiver.fld)` + /// arms of [`Self::syn`]. + fn apply_codomain_morphism( + &mut self, + f: &str, + arg_s: TmS, + arg_v: TmV, + arg_ty: TyV, + arg_label_str: &str, + ) -> (TmS, TmV, TyV) { + let Some(model_ty) = self.current_instance_codomain().cloned() else { + return self.syn_error( + "applied codomain morphism is only allowed inside an instance body", + ); + }; + let TyV_::Over(src_path) = &*arg_ty else { + let quoted = self.evaluator().quote_ty(&arg_ty); + return self.syn_error(format!( + "argument {arg_label_str} has type {quoted}, expected a fiber type", + )); + }; + let f_label = label_seg(f); + let f_name = name_seg(f); + let TyV_::Record(cod_r) = &*model_ty else { + return self.syn_error("instance codomain is not a record type"); + }; + let Some(mor_ty_s) = cod_r.fields.get(f_name) else { + return self.syn_error(format!("no such codomain morphism {f_name}")); + }; + let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + return self.syn_error(format!("codomain field {f_name} is not a morphism")); + }; + let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) else { + return self.syn_error(format!( + "codomain morphism {f_name} has non-path dom/cod; \ + applied-morphism syntax requires both to be paths", + )); + }; + if dom_path != *src_path { + return self.syn_error(format!( + "codomain morphism {f_name} has source path differing from the argument", + )); + } + ( + TmS::over_app(f_name, f_label, cod_path.clone(), arg_s), + TmV::over_app(f_name, f_label, cod_path.clone(), arg_v), + TyV::over(cod_path), + ) + } + + /// Elaborate an instance body — a tuple of `name : type`, `field + /// := [names]`, and `mor(arg) := target` clauses — against the + /// enclosing sketch type `model_ty`. Produces a [`TmS_::Instance`] + /// / [`TmV_::Instance`] pair whose payload is the instance's + /// generator slots, equation witnesses, and sub-instance imports. + /// + /// The codomain is pushed onto the instance-codomain stack so that + /// `@over .X` annotations and applied-codomain-morphism syntax + /// inside the body resolve correctly. + fn instance_body(&mut self, model_ty: &TyV, n: &FNtn) -> (TmS, TmV) { + self.push_instance_codomain(model_ty.clone()); + let result = self.instance_body_inner(n); + self.pop_instance_codomain(); + result + } + + fn instance_body_inner(&mut self, n: &FNtn) -> (TmS, TmV) { + let mut elab = self.enter(n.loc()); + let Tuple(field_ns) = n.ast0() else { + elab.error::<()>("expected a tuple instance body"); + return (TmS::instance(InstanceBodyS::default()), TmV::instance(InstanceBodyV::default())); + }; + let mut gens: IndexMap)> = + IndexMap::new(); + let mut eqns_s: Vec<(TmS, TmS)> = Vec::new(); + let mut eqns_v: Vec<(TmV, TmV)> = Vec::new(); + let mut subs_s: IndexMap = IndexMap::new(); + let mut subs_v: IndexMap = IndexMap::new(); + let mut failed = false; + + for field_n in field_ns.iter() { + elab.loc = Some(field_n.loc()); + match field_n.ast0() { + // `name : type` — generator, sub-instance, or + // anonymous equation clause (dispatched on the + // elaborated type's shape). + App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { + let name_str = *name; + let n_seg = if name_str == "_" { + NameSegment::Uuid(uuid::Uuid::new_v4()) + } else { + name_seg(name_str) + }; + let label = label_seg(name_str); + let (_, ty_v) = elab.ty(ty_n); + match &*ty_v { + TyV_::Over(path) => { + gens.insert(n_seg, (label, path.clone())); + elab.intro(n_seg, label, Some(ty_v)); + } + TyV_::Record(_) => { + let (sub_s, sub_v) = match ty_n.ast0() { + Var(sub_name) => { + let topvar = name_seg(*sub_name); + match elab.toplevel.declarations.get(&topvar) { + Some(TopDecl::Diag(d)) => { + (d.body_stx.clone(), d.body_val.clone()) + } + _ => { + elab.error::<()>(format!( + "sub-instance {sub_name} must reference a \ + top-level instance declaration", + )); + failed = true; + continue; + } + } + } + _ => { + elab.error::<()>( + "sub-instance type must be a top-level def name", + ); + failed = true; + continue; + } + }; + subs_s.insert(n_seg, (label, sub_s)); + subs_v.insert(n_seg, (label, sub_v)); + elab.intro(n_seg, label, Some(ty_v)); + } + TyV_::Id(_, lhs, rhs) => { + let evaluator = elab.evaluator(); + let lhs_s = evaluator.quote_tm(lhs); + let rhs_s = evaluator.quote_tm(rhs); + eqns_s.push((lhs_s, rhs_s)); + eqns_v.push((lhs.clone(), rhs.clone())); + } + _ => { + let quoted = elab.evaluator().quote_ty(&ty_v); + elab.error::<()>(format!( + "instance clause {name_str} has type {quoted}, expected \ + @over, a sub-sketch, or an equation", + )); + failed = true; + } + } + } + // `field := [k1 := t1, k2 := t2, ...]` — mapping-literal + // assignment to a morphism-typed field of the codomain. + // Equivalent to a sequence of per-entry mapping clauses + // `field(k1) := t1`, `field(k2) := t2`, ... + App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(entries))) + if !entries.is_empty() + && entries + .iter() + .all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) => + { + let Some(codomain) = elab.current_instance_codomain().cloned() else { + elab.error::<()>( + "mapping-literal assignment is only allowed inside an instance body", + ); + failed = true; + continue; + }; + let TyV_::Record(cod_r) = &*codomain else { + elab.error::<()>("instance codomain is not a record type"); + failed = true; + continue; + }; + let f_seg = name_seg(*field_name); + let f_label = label_seg(*field_name); + let Some(mor_ty_s) = cod_r.fields.get(f_seg) else { + elab.error::<()>(format!( + "no such codomain field {field_name}" + )); + failed = true; + continue; + }; + let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + elab.error::<()>(format!( + "mapping-literal assignment requires field {field_name} to be \ + morphism-typed", + )); + failed = true; + continue; + }; + let (Some(dom_path), Some(cod_path)) = + (tms_to_path(dom_s), tms_to_path(cod_s)) + else { + elab.error::<()>(format!( + "codomain morphism {field_name} has non-path dom/cod; \ + mapping-literal assignment requires both to be paths", + )); + failed = true; + continue; + }; + let mut entry_failed = false; + for entry_n in entries.iter() { + elab.loc = Some(entry_n.loc()); + let App2(L(_, Keyword(":=")), key_n, target_n) = entry_n.ast0() else { + unreachable!("guard ensured all entries are `:=` clauses"); + }; + let (key_s, key_v, key_ty) = elab.syn(key_n); + let TyV_::Over(key_path) = &*key_ty else { + let quoted = elab.evaluator().quote_ty(&key_ty); + elab.error::<()>(format!( + "mapping-literal key has type {quoted}, expected an @over type", + )); + entry_failed = true; + break; + }; + if key_path != &dom_path { + elab.error::<()>(format!( + "mapping-literal key's @over path does not match the source of {field_name}", + )); + entry_failed = true; + break; + } + let lhs_ty = TyV::over(cod_path.clone()); + let lhs_s = + TmS::over_app(f_seg, f_label, cod_path.clone(), key_s); + let lhs_v = + TmV::over_app(f_seg, f_label, cod_path.clone(), key_v); + let (rhs_s, rhs_v) = elab.chk(&lhs_ty, target_n); + eqns_s.push((lhs_s, rhs_s)); + eqns_v.push((lhs_v, rhs_v)); + } + if entry_failed { + failed = true; + continue; + } + } + // `field := [n1, n2, ...]` — set-literal assignment to + // an object-typed field of the codomain. + App2( + L(_, Keyword(":=")), + L(_, Var(field_name)), + L(_, Tuple(name_ns)), + ) => { + let Some(codomain) = elab.current_instance_codomain().cloned() else { + elab.error::<()>( + "set-literal field assignment is only allowed inside an \ + instance body", + ); + failed = true; + continue; + }; + let TyV_::Record(cod_r) = &*codomain else { + elab.error::<()>("instance codomain is not a record type"); + failed = true; + continue; + }; + let f_seg = name_seg(*field_name); + let f_label = label_seg(*field_name); + let Some(field_ty_s) = cod_r.fields.get(f_seg) else { + elab.error::<()>(format!("no such codomain field {field_name}")); + failed = true; + continue; + }; + if !matches!(&**field_ty_s, TyS_::Object(_)) { + elab.error::<()>(format!( + "set-literal assignment requires field {field_name} to be \ + object-typed", + )); + failed = true; + continue; + } + let path = vec![(f_seg, f_label)]; + let gen_ty = TyV::over(path.clone()); + for name_n in name_ns.iter() { + let Var(gen_name) = name_n.ast0() else { + elab.loc = Some(name_n.loc()); + elab.error::<()>("set-literal entries must be bare names"); + failed = true; + break; + }; + let gen_seg = name_seg(*gen_name); + let gen_label = label_seg(*gen_name); + gens.insert(gen_seg, (gen_label, path.clone())); + elab.intro(gen_seg, gen_label, Some(gen_ty.clone())); + } + } + // `mor(arg) := target` — mapping-entry clause: + // an equation witness. + App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { + let (lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); + if !matches!(&*lhs_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { + elab.loc = Some(lhs_n.loc()); + elab.error::<()>( + "mapping-entry clause `mor(arg) := target` requires the LHS \ + to have a morphism or @over type", + ); + failed = true; + continue; + } + let (rhs_s, rhs_v) = elab.chk(&lhs_ty, rhs_n); + eqns_s.push((lhs_s, rhs_s)); + eqns_v.push((lhs_v, rhs_v)); + } + _ => { + elab.error::<()>( + "expected fields in the form `name : type`, \ + `field := [names]`, or `mor(arg) := target`", + ); + failed = true; + } + } + } + + if failed { + return ( + TmS::instance(InstanceBodyS::default()), + TmV::instance(InstanceBodyV::default()), + ); + } + let body_s = InstanceBodyS { + generators: gens.clone(), + equations: eqns_s, + sub_instances: subs_s, + }; + let body_v = InstanceBodyV { + generators: gens, + equations: eqns_v, + sub_instances: subs_v, + }; + (TmS::instance(body_s), TmV::instance(body_v)) + } + fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, TyS, TyV)> { let mut elab = self.enter(n.loc()); match n.ast0() { @@ -434,10 +753,11 @@ impl<'a> Elaborator<'a> { } TopDecl::Diag(d) => { if d.theory == self.theory { - // Using a diagram name as a type yields the diagram's - // body record, allowing `we : I & [...]`-style - // specializations to walk paths through I's fields. - (TyS::topvar(name), d.body_val.clone()) + // Using an instance name as a type yields the + // instance's synthesized record type, allowing + // sub-instance imports (`we : Edge`) to project + // into Edge's fields. + (TyS::topvar(name), d.body_ty.clone()) } else { self.ty_error(format!( "{name} refers to a diagram in theory {}, expected theory {}", @@ -524,34 +844,6 @@ impl<'a> Elaborator<'a> { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); (TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v)) } - // `@Instance(X)` is a structural alias for `X`, used in diagram - // annotations. Treating it as an alias keeps the type system - // simple; `I` is kept out of term lookup by the diagram-kind - // binding pushed in the `diagram` toplevel arm. - App1(L(_, Prim("Instance")), x_n) => elab.ty(x_n), - App1(L(_, Prim("over")), p_n) => { - let Some(path) = elab.path(p_n) else { - return elab.ty_hole(); - }; - // The enclosing instance body gives the codomain against - // which we validate the path. The codomain's identity is - // not stored in the resulting type — `@over` names a - // fiber-type that's shared across instances. - let Some(model_ty) = elab.current_instance_codomain().cloned() else { - return elab.ty_error("@over is only allowed inside an instance body"); - }; - let evaluator = elab.evaluator(); - let (self_n, _) = evaluator.bind_self(model_ty.clone()); - let self_val = evaluator.eta_neu(&self_n, &model_ty); - let resolved = match evaluator.path_ty(&model_ty, &self_val, &path) { - Ok(t) => t, - Err(msg) => return elab.ty_error(msg), - }; - let TyV_::Object(_) = &*resolved else { - return elab.ty_error("path does not refer to an object generator"); - }; - (TyS::over(path.clone()), TyV::over(path)) - } App1(mt_n, L(_, Tuple(domcod_n))) => { let [dom_n, cod_n] = domcod_n.as_slice() else { return elab.ty_error("expected two arguments for morphism type"); @@ -570,131 +862,28 @@ impl<'a> Elaborator<'a> { let c = elab.checkpoint(); for field_n in field_ns.iter() { elab.loc = Some(field_n.loc()); - // Each clause emits zero or more field-row entries. - // Set-literal clauses introduce one entry per generator - // name; everything else is single-entry. - let entries: Vec<(FieldName, LabelSegment, TyV)> = match field_n.ast0() { - // `name : type` — ordinary typed field declaration - // (generator slot, sub-instance, or annotated - // equation field). + let Some((name, label, ty_n)) = (match field_n.ast0() { App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { // `_` is a fresh anonymous binder — give it a // UUID name so distinct `_` fields don't collide // in the record row. - let n = if *name == "_" { + let name_seg = if *name == "_" { NameSegment::Uuid(uuid::Uuid::new_v4()) } else { name_seg(*name) }; - let (_, ty_v) = elab.ty(ty_n); - vec![(n, label_seg(*name), ty_v)] - } - // `field := [name1, name2, ...]` — set-literal - // assignment to an object-typed field of the - // enclosing instance codomain. Each `nameI` - // introduces a fresh generator slot of type - // `@over .`. - App2( - L(_, Keyword(":=")), - L(_, Var(field_name)), - L(_, Tuple(name_ns)), - ) => { - let Some(model_ty) = elab.current_instance_codomain().cloned() else { - elab.error::<()>( - "set-literal field assignment is only allowed inside an \ - instance body", - ); - failed = true; - continue; - }; - let TyV_::Record(cod_r) = &*model_ty else { - elab.error::<()>("instance codomain is not a record type"); - failed = true; - continue; - }; - let f_seg = name_seg(*field_name); - let f_label = label_seg(*field_name); - let Some(field_ty_s) = cod_r.fields.get(f_seg) else { - elab.error::<()>(format!( - "no such codomain field {field_name}" - )); - failed = true; - continue; - }; - if !matches!(&**field_ty_s, TyS_::Object(_)) { - elab.error::<()>(format!( - "set-literal assignment requires field {field_name} to be \ - object-typed" - )); - failed = true; - continue; - }; - let path = vec![(f_seg, f_label)]; - let gen_ty_v = TyV::over(path); - let mut entries = Vec::with_capacity(name_ns.len()); - let mut local_failed = false; - for name_n in name_ns.iter() { - let Var(gen_name) = name_n.ast0() else { - elab.loc = Some(name_n.loc()); - elab.error::<()>( - "set-literal entries must be bare names", - ); - local_failed = true; - break; - }; - entries.push(( - name_seg(*gen_name), - label_seg(*gen_name), - gen_ty_v.clone(), - )); - } - if local_failed { - failed = true; - continue; - } - entries - } - // `mor(arg) := target` — mapping-entry clause: - // sugar for an anonymous Id-typed field witnessing - // the equation `mor(arg) == target`. The LHS - // synthesizes (and validates) via the same `f(x)` - // arm in [`Self::syn`]. - App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { - let (_lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); - if !matches!(&*lhs_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { - elab.loc = Some(lhs_n.loc()); - elab.error::<()>( - "mapping-entry clause `mor(arg) := target` requires the LHS \ - to have a morphism or @over type", - ); - failed = true; - continue; - } - let (_rhs_s, rhs_v) = elab.chk(&lhs_ty, rhs_n); - let id_ty_v = TyV::id(lhs_ty, lhs_v, rhs_v); - vec![( - NameSegment::Uuid(uuid::Uuid::new_v4()), - label_seg("_"), - id_ty_v, - )] - } - _ => { - elab.error::<()>( - "expected fields in the form `name : type`, \ - `field := [names]`, or `mor(arg) := target`", - ); - failed = true; - continue; + Some((name_seg, label_seg(*name), ty_n)) } + _ => elab.error("expected fields in the form : "), + }) else { + failed = true; + continue; }; - for (name, label, ty_v) in entries { - field_ty_vs.push((name, (label, ty_v.clone()))); - elab.ctx.push_scope(name, label, Some(ty_v.clone())); - elab.ctx.env = elab.ctx.env.snoc(TmV::neu( - TmN::proj(self_var.clone(), name, label), - ty_v, - )); - } + let (_, ty_v) = elab.ty(ty_n); + field_ty_vs.push((name, (label, ty_v.clone()))); + elab.ctx.push_scope(name, label, Some(ty_v.clone())); + elab.ctx.env = + elab.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } if failed { return elab.ty_hole(); @@ -803,89 +992,34 @@ impl<'a> Elaborator<'a> { elab.evaluator().field_ty(&ty_v, &tm_v, f), ) } - // Applied codomain-morphism syntax, in either of two shapes: + // Applied codomain-morphism syntax. Two shapes: // - // `f(x)` — `x` is a variable of `@over ` in - // scope (e.g. a generator declared by an - // enclosing field), and `f` is the codomain - // morphism name. - // `tm.f(x)` — same idea, but the argument is the field - // `x` of the record-typed receiver `tm`, - // and `f` is still the codomain morphism - // name. The receiver belongs to the - // *argument*, not the morphism — `tm.f(x)` - // is sugar for `f(tm.x)`. + // `f(x)` — `x` is a variable of fiber type in + // scope. + // `f(receiver.fld)` — argument is a record projection + // (e.g. `src(we.e)`). // - // Both shapes elaborate to a [`TmS_::OverApp`] applying `f` - // (a codomain morphism in the enclosing diagram) to the + // Both elaborate to a [`TmS_::OverApp`] applying `f` (a + // codomain morphism in the enclosing instance) to the // resolved argument term. - App1(head_n, L(_, Var(x))) => { - let (f, inner) = match head_n.ast0() { - Var(f) => (f, None), - App1(tm_n, L(_, Field(f))) => (f, Some(tm_n)), - _ => return elab.syn_error("unexpected notation for term"), - }; - let Some(model_ty) = elab.current_instance_codomain().cloned() else { - return elab.syn_error( - "applied codomain morphism is only allowed inside an instance body", - ); - }; - let (inner_s, inner_v, inner_ty) = match inner { - None => elab.lookup_tm(ustr(*x)), - Some(tm_n) => { - let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - let TyV_::Record(r) = &*ty_v else { - return elab.syn_error("can only project from record type"); - }; - let x_label = label_seg(*x); - let x_name = name_seg(*x); - if !r.fields.has(x_name) { - return elab.syn_error(format!("no such field {x_name}")); - } - let ty = elab.evaluator().field_ty(&ty_v, &tm_v, x_name); - let s = TmS::proj(tm_s, x_name, x_label); - let v = elab.evaluator().proj(&tm_v, x_name, x_label); - (s, v, ty) - } - }; - let TyV_::Over(src_path) = &*inner_ty else { - let quoted = elab.evaluator().quote_ty(&inner_ty); - return elab.syn_error(format!( - "argument {x} has type {quoted}, expected an @over type" - )); - }; - let f_label = label_seg(*f); - let f_name = name_seg(*f); - let TyV_::Record(cod_r) = &*model_ty else { - return elab.syn_error("diagram codomain is not a record type"); - }; - let Some(mor_ty_s) = cod_r.fields.get(f_name) else { - return elab.syn_error(format!("no such codomain morphism {f_name}")); - }; - let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { - return elab.syn_error(format!( - "codomain field {f_name} is not a morphism" - )); - }; - let (Some(dom_path), Some(cod_path)) = - (tms_to_path(dom_s), tms_to_path(cod_s)) - else { - return elab.syn_error(format!( - "codomain morphism {f_name} has non-path dom/cod; \ - applied-projection syntax requires both to be paths" - )); + App1(L(_, Var(f)), L(_, Var(x))) => { + let (inner_s, inner_v, inner_ty) = elab.lookup_tm(ustr(*x)); + elab.apply_codomain_morphism(*f, inner_s, inner_v, inner_ty, *x) + } + App1(L(_, Var(f)), L(_, App1(recv_n, L(_, Field(arg_field))))) => { + let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); + let TyV_::Record(r) = &*recv_ty else { + return elab.syn_error("can only project from record type"); }; - if dom_path != *src_path { - return elab.syn_error(format!( - "codomain morphism {f_name} has source path differing from the \ - @over path of {x}" - )); + let arg_name = name_seg(*arg_field); + let arg_label = label_seg(*arg_field); + if !r.fields.has(arg_name) { + return elab.syn_error(format!("no such field {arg_name}")); } - ( - TmS::over_app(f_name, f_label, cod_path.clone(), inner_s), - TmV::over_app(f_name, f_label, cod_path.clone(), inner_v), - TyV::over(cod_path), - ) + let arg_ty = elab.evaluator().field_ty(&recv_ty, &recv_v, arg_name); + let arg_s = TmS::proj(recv_s, arg_name, arg_label); + let arg_v = elab.evaluator().proj(&recv_v, arg_name, arg_label); + elab.apply_codomain_morphism(*f, arg_s, arg_v, arg_ty, *arg_field) } App1(L(_, Prim("id")), ob_n) => { let (ob_s, ob_v, ob_t) = elab.syn(ob_n); @@ -992,6 +1126,53 @@ impl<'a> Elaborator<'a> { let mut elab = self.enter(n.loc()); match (&**ty, n.ast0()) { (TyV_::Record(r), Tuple(field_ns)) => { + // Dispatch by clause shape. An instance body has at + // least one clause that doesn't fit the + // `field := value` record-construction shape — either a + // `:`-typed slot, a `mor(arg) := target` mapping entry + // (LHS is an application), a `field := [names]` + // set-literal assignment (RHS is a tuple of bare names), + // or a `field := [key := target, ...]` mapping-literal + // assignment (RHS is a tuple of `:=`-clauses). The + // remaining `field := value` shape — and a tuple of + // `:=`-clauses where each LHS matches an inner field of + // a nested record type — is ordinary record construction. + if field_ns.iter().any(|f| match f.ast0() { + App2(L(_, Keyword(":")), _, _) => true, + App2(L(_, Keyword(":=")), L(_, App1(_, _)), _) => true, + App2(L(_, Keyword(":=")), L(_, Var(_)), L(_, Tuple(elems))) => { + // Set-literal: tuple of bare names. + elems.iter().all(|e| matches!(e.ast0(), Var(_))) + } + _ => false, + }) { + return elab.instance_body(ty, n); + } + // Mapping-literal disambiguation: `field := [k := v, ...]` + // where the LHS field is a *morphism* of the enclosing + // record (sketch). If the field is object- or + // record-typed, treat as nested record construction + // instead — this distinction can only be made by + // consulting the record's field type. + if field_ns.iter().any(|f| { + let App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(elems))) = + f.ast0() + else { + return false; + }; + if !elems + .iter() + .all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) + { + return false; + } + let Some(field_ty_s) = r.fields.get(name_seg(*field_name)) else { + return false; + }; + matches!(&**field_ty_s, TyS_::Morphism(_, _, _)) + }) { + return elab.instance_body(ty, n); + } if r.fields.len() != field_ns.len() { return elab.chk_error(format!( "wrong number of fields provided, expected {}, got {}", @@ -1133,7 +1314,7 @@ mod tests { let result = Model::from_text(&th, "[ : Entit]"); let expected = expect![[r#" - error[elab]: expected fields in the form `name : type`, `field := [names]`, or `mor(arg) := target` + error[elab]: expected fields in the form : --> :1:3 1| [ : Entit] 1| ^^^^^^^ diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 03808a16f..2b5229ded 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -67,18 +67,28 @@ pub struct Def { pub body: TmS, } -/// A toplevel declaration of a diagram. +/// A toplevel declaration of an instance of a model (sketch). +/// +/// The body is a level-shifted introduction value of the model type +/// — generator slots, equation witnesses, and sub-instance imports +/// packaged into a [`TmS_::Instance`](super::stx::TmS_::Instance) +/// term whose evaluated form is [`TmV_::Instance`](super::val::TmV_::Instance). #[derive(Constructor, Clone)] pub struct Diag { - /// The theory that the diagram is defined in. + /// The theory that the instance is defined in. pub theory: Theory, - /// The model G that this diagram is an instance of. + /// The model (sketch) type that this is an instance of. pub model: TyV, - /// The body: a record TyS whose fields have @over-typed entries, - /// presenting both the domain generators and the mapping. - pub body_stx: TyS, - /// Evaluated body — useful for downstream consumers that want the TyV directly. - pub body_val: TyV, + /// Body syntax: a [`TmS_::Instance`](super::stx::TmS_::Instance) term. + pub body_stx: TmS, + /// Body value: a [`TmV_::Instance`](super::val::TmV_::Instance) term. + pub body_val: TmV, + /// Synthesized record type matching the instance's structure — the + /// shape a sub-instance import sees when this instance is named as + /// a type via `lookup_ty`. Each generator becomes an `@over`-typed + /// field; each sub-instance recurses; equations are elided (they're + /// not projectable). + pub body_ty: TyV, } impl TopDecl { /// Unwraps the type for a toplevel-declaration of a type, or panics. diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index f08e152a5..6b9759c21 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -228,6 +228,18 @@ impl TmN { } } +/// Value-level payload of [`TmV_::Instance`]. Parallels +/// [`InstanceBodyS`](super::stx::InstanceBodyS). +#[derive(Default)] +pub struct InstanceBodyV { + /// Generators introduced by this instance, with their fibers. + pub generators: IndexMap)>, + /// Equation witnesses, asserted to hold in this instance. + pub equations: Vec<(TmV, TmV)>, + /// Sub-instance imports, keyed by import name. + pub sub_instances: IndexMap, +} + /// Inner enum for [TmV]. pub enum TmV_ { /// Neutrals. @@ -240,6 +252,10 @@ pub enum TmV_ { /// term. See [`TmS_::OverApp`] for the syntactic counterpart and /// argument-by-argument documentation. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), + /// An instance value of a model (sketch) type. See + /// [`stx::InstanceBodyS`](super::stx::InstanceBodyS) for the + /// payload description; this is its value-level counterpart. + Instance(InstanceBodyV), /// Lists of objects. List(Vec), /// Records. @@ -282,6 +298,11 @@ impl TmV { TmV(Rc::new(TmV_::OverApp(mor, mor_label, tgt_path, inner))) } + /// Smart constructor for [TmV], [TmV_::Instance] case. + pub fn instance(body: InstanceBodyV) -> Self { + TmV(Rc::new(TmV_::Instance(body))) + } + /// Smart constructor for [TmV], [TmV_::List] case. pub fn list(elems: Vec) -> Self { TmV(Rc::new(TmV_::List(elems))) From c9ceafb9f3105725c8c59ca07fed4dc8de8579c3 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 11:47:59 -0700 Subject: [PATCH 16/53] clippy --- packages/catlog/src/tt/batch.rs | 7 +---- packages/catlog/src/tt/eval.rs | 15 +++------- packages/catlog/src/tt/notebook_elab.rs | 4 +-- packages/catlog/src/tt/stx.rs | 22 +++++++------- packages/catlog/src/tt/text_elab.rs | 40 ++++++++++--------------- 5 files changed, 32 insertions(+), 56 deletions(-) diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index b79b1b808..95fa1d829 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -11,12 +11,7 @@ use scopeguard::guard; use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; -use super::{ - modelgen::instance_from_diag, - text_elab::*, - theory::std_theories, - toplevel::*, -}; +use super::{modelgen::instance_from_diag, text_elab::*, theory::std_theories, toplevel::*}; use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; use crate::zero::NameSegment; diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 8efc31503..58e8ea7ec 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -490,9 +490,7 @@ impl<'a> Evaluator<'a> { } (TmV_::OverApp(mor1, _, _, inner1), TmV_::OverApp(mor2, _, _, inner2)) => { if mor1 != mor2 { - return Err(t(format!( - "OverApp morphisms {mor1} and {mor2} are not equal" - ))); + return Err(t(format!("OverApp morphisms {mor1} and {mor2} are not equal"))); } self.equal_tm_helper(inner1, inner2, strict1, strict2) } @@ -503,16 +501,13 @@ impl<'a> Evaluator<'a> { { return Err(t("instance bodies have differing shapes")); } - for ((n1, (_, p1)), (n2, (_, p2))) in - b1.generators.iter().zip(b2.generators.iter()) + for ((n1, (_, p1)), (n2, (_, p2))) in b1.generators.iter().zip(b2.generators.iter()) { if n1 != n2 || p1 != p2 { return Err(t(format!("instance generator {n1} differs from {n2}"))); } } - for ((lhs1, rhs1), (lhs2, rhs2)) in - b1.equations.iter().zip(b2.equations.iter()) - { + for ((lhs1, rhs1), (lhs2, rhs2)) in b1.equations.iter().zip(b2.equations.iter()) { self.equal_tm_helper(lhs1, lhs2, strict1, strict2)?; self.equal_tm_helper(rhs1, rhs2, strict1, strict2)?; } @@ -520,9 +515,7 @@ impl<'a> Evaluator<'a> { b1.sub_instances.iter().zip(b2.sub_instances.iter()) { if n1 != n2 { - return Err(t(format!( - "instance sub-instance {n1} differs from {n2}" - ))); + return Err(t(format!("instance sub-instance {n1} differs from {n2}"))); } self.equal_tm_helper(t1, t2, strict1, strict2)?; } diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index fc1267e79..f84623bb8 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -452,9 +452,7 @@ impl<'a> Elaborator<'a> { nb::ModelJudgment::Equation(eqn_decl) => self.equation_cell(eqn_decl), }; field_ty_vs.push((name, (label, ty_v.clone()))); - self.ctx - .scope - .push(VarInContext::new(name, label, Some(ty_v.clone()))); + self.ctx.scope.push(VarInContext::new(name, label, Some(ty_v.clone()))); self.ctx.env = self.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 0cd660c84..3b8cb8abd 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -213,12 +213,14 @@ fn instance_body_to_doc<'a>(body: &InstanceBodyS) -> D<'a> { let gens = body.generators.iter().map(|(_, (label, path))| { binop(t(":"), t(format!("{label}")), t(format!("@over{}", path_to_string(path)))) }); - let eqns = body.equations.iter().map(|(lhs, rhs)| { - binop(t(":="), lhs.to_doc(), rhs.to_doc()) - }); - let subs = body.sub_instances.iter().map(|(_, (label, inner))| { - binop(t(":"), t(format!("{label}")), inner.to_doc()) - }); + let eqns = body + .equations + .iter() + .map(|(lhs, rhs)| binop(t(":="), lhs.to_doc(), rhs.to_doc())); + let subs = body + .sub_instances + .iter() + .map(|(_, (label, inner))| binop(t(":"), t(format!("{label}")), inner.to_doc())); tuple(gens.chain(subs).chain(eqns)) } @@ -271,7 +273,7 @@ pub enum TmS_ { /// `we.e`, whose type is the fiber over `` where the /// codomain morphism `mor : `). /// - /// Example: in + /// Here is an example: /// ```text /// def Edge : WeightedGraph := [E := [e]] /// def I : WeightedGraph := [ @@ -280,7 +282,7 @@ pub enum TmS_ { /// we.src(e) := v1, /// ] /// ``` - /// the LHS of the mapping clause elaborates to + /// The LHS of the mapping clause elaborates to /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of fiber-type /// over `.V`. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmS), @@ -419,9 +421,7 @@ impl ToDoc for TmS { TmS_::Compose(f, g) => binop(t("·"), f.to_doc(), g.to_doc()), TmS_::ObApp(name, x) => unop(t(format!("@{name}")), x.to_doc()), TmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), - TmS_::OverApp(_, mor_label, _, inner) => { - inner.to_doc() + t(format!(".{mor_label}")) - } + TmS_::OverApp(_, mor_label, _, inner) => inner.to_doc() + t(format!(".{mor_label}")), TmS_::Instance(body) => instance_body_to_doc(body), TmS_::Tt => t("tt"), TmS_::Meta(mv) => t(format!("?{}", mv.id)), diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index e879bc088..eea837280 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -407,9 +407,8 @@ impl<'a> Elaborator<'a> { arg_label_str: &str, ) -> (TmS, TmV, TyV) { let Some(model_ty) = self.current_instance_codomain().cloned() else { - return self.syn_error( - "applied codomain morphism is only allowed inside an instance body", - ); + return self + .syn_error("applied codomain morphism is only allowed inside an instance body"); }; let TyV_::Over(src_path) = &*arg_ty else { let quoted = self.evaluator().quote_ty(&arg_ty); @@ -466,7 +465,10 @@ impl<'a> Elaborator<'a> { let mut elab = self.enter(n.loc()); let Tuple(field_ns) = n.ast0() else { elab.error::<()>("expected a tuple instance body"); - return (TmS::instance(InstanceBodyS::default()), TmV::instance(InstanceBodyV::default())); + return ( + TmS::instance(InstanceBodyS::default()), + TmV::instance(InstanceBodyV::default()), + ); }; let mut gens: IndexMap)> = IndexMap::new(); @@ -568,9 +570,7 @@ impl<'a> Elaborator<'a> { let f_seg = name_seg(*field_name); let f_label = label_seg(*field_name); let Some(mor_ty_s) = cod_r.fields.get(f_seg) else { - elab.error::<()>(format!( - "no such codomain field {field_name}" - )); + elab.error::<()>(format!("no such codomain field {field_name}")); failed = true; continue; }; @@ -582,8 +582,7 @@ impl<'a> Elaborator<'a> { failed = true; continue; }; - let (Some(dom_path), Some(cod_path)) = - (tms_to_path(dom_s), tms_to_path(cod_s)) + let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) else { elab.error::<()>(format!( "codomain morphism {field_name} has non-path dom/cod; \ @@ -615,10 +614,8 @@ impl<'a> Elaborator<'a> { break; } let lhs_ty = TyV::over(cod_path.clone()); - let lhs_s = - TmS::over_app(f_seg, f_label, cod_path.clone(), key_s); - let lhs_v = - TmV::over_app(f_seg, f_label, cod_path.clone(), key_v); + let lhs_s = TmS::over_app(f_seg, f_label, cod_path.clone(), key_s); + let lhs_v = TmV::over_app(f_seg, f_label, cod_path.clone(), key_v); let (rhs_s, rhs_v) = elab.chk(&lhs_ty, target_n); eqns_s.push((lhs_s, rhs_s)); eqns_v.push((lhs_v, rhs_v)); @@ -630,11 +627,7 @@ impl<'a> Elaborator<'a> { } // `field := [n1, n2, ...]` — set-literal assignment to // an object-typed field of the codomain. - App2( - L(_, Keyword(":=")), - L(_, Var(field_name)), - L(_, Tuple(name_ns)), - ) => { + App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(name_ns))) => { let Some(codomain) = elab.current_instance_codomain().cloned() else { elab.error::<()>( "set-literal field assignment is only allowed inside an \ @@ -1003,8 +996,8 @@ impl<'a> Elaborator<'a> { // codomain morphism in the enclosing instance) to the // resolved argument term. App1(L(_, Var(f)), L(_, Var(x))) => { - let (inner_s, inner_v, inner_ty) = elab.lookup_tm(ustr(*x)); - elab.apply_codomain_morphism(*f, inner_s, inner_v, inner_ty, *x) + let (inner_s, inner_v, inner_ty) = elab.lookup_tm(ustr(x)); + elab.apply_codomain_morphism(f, inner_s, inner_v, inner_ty, x) } App1(L(_, Var(f)), L(_, App1(recv_n, L(_, Field(arg_field))))) => { let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); @@ -1019,7 +1012,7 @@ impl<'a> Elaborator<'a> { let arg_ty = elab.evaluator().field_ty(&recv_ty, &recv_v, arg_name); let arg_s = TmS::proj(recv_s, arg_name, arg_label); let arg_v = elab.evaluator().proj(&recv_v, arg_name, arg_label); - elab.apply_codomain_morphism(*f, arg_s, arg_v, arg_ty, *arg_field) + elab.apply_codomain_morphism(f, arg_s, arg_v, arg_ty, arg_field) } App1(L(_, Prim("id")), ob_n) => { let (ob_s, ob_v, ob_t) = elab.syn(ob_n); @@ -1160,10 +1153,7 @@ impl<'a> Elaborator<'a> { else { return false; }; - if !elems - .iter() - .all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) - { + if !elems.iter().all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) { return false; } let Some(field_ty_s) = r.fields.get(name_seg(*field_name)) else { From 57f5d5c0c095841bc98077918b02948496a54914 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 12:01:06 -0700 Subject: [PATCH 17/53] cargo doc --- packages/catlog/src/tt/stx.rs | 5 +++-- packages/catlog/src/tt/toplevel.rs | 8 ++++---- packages/catlog/src/tt/val.rs | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 3b8cb8abd..eb47b0e97 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -267,8 +267,9 @@ pub enum TmS_ { /// (e.g. `src` in `we.src(e)`). /// 2. `mor_label` — display label for that name. /// 3. `tgt_path` — the codomain object-path that `mor` lands at. - /// Stored on the node so [`Evaluator::eval_tm`] can recover the - /// result fiber type without consulting the codomain. + /// Stored on the node so [`super::eval::Evaluator::eval_tm`] + /// can recover the result fiber type without consulting the + /// codomain. /// 4. `inner` — the fiber-typed argument (e.g. the elaboration of /// `we.e`, whose type is the fiber over `` where the /// codomain morphism `mor : `). diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 2b5229ded..42217a1c3 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -71,17 +71,17 @@ pub struct Def { /// /// The body is a level-shifted introduction value of the model type /// — generator slots, equation witnesses, and sub-instance imports -/// packaged into a [`TmS_::Instance`](super::stx::TmS_::Instance) -/// term whose evaluated form is [`TmV_::Instance`](super::val::TmV_::Instance). +/// packaged into a [`super::stx::TmS_::Instance`] +/// term whose evaluated form is [`super::val::TmV_::Instance`]. #[derive(Constructor, Clone)] pub struct Diag { /// The theory that the instance is defined in. pub theory: Theory, /// The model (sketch) type that this is an instance of. pub model: TyV, - /// Body syntax: a [`TmS_::Instance`](super::stx::TmS_::Instance) term. + /// Body syntax: a [`super::stx::TmS_::Instance`] term. pub body_stx: TmS, - /// Body value: a [`TmV_::Instance`](super::val::TmV_::Instance) term. + /// Body value: a [`super::val::TmV_::Instance`] term. pub body_val: TmV, /// Synthesized record type matching the instance's structure — the /// shape a sub-instance import sees when this instance is named as diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 6b9759c21..68363416b 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -229,7 +229,7 @@ impl TmN { } /// Value-level payload of [`TmV_::Instance`]. Parallels -/// [`InstanceBodyS`](super::stx::InstanceBodyS). +/// [`super::stx::InstanceBodyS`]. #[derive(Default)] pub struct InstanceBodyV { /// Generators introduced by this instance, with their fibers. @@ -253,8 +253,8 @@ pub enum TmV_ { /// argument-by-argument documentation. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), /// An instance value of a model (sketch) type. See - /// [`stx::InstanceBodyS`](super::stx::InstanceBodyS) for the - /// payload description; this is its value-level counterpart. + /// [`super::stx::InstanceBodyS`] for the payload description; this + /// is its value-level counterpart. Instance(InstanceBodyV), /// Lists of objects. List(Vec), From b3dfbe959fafbf2fb138ceed0cc5bf22331d6983 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 19:48:24 -0600 Subject: [PATCH 18/53] non-recursive discrete instance terms --- .../catlog/src/dbl/discrete/model_instance.rs | 24 +++++----- packages/catlog/src/dbl/model_instance.rs | 22 ++++----- packages/catlog/src/tt/batch.rs | 13 +++-- packages/catlog/src/tt/modelgen.rs | 47 ++++++++++++++----- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/packages/catlog/src/dbl/discrete/model_instance.rs b/packages/catlog/src/dbl/discrete/model_instance.rs index 2f2c83d17..2aed6f7dd 100644 --- a/packages/catlog/src/dbl/discrete/model_instance.rs +++ b/packages/catlog/src/dbl/discrete/model_instance.rs @@ -6,20 +6,20 @@ use crate::zero::QualifiedName; use super::model::DiscreteDblModel; -/// A term in an instance of a discrete double model. +/// A term in an instance of a discrete double model: a model morphism +/// applied to a single instance generator. /// -/// Discrete morphisms have single-object domains, so applications are -/// 1-ary. The morphism is identified by its name (a [`QualifiedName`]) -/// rather than a path, since instance terms only ever apply a single -/// model morphism at a time — composition is reflected by the -/// composability of [`Apply`](Self::Apply) nodes themselves, not by -/// path values living inside one. +/// Composition of model morphisms is reflected inside [`path`](Self::path) +/// itself, not by nesting term constructors, so every term has the +/// flat canonical shape `path(base)`. When `path` is the identity, the +/// term denotes `base` directly; its `Id` vertex must agree with the +/// fiber of `base` in the surrounding instance. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum DiscreteInstanceTerm { - /// A bare instance generator. - Generator(QualifiedName), - /// A morphism of the model applied to one argument term. - Apply(QualifiedName, Box), +pub struct DiscreteInstanceTerm { + /// Model morphism applied to `base`. + pub path: QualifiedPath, + /// The instance generator at the root of the term. + pub base: QualifiedName, } impl InstanceTerm for DiscreteInstanceTerm { diff --git a/packages/catlog/src/dbl/model_instance.rs b/packages/catlog/src/dbl/model_instance.rs index 1c67c7934..07d58040e 100644 --- a/packages/catlog/src/dbl/model_instance.rs +++ b/packages/catlog/src/dbl/model_instance.rs @@ -1,17 +1,16 @@ //! Instances of models of a double theory. //! -//! An **instance** of a model presents a fibered structure over the model: +//! An **instance** of a model (see [Carlson-Patterson 2025][https://arxiv.org/abs/2510.08861]) +//! is here presented via //! a set of generators living over each object of the model, together with //! equations between terms built from morphisms of the model applied to -//! those generators. Crucially, the equations are stated *abstractly*: the +//! those generators. Crucially, the //! lift targets and lift morphisms forced by the discrete-opfibration -//! condition are *not* materialized as explicit generators. This is what -//! makes the data structure suited to "minimalist" presentations such as -//! the ones produced by the `diagram` form in DoubleTT. +//! condition may be used in equations but are *not* materialized as explicit generators. //! //! The term language is left to each doctrine to define, via the //! [`InstanceTerm`] trait and the [`HasInstanceTerm`] extension on -//! [`DblModel`]. Discrete doctrines typically need only bare generators +//! [`DblModel`]. Discrete doctrines need only bare generators //! and morphism applications; modal doctrines additionally allow list //! terms to feed list-shaped morphism domains. //! @@ -22,18 +21,17 @@ use std::rc::Rc; use super::model::DblModel; -use crate::one::Category; use crate::zero::{Column, HashColumn, MutMapping, QualifiedName}; /// A term in the language of an instance of some model. /// /// Each doctrine implements its own concrete term type. The associated /// [`Mor`](Self::Mor) type ties the term language to a particular +/// /// model's morphism type. pub trait InstanceTerm { - /// Morphism type from the associated model that this term language - /// can apply to its arguments. - type Mor; + /// The type of morphisms from the associated model. + type Mor; } /// A [`DblModel`] that has an associated term language for instances. @@ -43,10 +41,10 @@ pub trait InstanceTerm { pub trait HasInstanceTerm: DblModel { /// The kind of term used to express equations in instances of this /// model. - type Term: InstanceTerm::Mor>; + type Term: InstanceTerm; } -/// An instance of a model: a set of fibered generators plus equations +/// An instance of a model: a fibered set of generators plus equations /// between terms in the model's instance-term language. /// /// Owns the generator-to-fiber assignment and the equations, but does diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 95fa1d829..a8b55239c 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -13,6 +13,7 @@ use tattle::{Reporter, declare_error}; use super::{modelgen::instance_from_diag, text_elab::*, theory::std_theories, toplevel::*}; use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; +use crate::one::path::Path; use crate::zero::NameSegment; declare_error!(TOP_ERROR, "top", "an error at the top-level"); @@ -290,12 +291,14 @@ fn snapshot_examples() { assert!(succeeded); } -/// Render a [`QualifiedPath`] for snapshot output. +/// Render an instance term for snapshot output as `f(g(base))`, with +/// `f` the outermost (last-applied) model morphism in the path. fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { - match tm { - DiscreteInstanceTerm::Generator(name) => format!("{name}"), - DiscreteInstanceTerm::Apply(mor, arg) => { - format!("{}({})", mor, format_instance_term(arg)) + let mut s = format!("{}", tm.base); + if let Path::Seq(edges) = &tm.path { + for mor in edges.iter() { + s = format!("{}({})", mor, s); } } + s } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 22f1c6bc4..5b30cddea 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -452,8 +452,8 @@ fn extract_instance_body( instance.add_generator(qname, fiber); } for (lhs, rhs) in &body.equations { - let lhs_t = tm_to_discrete_instance_term_with_prefix(lhs, prefix)?; - let rhs_t = tm_to_discrete_instance_term_with_prefix(rhs, prefix)?; + let lhs_t = tm_to_discrete_instance_term_with_prefix(instance, lhs, prefix)?; + let rhs_t = tm_to_discrete_instance_term_with_prefix(instance, rhs, prefix)?; instance.add_equation(lhs_t, rhs_t); } for (name, (label, sub_v)) in &body.sub_instances { @@ -473,23 +473,44 @@ fn extract_instance_body( /// Convert an `@over`-typed term value into a [`DiscreteInstanceTerm`], /// prefixing each generator name with the path into any enclosing /// sub-instances. +/// +/// Nested [`TmV_::OverApp`]s are flattened into a single morphism path +/// applied to the generator at the leaf, matching the flat canonical +/// shape of [`DiscreteInstanceTerm`]. fn tm_to_discrete_instance_term_with_prefix( + instance: &DiscreteDblModelInstance, tm: &TmV, prefix: &[NameSegment], ) -> Result { - match &**tm { - TmV_::Neu(n, _) => { - let mut segs = prefix.to_vec(); - segs.extend(neu_full_name(n)); - Ok(DiscreteInstanceTerm::Generator(segs.into())) + // Walk outer-to-inner, recording each applied morphism. + let mut mors_outer_first: Vec = Vec::new(); + let mut cur = tm; + let base: QualifiedName = loop { + match &**cur { + TmV_::Neu(n, _) => { + let mut segs = prefix.to_vec(); + segs.extend(neu_full_name(n)); + break segs.into(); + } + TmV_::OverApp(mor_name, _, _, inner) => { + mors_outer_first.push(QualifiedName::single(*mor_name)); + cur = inner; + } + _ => return Err("term is not a generator or codomain-morphism application".into()), } - TmV_::OverApp(mor_name, _, _, inner) => { - let mor_qname = QualifiedName::single(*mor_name); - let inner_t = tm_to_discrete_instance_term_with_prefix(inner, prefix)?; - Ok(DiscreteInstanceTerm::Apply(mor_qname, Box::new(inner_t))) + }; + // Path order is innermost-first (apply first goes first). + mors_outer_first.reverse(); + let path = match Path::from_vec(mors_outer_first) { + Some(p) => p, + None => { + let fiber = instance.fiber_of(&base).ok_or_else(|| { + format!("instance term mentions unknown generator {base}") + })?; + Path::Id(fiber.clone()) } - _ => Err("term is not a generator or codomain-morphism application".into()), - } + }; + Ok(DiscreteInstanceTerm { path, base }) } /// Read off the full name of a neutral, including its leading variable From 9a61d40e0a1c49ead8b02b2beb8ac1e2d7ec71b4 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 20:00:25 -0600 Subject: [PATCH 19/53] revert special-casing for _ ident --- packages/catlog/Cargo.toml | 2 +- packages/catlog/src/tt/text_elab.rs | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/catlog/Cargo.toml b/packages/catlog/Cargo.toml index 99b5fbf61..dc0f9219c 100644 --- a/packages/catlog/Cargo.toml +++ b/packages/catlog/Cargo.toml @@ -36,7 +36,7 @@ tattle = "0.4.3" thiserror = "1" tsify = { version = "0.5.6", features = ["js"], optional = true } ustr = "1" -uuid = { version = "1.18", features = ["v4"] } +uuid = { version = "1.18" } wasm-bindgen = { version = "0.2.100", optional = true } catcolab-document-types = { version = "0.1.0", path = "../document-types" } sea-query = { version = "0.32.7", optional = true } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index eea837280..e87f701e2 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -486,11 +486,7 @@ impl<'a> Elaborator<'a> { // elaborated type's shape). App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { let name_str = *name; - let n_seg = if name_str == "_" { - NameSegment::Uuid(uuid::Uuid::new_v4()) - } else { - name_seg(name_str) - }; + let n_seg = name_seg(name_str); let label = label_seg(name_str); let (_, ty_v) = elab.ty(ty_n); match &*ty_v { @@ -857,14 +853,7 @@ impl<'a> Elaborator<'a> { elab.loc = Some(field_n.loc()); let Some((name, label, ty_n)) = (match field_n.ast0() { App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { - // `_` is a fresh anonymous binder — give it a - // UUID name so distinct `_` fields don't collide - // in the record row. - let name_seg = if *name == "_" { - NameSegment::Uuid(uuid::Uuid::new_v4()) - } else { - name_seg(*name) - }; + let name_seg = name_seg(*name); Some((name_seg, label_seg(*name), ty_n)) } _ => elab.error("expected fields in the form : "), From db2e8f1a7cabb5aaf9e2caae2766f8e7b3732987 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 12 Jun 2026 20:05:09 -0600 Subject: [PATCH 20/53] fix todo --- packages/catlog/src/tt/eval.rs | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 58e8ea7ec..553a3525d 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -529,8 +529,6 @@ impl<'a> Evaluator<'a> { } } - // TODO: refactor to use [`Evaluator::path_ty`] for the descent and - // keep only the subtype check here. fn can_specialize( &self, ty: &TyV, @@ -539,28 +537,15 @@ impl<'a> Evaluator<'a> { field_ty: TyV, ) -> Result<(), String> { assert!(!path.is_empty()); - - let TyV_::Record(r) = &**ty else { - return Err("cannot specialize a non-record type".into()); - }; - - let (field, path) = (path[0], &path[1..]); - if !r.fields.has(field.0) { - return Err(format!("no such field .{}", field.1)); - } - let orig_field_ty = self.field_ty(ty, val, field.0); - if path.is_empty() { - self.subtype(&field_ty, &orig_field_ty).map_err(|msg| { - format!( - "{} is not a subtype of {}:\n... because {}", - self.quote_ty(&field_ty), - self.quote_ty(&orig_field_ty), - msg.pretty() - ) - }) - } else { - self.can_specialize(&orig_field_ty, &self.proj(val, field.0, field.1), path, field_ty) - } + let orig_field_ty = self.path_ty(ty, val, path)?; + self.subtype(&field_ty, &orig_field_ty).map_err(|msg| { + format!( + "{} is not a subtype of {}:\n... because {}", + self.quote_ty(&field_ty), + self.quote_ty(&orig_field_ty), + msg.pretty() + ) + }) } /// Walk `path` from the value `val` of record type `ty`, returning From f392286edc88b9aa3128ff2e97cb4f43e5bdda8e Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 15 Jun 2026 09:04:20 -0500 Subject: [PATCH 21/53] clippy --- packages/catlog/src/dbl/model_instance.rs | 5 ++--- packages/catlog/src/tt/modelgen.rs | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/catlog/src/dbl/model_instance.rs b/packages/catlog/src/dbl/model_instance.rs index 07d58040e..41642e700 100644 --- a/packages/catlog/src/dbl/model_instance.rs +++ b/packages/catlog/src/dbl/model_instance.rs @@ -1,6 +1,6 @@ //! Instances of models of a double theory. //! -//! An **instance** of a model (see [Carlson-Patterson 2025][https://arxiv.org/abs/2510.08861]) +//! An **instance** of a model (see [Carlson-Patterson 2025][https://arxiv.org/abs/2510.08861]) //! is here presented via //! a set of generators living over each object of the model, together with //! equations between terms built from morphisms of the model applied to @@ -27,11 +27,10 @@ use crate::zero::{Column, HashColumn, MutMapping, QualifiedName}; /// /// Each doctrine implements its own concrete term type. The associated /// [`Mor`](Self::Mor) type ties the term language to a particular -/// /// model's morphism type. pub trait InstanceTerm { /// The type of morphisms from the associated model. - type Mor; + type Mor; } /// A [`DblModel`] that has an associated term language for instances. diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 5b30cddea..318e2162c 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -504,9 +504,9 @@ fn tm_to_discrete_instance_term_with_prefix( let path = match Path::from_vec(mors_outer_first) { Some(p) => p, None => { - let fiber = instance.fiber_of(&base).ok_or_else(|| { - format!("instance term mentions unknown generator {base}") - })?; + let fiber = instance + .fiber_of(&base) + .ok_or_else(|| format!("instance term mentions unknown generator {base}"))?; Path::Id(fiber.clone()) } }; From ffbd293622129b986c2348c1c36b20292f8a3431 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 15 Jun 2026 09:54:26 -0500 Subject: [PATCH 22/53] delete spurious diagram toplevel --- packages/catlog/src/tt/batch.rs | 15 ++++--- packages/catlog/src/tt/eval.rs | 9 +++- packages/catlog/src/tt/modelgen.rs | 20 ++++----- packages/catlog/src/tt/text_elab.rs | 66 +++++++++++------------------ packages/catlog/src/tt/toplevel.rs | 42 ++++-------------- 5 files changed, 60 insertions(+), 92 deletions(-) diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index a8b55239c..9f9590fef 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -11,7 +11,9 @@ use scopeguard::guard; use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; -use super::{modelgen::instance_from_diag, text_elab::*, theory::std_theories, toplevel::*}; +use super::{ + modelgen::instance_from_def, text_elab::*, theory::std_theories, toplevel::*, val::TmV_, +}; use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; use crate::one::path::Path; use crate::zero::NameSegment; @@ -217,14 +219,17 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result { - let is_diag = matches!(top_decl, TopDecl::Diag(_)); + let is_instance = matches!( + &top_decl, + TopDecl::DefConst(d) if matches!(&*d.val, TmV_::Instance(_)) + ); toplevel.declarations.insert(name_segment, top_decl); output.declared(name_segment); - if is_diag - && let Some(TopDecl::Diag(diag)) = + if is_instance + && let Some(TopDecl::DefConst(def)) = toplevel.declarations.get(&name_segment) { - match instance_from_diag(&toplevel, &diag.theory.definition, diag) { + match instance_from_def(&toplevel, &def.theory.definition, def) { Ok((instance, _)) => output.instance_summary(&instance), Err(msg) => output.instance_error(&msg), } diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 553a3525d..0fb6ad3de 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -51,8 +51,13 @@ impl<'a> Evaluator<'a> { match &**ty { TyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), - TopDecl::Diag(d) => d.body_ty.clone(), - _ => panic!("top-level {tv} should be a type or diagram declaration"), + // An instance term used in type position evaluates to the + // record type synthesized from its body (see `lookup_ty`). + TopDecl::DefConst(d) => match &*d.val { + TmV_::Instance(body) => self.synth_instance_body_ty(body), + _ => panic!("top-level {tv} should be a type or instance declaration"), + }, + _ => panic!("top-level {tv} should be a type or instance declaration"), }, TyS_::Object(ot) => TyV::object(ot.clone()), TyS_::Morphism(pt, dom, cod) => { diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 318e2162c..868343f73 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -406,7 +406,8 @@ impl<'a> ModelGenerator<'a> { } } -/// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Diag`]. +/// Generates a [`DiscreteDblModelInstance`] from an elaborated instance +/// term — a [`DefConst`] whose value is a [`TmV_::Instance`]. /// /// Walks the instance body's [`TmV_::Instance`] payload, registering /// each generator with its fiber, each equation as a pair of @@ -414,20 +415,20 @@ impl<'a> ModelGenerator<'a> { /// the appropriate prefix. /// /// Restricted to discrete double theories for the moment. -pub fn instance_from_diag( +pub fn instance_from_def( toplevel: &Toplevel, th: &TheoryDef, - diag: &Diag, + def: &DefConst, ) -> Result<(DiscreteDblModelInstance, Namespace), String> { let TheoryDef::Discrete(_) = th else { return Err("instance generation only supports discrete double theories".into()); }; - let (cod_model, _) = Model::from_ty(toplevel, th, &diag.model); + let (cod_model, _) = Model::from_ty(toplevel, th, &def.ty); let cod_model = cod_model .as_discrete() .ok_or_else(|| "expected a discrete codomain model".to_string())?; let mut instance = DiscreteDblModelInstance::new(Rc::new(cod_model)); - let TmV_::Instance(body) = &*diag.body_val else { + let TmV_::Instance(body) = &*def.val else { return Err("expected a TmV::Instance body".into()); }; let mut namespace = Namespace::new_for_uuid(); @@ -583,12 +584,11 @@ def I : WeightedGraph := [ ] "#; let toplevel = elaborate_to_toplevel(src); - let diag = match toplevel.declarations.get(&name_seg("I")) { - Some(TopDecl::Diag(d)) => d.clone(), - _ => panic!("expected I to be a diagram declaration"), + let def = match toplevel.declarations.get(&name_seg("I")) { + Some(TopDecl::DefConst(d)) if matches!(&*d.val, TmV_::Instance(_)) => d.clone(), + _ => panic!("expected I to be an instance declaration"), }; - let (instance, _ns) = - instance_from_diag(&toplevel, &diag.theory.definition, &diag).unwrap(); + let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); let e_qname: QualifiedName = vec![name_seg("e")].into(); let e_fiber: QualifiedName = vec![name_seg("E")].into(); diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index e87f701e2..710ae7d45 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -162,35 +162,15 @@ impl TopElaborator { let mut elab = self.elaborator(&theory, toplevel); let (_, ret_ty_v) = elab.ty(ty_n); let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); - // chk dispatches: if the body was instance-shaped, - // the returned TmV is a [`TmV_::Instance`], and we - // wrap as an instance declaration with a - // synthesized record type for sub-instance lookups. - // Otherwise the body is an ordinary const term. - match &*tm_v { - TmV_::Instance(body) => { - let body_ty = elab.evaluator().synth_instance_body_ty(body); - Some(TopElabResult::Declaration( - name, - TopDecl::Diag(Diag::new( - theory.clone(), - ret_ty_v, - tm_s, - tm_v, - body_ty, - )), - )) - } - _ => Some(TopElabResult::Declaration( - name, - TopDecl::DefConst(DefConst::new( - theory.clone(), - tm_s, - tm_v, - ret_ty_v, - )), - )), - } + // A term in the empty context. The body may be an + // ordinary constant or a [`TmV_::Instance`] (when it + // was instance-shaped); both are just terms, so the + // declaration is the same `DefConst` either way. + // Instance-ness is later read off the term value. + Some(TopElabResult::Declaration( + name, + TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), + )) } } } @@ -499,8 +479,10 @@ impl<'a> Elaborator<'a> { Var(sub_name) => { let topvar = name_seg(*sub_name); match elab.toplevel.declarations.get(&topvar) { - Some(TopDecl::Diag(d)) => { - (d.body_stx.clone(), d.body_val.clone()) + Some(TopDecl::DefConst(d)) + if matches!(&*d.val, TmV_::Instance(_)) => + { + (d.stx.clone(), d.val.clone()) } _ => { elab.error::<()>(format!( @@ -740,16 +722,19 @@ impl<'a> Elaborator<'a> { )) } } - TopDecl::Diag(d) => { + // An instance term used in type position yields the record + // type synthesized from its body, allowing sub-instance + // imports (`we : Edge`) to project into Edge's fields. + TopDecl::DefConst(d) if matches!(&*d.val, TmV_::Instance(_)) => { if d.theory == self.theory { - // Using an instance name as a type yields the - // instance's synthesized record type, allowing - // sub-instance imports (`we : Edge`) to project - // into Edge's fields. - (TyS::topvar(name), d.body_ty.clone()) + let TmV_::Instance(body) = &*d.val else { + unreachable!("guarded by the match arm above") + }; + let body_ty = self.evaluator().synth_instance_body_ty(body); + (TyS::topvar(name), body_ty) } else { self.ty_error(format!( - "{name} refers to a diagram in theory {}, expected theory {}", + "{name} refers to an instance in theory {}, expected theory {}", d.theory, self.theory )) } @@ -942,11 +927,10 @@ impl<'a> Elaborator<'a> { } else if let Some(d) = self.toplevel.lookup(name) { match d { TopDecl::Type(_) => self.syn_error(format!("{name} refers type, not term")), + // An instance is just a term (its `val` is a `TmV_::Instance`), + // so it resolves here like any other constant. TopDecl::DefConst(d) => (TmS::topvar(name), d.val.clone(), d.ty.clone()), TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")), - TopDecl::Diag(_) => { - self.syn_error(format!("{name} refers to a diagram, not a term")) - } } } else { self.syn_error(format!("no such variable {name}")) diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 42217a1c3..ee6f97a6b 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -1,7 +1,7 @@ //! Data structures for managing toplevel declarations in the type theory. //! //! Specifically, notebooks will produce [TopDecl::Type] declarations, or -//! maybe [TopDecl::Diag] declartions. +//! [TopDecl::DefConst] declarations. use derive_more::Constructor; @@ -17,8 +17,6 @@ pub enum TopDecl { DefConst(DefConst), /// See [Def]. Def(Def), - /// See [Diag]. - Diag(Diag), } /// A toplevel declaration of a type. @@ -40,6 +38,13 @@ pub struct Type { /// Also stores the evaluation of that term, and the evaluation of the /// corresponding type of that term. Because this is an evaluation in the empty /// context, this is OK to use in any other context as well. +/// +/// An *instance* of a model is just such a term whose [`val`](Self::val) is a +/// [`TmV_::Instance`](super::val::TmV_::Instance): a generator/equation/ +/// sub-instance body packaged as an introduction value of a record type. +/// When an instance name is used in *type* position (for a +/// sub-instance import), its type is the record type synthesized from that body +/// by [`synth_instance_body_ty`](super::eval::Evaluator::synth_instance_body_ty). #[derive(Constructor, Clone)] pub struct DefConst { /// The theory that the constant is defined in. @@ -67,29 +72,6 @@ pub struct Def { pub body: TmS, } -/// A toplevel declaration of an instance of a model (sketch). -/// -/// The body is a level-shifted introduction value of the model type -/// — generator slots, equation witnesses, and sub-instance imports -/// packaged into a [`super::stx::TmS_::Instance`] -/// term whose evaluated form is [`super::val::TmV_::Instance`]. -#[derive(Constructor, Clone)] -pub struct Diag { - /// The theory that the instance is defined in. - pub theory: Theory, - /// The model (sketch) type that this is an instance of. - pub model: TyV, - /// Body syntax: a [`super::stx::TmS_::Instance`] term. - pub body_stx: TmS, - /// Body value: a [`super::val::TmV_::Instance`] term. - pub body_val: TmV, - /// Synthesized record type matching the instance's structure — the - /// shape a sub-instance import sees when this instance is named as - /// a type via `lookup_ty`. Each generator becomes an `@over`-typed - /// field; each sub-instance recurses; equations are elided (they're - /// not projectable). - pub body_ty: TyV, -} impl TopDecl { /// Unwraps the type for a toplevel-declaration of a type, or panics. /// @@ -120,14 +102,6 @@ impl TopDecl { _ => panic!("top-level should be a term judgment"), } } - - /// Unwraps the diagram for a toplevel diagram declaration, or panics. - pub fn unwrap_diag(self) -> Diag { - match self { - TopDecl::Diag(d) => d, - _ => panic!("top-level should be a diagram declaration"), - } - } } /// Storage for toplevel declarations. From cbd2175d21ed7b4c3c9d3c8a2560c98d6dca784c Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 15 Jun 2026 15:09:28 -0700 Subject: [PATCH 23/53] make instance body type more honest --- packages/catlog/src/tt/eval.rs | 99 +++++++++++++++++++++++++++++- packages/catlog/src/tt/modelgen.rs | 71 +++++++++++++++++++++ 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 0fb6ad3de..b86b5df57 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -169,9 +169,27 @@ impl<'a> Evaluator<'a> { self.bind_neu("self".into(), "self".into(), ty) } - /// Synthesize the record type matching an instance body's - /// structure. Generators become `@over`-typed fields; sub-instances - /// recurse. Equations are elided (they aren't projectable). + /// Synthesize the type of an instance body when its name is used in + /// *type* position — i.e. the representable `Hom_Inst(D, self)`, the + /// type of instance morphisms out of the instance `D`. + /// + /// By Yoneda such a morphism is determined by where `D`'s generators + /// land, so each generator becomes an `@over`-typed field and each + /// sub-instance recurses. A *fiber* equation (`mor(gen) := target`) + /// is exactly a condition cutting this down from the free product of + /// `@over` fields to the presented representable — the equalizer — so + /// each becomes an `Id`-typed field witnessing that the constraint + /// holds for the morphism's images. Morphism equations (`==`) + /// constrain the model rather than the generator images, so they are + /// correctly *not* equalizer conditions and are omitted. + /// + /// WARNING: these `Id` fields make the type *honest* but are not + /// *enforced*. An import (`we : D`) introduces a neutral of this type + /// rather than constructing one, so nothing checks the equalizer + /// conditions, and equation extraction reads the inlined instance body + /// (not this type). An import that contradicts `D`'s fiber equations is + /// therefore not rejected here — consistency of the resulting equation + /// set is a model-layer concern, not checked during elaboration. pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { let mut fields: Row = Row::empty(); for (name, (label, path)) in &body.generators { @@ -185,6 +203,24 @@ impl<'a> Evaluator<'a> { let sub_ty_s = self.quote_ty(&sub_ty_v); fields.insert(*name, *label, sub_ty_s); } + // The equation terms reference this body's generators as local + // binders; re-root them at `self` (the record being built) so they + // read as field projections, exactly as sibling references do in an + // ordinary record type. The self var's type is irrelevant — quoting + // a neutral ignores it — so a placeholder suffices. Quoting in `ev` + // (one binder deeper) sends `self` to de Bruijn index 0, matching + // how `field_ty` snocs the record value when projecting. + let (self_n, ev) = self.bind_self(TyV::unit()); + for (i, (lhs_v, rhs_v)) in body.equations.iter().enumerate() { + let Some(over_ty) = fiber_equation_ty(lhs_v) else { + continue; + }; + let lhs_s = ev.quote_tm(&reroot_at_self(lhs_v, &self_n, body)); + let rhs_s = ev.quote_tm(&reroot_at_self(rhs_v, &self_n, body)); + let eq_ty = TyS::id(ev.quote_ty(&over_ty), lhs_s, rhs_s); + let key = format!("_eq{i}"); + fields.insert(name_seg(key.as_str()), label_seg(key.as_str()), eq_ty); + } let r_v = RecordV::new(self.env.clone(), fields, Dtry::empty()); TyV::record(r_v) } @@ -599,3 +635,60 @@ impl<'a> Evaluator<'a> { Ok(TyV::record(r.add_specialization(path, field_ty))) } } + +/// The `@over` type of a *fiber* equation's side, if it is one. +/// +/// Fiber equations (`mor(gen) := target`) relate `@over`-typed terms; their +/// common type is recoverable from the left-hand side. Returns `None` for +/// morphism (`==`) equations, which are not equalizer conditions on an +/// instance's generator images (see [`Evaluator::synth_instance_body_ty`]). +fn fiber_equation_ty(tm: &TmV) -> Option { + match &**tm { + TmV_::OverApp(_, _, tgt_path, _) => Some(TyV::over(tgt_path.clone())), + TmV_::Neu(_, ty) => matches!(&**ty, TyV_::Over(_)).then(|| ty.clone()), + _ => None, + } +} + +/// Re-root an instance-body term so its generator/sub-instance references +/// become projections of `self_n`. +/// +/// In an instance body a generator (e.g. `v1`) or sub-instance (e.g. `we`) +/// is a local binder, so it appears as the leading [`TmN_::Var`] of a +/// neutral. To reuse such a term as a field type of the representable +/// record `Hom_Inst(D, self)`, that leading variable must instead read as +/// `self.v1` / `self.we` — a projection of the record. Names not bound by +/// this body are left untouched. +fn reroot_at_self(tm: &TmV, self_n: &TmN, body: &InstanceBodyV) -> TmV { + match &**tm { + TmV_::Neu(n, ty) => TmV::neu(reroot_neu_at_self(n, self_n, body), ty.clone()), + TmV_::OverApp(mor, mor_label, tgt_path, inner) => { + TmV::over_app(*mor, *mor_label, tgt_path.clone(), reroot_at_self(inner, self_n, body)) + } + TmV_::App(name, x) => TmV::app(*name, reroot_at_self(x, self_n, body)), + TmV_::Compose(f, g) => { + TmV::compose(reroot_at_self(f, self_n, body), reroot_at_self(g, self_n, body)) + } + TmV_::Id(x) => TmV::id(reroot_at_self(x, self_n, body)), + TmV_::Tab(x) => TmV::tab(reroot_at_self(x, self_n, body)), + TmV_::List(elems) => { + TmV::list(elems.iter().map(|e| reroot_at_self(e, self_n, body)).collect()) + } + _ => tm.clone(), + } +} + +fn reroot_neu_at_self(n: &TmN, self_n: &TmN, body: &InstanceBodyV) -> TmN { + match &**n { + TmN_::Var(_, name, label) => { + if body.generators.contains_key(name) || body.sub_instances.contains_key(name) { + TmN::proj(self_n.clone(), *name, *label) + } else { + n.clone() + } + } + TmN_::Proj(inner, field, label) => { + TmN::proj(reroot_neu_at_self(inner, self_n, body), *field, *label) + } + } +} diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 868343f73..3a31a8143 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -595,4 +595,75 @@ def I : WeightedGraph := [ assert_eq!(instance.fiber_of(&e_qname), Some(&e_fiber)); assert_eq!(instance.equations().count(), 1); } + + /// An instance carrying fiber equations, imported as a sub-instance. + /// + /// Exercises the presented-representable synthesis: `Loop`'s type (as + /// used by the import `l : Loop`) must carry an `Id`-typed field per + /// fiber equation, and that type must quote/evaluate without panicking. + /// Extraction of the importer must still recover both copies of the + /// loop's structure. + #[test] + fn import_of_instance_with_fiber_equations() { + let src = r#" +set_theory ThSchema + +type WeightedGraph := [ + V : Entity, + E : Entity, + Weight : AttrType, + src : (Hom Entity)[E, V], + tgt : (Hom Entity)[E, V], + weight : Attr[E, Weight] +] + +def Loop : WeightedGraph := [ + V := [v], + E := [e], + src(e) := v, + tgt(e) := v +] + +def UseLoop : WeightedGraph := [ + l : Loop +] +"#; + let toplevel = elaborate_to_toplevel(src); + + // `Loop`'s representable type must expose its two fiber equations as + // `Id`-typed fields alongside the `e`/`v` generators, and quoting it + // (which re-evaluates every field type against a bound `self`) must + // not panic. + let loop_def = match toplevel.declarations.get(&name_seg("Loop")) { + Some(TopDecl::DefConst(d)) => d.clone(), + _ => panic!("expected Loop to be an instance declaration"), + }; + let TmV_::Instance(loop_body) = &*loop_def.val else { + panic!("Loop should evaluate to an instance"); + }; + let eval = Evaluator::empty(&toplevel); + let body_ty = eval.synth_instance_body_ty(loop_body); + let TyV_::Record(r) = &*body_ty else { + panic!("synthesized instance type should be a record"); + }; + assert!(r.fields.get(name_seg("e")).is_some(), "generator field e"); + assert!(r.fields.get(name_seg("v")).is_some(), "generator field v"); + assert!(r.fields.get(name_seg("_eq0")).is_some(), "first fiber equation"); + assert!(r.fields.get(name_seg("_eq1")).is_some(), "second fiber equation"); + // Round-trips through eval+quote of the dependent `Id` fields. + let _ = eval.quote_ty(&body_ty); + + // The importer still extracts both generators and the imported + // copy's two equations. + let use_def = match toplevel.declarations.get(&name_seg("UseLoop")) { + Some(TopDecl::DefConst(d)) => d.clone(), + _ => panic!("expected UseLoop to be an instance declaration"), + }; + let (instance, _ns) = + instance_from_def(&toplevel, &use_def.theory.definition, &use_def).unwrap(); + let le_qname: QualifiedName = vec![name_seg("l"), name_seg("e")].into(); + let e_fiber: QualifiedName = vec![name_seg("E")].into(); + assert_eq!(instance.fiber_of(&le_qname), Some(&e_fiber)); + assert_eq!(instance.equations().count(), 2); + } } From 69a048f0afe78e26411de5d43a9562efe959c91b Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 16 Jun 2026 15:32:51 -0700 Subject: [PATCH 24/53] eliminate stale mentions of @over --- packages/catlog/src/tt/eval.rs | 8 ++++---- packages/catlog/src/tt/modelgen.rs | 2 +- packages/catlog/src/tt/stx.rs | 24 +++++++++++++++------- packages/catlog/src/tt/text_elab.rs | 31 +++++++++++++++++++---------- packages/catlog/src/tt/val.rs | 6 +++--- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index b86b5df57..a4105ffe1 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -174,10 +174,10 @@ impl<'a> Evaluator<'a> { /// type of instance morphisms out of the instance `D`. /// /// By Yoneda such a morphism is determined by where `D`'s generators - /// land, so each generator becomes an `@over`-typed field and each + /// land, so each generator becomes an `Over`-typed field and each /// sub-instance recurses. A *fiber* equation (`mor(gen) := target`) /// is exactly a condition cutting this down from the free product of - /// `@over` fields to the presented representable — the equalizer — so + /// `Over` fields to the presented representable — the equalizer — so /// each becomes an `Id`-typed field witnessing that the constraint /// holds for the morphism's images. Morphism equations (`==`) /// constrain the model rather than the generator images, so they are @@ -636,9 +636,9 @@ impl<'a> Evaluator<'a> { } } -/// The `@over` type of a *fiber* equation's side, if it is one. +/// The `Over` type of a *fiber* equation's side, if it is one. /// -/// Fiber equations (`mor(gen) := target`) relate `@over`-typed terms; their +/// Fiber equations (`mor(gen) := target`) relate `Over`-typed terms; their /// common type is recoverable from the left-hand side. Returns `None` for /// morphism (`==`) equations, which are not equalizer conditions on an /// instance's generator images (see [`Evaluator::synth_instance_body_ty`]). diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 3a31a8143..5085d1234 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -471,7 +471,7 @@ fn extract_instance_body( Ok(()) } -/// Convert an `@over`-typed term value into a [`DiscreteInstanceTerm`], +/// Convert an `Over`-typed term value into a [`DiscreteInstanceTerm`], /// prefixing each generator name with the path into any enclosing /// sub-instances. /// diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index eb47b0e97..76efe6ee8 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -196,7 +196,7 @@ impl ToDoc for TyS { ), TyS_::Unit => t("Unit"), TyS_::Meta(mv) => t(format!("?{}", mv.id)), - TyS_::Over(path) => t(format!("@over{}", path_to_string(path))), + TyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), } } } @@ -209,9 +209,19 @@ fn path_to_string(path: &[(FieldName, LabelSegment)]) -> String { out } +/// Render an object path as a dotted label string with no leading dot +/// (e.g. `V`, or `we.E` for a nested path), for use inside `Over(...)`. +fn object_path_to_string(path: &[(FieldName, LabelSegment)]) -> String { + path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") +} + fn instance_body_to_doc<'a>(body: &InstanceBodyS) -> D<'a> { let gens = body.generators.iter().map(|(_, (label, path))| { - binop(t(":"), t(format!("{label}")), t(format!("@over{}", path_to_string(path)))) + binop( + t(":"), + t(format!("{label}")), + t(format!("Over({})", object_path_to_string(path))), + ) }); let eqns = body .equations @@ -303,11 +313,11 @@ pub enum TmS_ { /// /// - `generators` maps each generator's local name (within this /// instance) to a path into the model identifying which object -/// generator's fiber it lives over. The path mirrors what a -/// surface `name : @over ` clause used to declare. -/// - `equations` is a list of `(lhs, rhs)` pairs over either `@over` -/// or morphism types. Order is preserved but identity isn't -/// semantically significant. +/// generator's fiber it lives over. Generators are introduced by +/// surface set-literal clauses `field := [...]` in the instance body. +/// - `equations` is a list of `(lhs, rhs)` pairs over either fiber +/// ([`TyS_::Over`]) or morphism types. Order is preserved but identity +/// isn't semantically significant. /// - `sub_instances` maps each sub-instance import's local name to a /// nested instance term. This is what surface `we : Edge` lowers to. #[derive(Default)] diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 710ae7d45..f0004d6f0 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -163,9 +163,7 @@ impl TopElaborator { let (_, ret_ty_v) = elab.ty(ty_n); let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); // A term in the empty context. The body may be an - // ordinary constant or a [`TmV_::Instance`] (when it - // was instance-shaped); both are just terms, so the - // declaration is the same `DefConst` either way. + // ordinary constant or a [`TmV_::Instance`]. // Instance-ness is later read off the term value. Some(TopElabResult::Declaration( name, @@ -250,8 +248,8 @@ pub struct Elaborator<'a> { loc: Option, ctx: Context, next_meta: usize, - /// Stack of model types currently being instantiated, used by - /// `@over .X` and the applied-codomain-morphism arms to recover + /// Stack of model types currently being instantiated, used by the + /// generator (fiber) and applied-codomain-morphism arms to recover /// the codomain. The top of the stack is the innermost enclosing /// instance body. instance_codomains: Vec, @@ -432,7 +430,7 @@ impl<'a> Elaborator<'a> { /// generator slots, equation witnesses, and sub-instance imports. /// /// The codomain is pushed onto the instance-codomain stack so that - /// `@over .X` annotations and applied-codomain-morphism syntax + /// generator (fiber) clauses and applied-codomain-morphism syntax /// inside the body resolve correctly. fn instance_body(&mut self, model_ty: &TyV, n: &FNtn) -> (TmS, TmV) { self.push_instance_codomain(model_ty.clone()); @@ -517,7 +515,7 @@ impl<'a> Elaborator<'a> { let quoted = elab.evaluator().quote_ty(&ty_v); elab.error::<()>(format!( "instance clause {name_str} has type {quoted}, expected \ - @over, a sub-sketch, or an equation", + an element over an object generator, a sub-sketch, or an equation", )); failed = true; } @@ -579,14 +577,17 @@ impl<'a> Elaborator<'a> { let TyV_::Over(key_path) = &*key_ty else { let quoted = elab.evaluator().quote_ty(&key_ty); elab.error::<()>(format!( - "mapping-literal key has type {quoted}, expected an @over type", + "mapping-literal key has type {quoted}, expected an element over {}", + object_path_str(&dom_path), )); entry_failed = true; break; }; if key_path != &dom_path { elab.error::<()>(format!( - "mapping-literal key's @over path does not match the source of {field_name}", + "mapping-literal key is an element over {}, but {field_name} expects an element over {}", + object_path_str(key_path), + object_path_str(&dom_path), )); entry_failed = true; break; @@ -657,7 +658,7 @@ impl<'a> Elaborator<'a> { elab.loc = Some(lhs_n.loc()); elab.error::<()>( "mapping-entry clause `mor(arg) := target` requires the LHS \ - to have a morphism or @over type", + to be a morphism or an element over an object", ); failed = true; continue; @@ -896,7 +897,9 @@ impl<'a> Elaborator<'a> { if !matches!(&*tm1_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { elab.loc = Some(tm1_n.loc()); return elab - .ty_error("Equality types are only supported for morphisms and @over"); + .ty_error( + "Equality types are only supported for morphisms and elements over an object", + ); } if let Err(e) = elab.evaluator().convertible_ty(&tm1_ty, &tm2_ty) { let eval = elab.evaluator(); @@ -1218,6 +1221,12 @@ impl<'a> Elaborator<'a> { /// returns `[]` and `Proj(Var(self), E, E_label)` returns `[(E, E_label)]`, /// matching the path representation produced by the surface `.E` syntax. /// Returns `None` if the term has any other shape. +/// Render an object path (e.g. `[(V, V)]`) as a dotted label string +/// (e.g. `V`, or `we.E` for a nested path) for use in error messages. +fn object_path_str(path: &[(FieldName, LabelSegment)]) -> String { + path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") +} + fn tms_to_path(tm: &TmS) -> Option> { match &**tm { TmS_::Var(_, _, _) => Some(vec![]), diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 68363416b..0dc4f672f 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -99,10 +99,10 @@ pub enum TyV_ { /// A metavariable, also see [TyS_::Meta]. Meta(MetaVar), /// The type of terms in a fiber over an object generator of some - /// diagram's codomain model. + /// instance's codomain model. /// /// See [TyS_::Over] for the syntactic counterpart and explanation - /// of why diagram identity is not part of this type. + /// of why instance identity is not part of this type. Over(Vec<(FieldName, LabelSegment)>), } @@ -248,7 +248,7 @@ pub enum TmV_ { Neu(TmN, TyV), /// Application of an object operation in the theory. App(VarName, TmV), - /// Application of a codomain morphism to an [`@over`-typed](TyV_::Over) + /// Application of a codomain morphism to an [`Over`-typed](TyV_::Over) /// term. See [`TmS_::OverApp`] for the syntactic counterpart and /// argument-by-argument documentation. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), From a8d2bde47008d74620d00ffe6769d43578ef88f6 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 16 Jun 2026 15:56:48 -0700 Subject: [PATCH 25/53] Get the codomains into context --- packages/catlog/src/tt/context.rs | 14 ++++++- packages/catlog/src/tt/text_elab.rs | 64 +++++++++-------------------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 0e313d478..2d31d7510 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -27,12 +27,18 @@ pub struct Context { pub env: Env, /// Stores the names and types of each of the variables in context. pub scope: Vec, + /// The codomain model of the instance body currently being + /// elaborated, if any. Its fields are the model's generators, + /// looked up by name — a separate namespace from `scope`, but + /// scoped state restored alongside it. + pub codomain: Option>, } /// A checkpoint that we can return the context to. pub struct ContextCheckpoint { env: Env, scope: usize, + codomain: Option>, } impl Default for Context { @@ -44,7 +50,11 @@ impl Default for Context { impl Context { /// Create an empty context. pub fn new() -> Self { - Self { env: Env::Nil, scope: Vec::new() } + Self { + env: Env::Nil, + scope: Vec::new(), + codomain: None, + } } /// Create a checkpoint from the current state of the context. @@ -52,6 +62,7 @@ impl Context { ContextCheckpoint { env: self.env.clone(), scope: self.scope.len(), + codomain: self.codomain.clone(), } } @@ -59,6 +70,7 @@ impl Context { pub fn reset_to(&mut self, c: ContextCheckpoint) { self.env = c.env; self.scope.truncate(c.scope); + self.codomain = c.codomain; } /// Add a new variable to scope (note: does not add it to the environment). diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index f0004d6f0..aaa6af0b1 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -248,11 +248,6 @@ pub struct Elaborator<'a> { loc: Option, ctx: Context, next_meta: usize, - /// Stack of model types currently being instantiated, used by the - /// generator (fiber) and applied-codomain-morphism arms to recover - /// the codomain. The top of the stack is the innermost enclosing - /// instance body. - instance_codomains: Vec, } struct ElaboratorCheckpoint { @@ -272,20 +267,14 @@ impl<'a> Elaborator<'a> { loc: None, ctx: Context::new(), next_meta: 0, - instance_codomains: Vec::new(), } } - fn push_instance_codomain(&mut self, ty: TyV) { - self.instance_codomains.push(ty); - } - - fn pop_instance_codomain(&mut self) { - self.instance_codomains.pop(); - } - - fn current_instance_codomain(&self) -> Option<&TyV> { - self.instance_codomains.last() + /// The codomain model of the instance body currently being + /// elaborated, if any. Its fields are the codomain's generators, + /// looked up by name by the instance-clause arms. + fn instance_codomain(&self) -> Option> { + self.ctx.codomain.clone() } fn theory(&self) -> &TheoryDef { @@ -384,7 +373,7 @@ impl<'a> Elaborator<'a> { arg_ty: TyV, arg_label_str: &str, ) -> (TmS, TmV, TyV) { - let Some(model_ty) = self.current_instance_codomain().cloned() else { + let Some(codomain) = self.instance_codomain() else { return self .syn_error("applied codomain morphism is only allowed inside an instance body"); }; @@ -396,10 +385,7 @@ impl<'a> Elaborator<'a> { }; let f_label = label_seg(f); let f_name = name_seg(f); - let TyV_::Record(cod_r) = &*model_ty else { - return self.syn_error("instance codomain is not a record type"); - }; - let Some(mor_ty_s) = cod_r.fields.get(f_name) else { + let Some(mor_ty_s) = codomain.fields.get(f_name) else { return self.syn_error(format!("no such codomain morphism {f_name}")); }; let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { @@ -425,17 +411,17 @@ impl<'a> Elaborator<'a> { /// Elaborate an instance body — a tuple of `name : type`, `field /// := [names]`, and `mor(arg) := target` clauses — against the - /// enclosing sketch type `model_ty`. Produces a [`TmS_::Instance`] + /// enclosing sketch type `codomain`. Produces a [`TmS_::Instance`] /// / [`TmV_::Instance`] pair whose payload is the instance's /// generator slots, equation witnesses, and sub-instance imports. /// - /// The codomain is pushed onto the instance-codomain stack so that - /// generator (fiber) clauses and applied-codomain-morphism syntax - /// inside the body resolve correctly. - fn instance_body(&mut self, model_ty: &TyV, n: &FNtn) -> (TmS, TmV) { - self.push_instance_codomain(model_ty.clone()); + /// The codomain is set on the context (and restored on exit) so + /// that generator (fiber) clauses and applied-codomain-morphism + /// syntax inside the body resolve their generators by name. + fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (TmS, TmV) { + let saved = self.ctx.codomain.replace(Rc::new(codomain.clone())); let result = self.instance_body_inner(n); - self.pop_instance_codomain(); + self.ctx.codomain = saved; result } @@ -531,21 +517,16 @@ impl<'a> Elaborator<'a> { .iter() .all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) => { - let Some(codomain) = elab.current_instance_codomain().cloned() else { + let Some(codomain) = elab.instance_codomain() else { elab.error::<()>( "mapping-literal assignment is only allowed inside an instance body", ); failed = true; continue; }; - let TyV_::Record(cod_r) = &*codomain else { - elab.error::<()>("instance codomain is not a record type"); - failed = true; - continue; - }; let f_seg = name_seg(*field_name); let f_label = label_seg(*field_name); - let Some(mor_ty_s) = cod_r.fields.get(f_seg) else { + let Some(mor_ty_s) = codomain.fields.get(f_seg) else { elab.error::<()>(format!("no such codomain field {field_name}")); failed = true; continue; @@ -607,7 +588,7 @@ impl<'a> Elaborator<'a> { // `field := [n1, n2, ...]` — set-literal assignment to // an object-typed field of the codomain. App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(name_ns))) => { - let Some(codomain) = elab.current_instance_codomain().cloned() else { + let Some(codomain) = elab.instance_codomain() else { elab.error::<()>( "set-literal field assignment is only allowed inside an \ instance body", @@ -615,14 +596,9 @@ impl<'a> Elaborator<'a> { failed = true; continue; }; - let TyV_::Record(cod_r) = &*codomain else { - elab.error::<()>("instance codomain is not a record type"); - failed = true; - continue; - }; let f_seg = name_seg(*field_name); let f_label = label_seg(*field_name); - let Some(field_ty_s) = cod_r.fields.get(f_seg) else { + let Some(field_ty_s) = codomain.fields.get(f_seg) else { elab.error::<()>(format!("no such codomain field {field_name}")); failed = true; continue; @@ -1115,7 +1091,7 @@ impl<'a> Elaborator<'a> { } _ => false, }) { - return elab.instance_body(ty, n); + return elab.instance_body(r, n); } // Mapping-literal disambiguation: `field := [k := v, ...]` // where the LHS field is a *morphism* of the enclosing @@ -1137,7 +1113,7 @@ impl<'a> Elaborator<'a> { }; matches!(&**field_ty_s, TyS_::Morphism(_, _, _)) }) { - return elab.instance_body(ty, n); + return elab.instance_body(r, n); } if r.fields.len() != field_ns.len() { return elab.chk_error(format!( From 950f254df929a64332d5dbd1ad8b7338fa9bf7aa Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 16 Jun 2026 16:09:21 -0700 Subject: [PATCH 26/53] documentation for instance_body_inner --- packages/catlog/src/tt/text_elab.rs | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index aaa6af0b1..d42eac94f 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -425,6 +425,30 @@ impl<'a> Elaborator<'a> { result } + /// Elaborate the clauses of an instance body (the tuple `n`) into the + /// payload of an [`InstanceBodyS`]/[`InstanceBodyV`]. The codomain is + /// already set on the context by [`Self::instance_body`]. + /// + /// Steps: + /// 1. Set up empty accumulators (see below) for the clauses to fill. + /// 2. Walk each clause, dispatching on its surface shape into one of + /// the forms below. A malformed clause reports an error and sets + /// `failed`, but the walk continues so a single pass surfaces as + /// many errors as possible. + /// 3. If any clause failed, return an empty instance (errors already + /// reported); otherwise assemble the accumulators into the paired + /// instance terms. + /// + /// The clause forms, in match order: + /// - `name : type` — dispatched on the *elaborated type's* shape: a + /// fiber type `Over(p)` declares a generator; a record type is a + /// sub-instance import (must name a top-level instance def); an + /// identity type `a == b` is an anonymous equation. + /// - `field := [k := t, ...]` — mapping-literal: sugar for a batch of + /// per-key equations `field(k) := t` against a codomain *morphism*. + /// - `field := [n1, n2, ...]` — set-literal: declares generators in + /// the fiber over a codomain *object* `field`. + /// - `mor(arg) := target` — a single equation witness. fn instance_body_inner(&mut self, n: &FNtn) -> (TmS, TmV) { let mut elab = self.enter(n.loc()); let Tuple(field_ns) = n.ast0() else { @@ -434,6 +458,10 @@ impl<'a> Elaborator<'a> { TmV::instance(InstanceBodyV::default()), ); }; + // Accumulators, assembled into the instance payload at the end: + // generators (with the codomain fiber path each lives over), + // equation witnesses (quoted lhs/rhs pairs), and imported + // sub-instances. `_s`/`_v` hold the syntactic / value forms. let mut gens: IndexMap)> = IndexMap::new(); let mut eqns_s: Vec<(TmS, TmS)> = Vec::new(); @@ -653,6 +681,9 @@ impl<'a> Elaborator<'a> { } } + // Assemble the accumulators into the instance payload, unless a + // clause failed — then errors are already reported, so bail with + // an empty instance rather than a half-built one. if failed { return ( TmS::instance(InstanceBodyS::default()), From 20feba68a45bc140444bcee388ea78fa5d632e72 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 16 Jun 2026 18:04:01 -0700 Subject: [PATCH 27/53] introduce instance keyword to disambiguate instance terms from cons terms --- .../examples/tt/text/test_instances.dbltt | 13 ++- .../tt/text/test_instances.dbltt.snapshot | 17 ++- packages/catlog/src/tt/eval.rs | 5 +- packages/catlog/src/tt/modelgen.rs | 6 +- packages/catlog/src/tt/stx.rs | 6 +- packages/catlog/src/tt/text_elab.rs | 106 +++++++++--------- packages/catlog/src/tt/toplevel.rs | 3 + 7 files changed, 89 insertions(+), 67 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 8e7355fba..6aa50e7b8 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -9,11 +9,11 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -def Edge : WeightedGraph := [ +instance Edge : WeightedGraph := [ E := [e] ] -def I : WeightedGraph := [ +instance I : WeightedGraph := [ V := [v1, v2], Weight := [w], we : Edge, @@ -26,7 +26,7 @@ def I : WeightedGraph := [ weight(wf.e) := w ] -def I2 : WeightedGraph := [ +instance I2 : WeightedGraph := [ V := [v1, v2], Weight := [w], we : Edge, @@ -36,6 +36,9 @@ def I2 : WeightedGraph := [ weight := [we.e := w, wf.e := w] ] -def I3 : WeightedGraph := [ +instance I3 : WeightedGraph := [ E := [e1, e2] -] \ No newline at end of file +] + +#(should_fail) +syn I.E \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index d1106af5b..51285c9ad 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -11,14 +11,14 @@ type WeightedGraph := [ ] #/ declared: WeightedGraph -def Edge : WeightedGraph := [ +instance Edge : WeightedGraph := [ E := [e] ] #/ declared: Edge #/ instance generators: #/ e : E -def I : WeightedGraph := [ +instance I : WeightedGraph := [ V := [v1, v2], Weight := [w], we : Edge, @@ -45,7 +45,7 @@ def I : WeightedGraph := [ #/ weight(we.e) == w #/ weight(wf.e) == w -def I2 : WeightedGraph := [ +instance I2 : WeightedGraph := [ V := [v1, v2], Weight := [w], we : Edge, @@ -69,7 +69,7 @@ def I2 : WeightedGraph := [ #/ weight(we.e) == w #/ weight(wf.e) == w -def I3 : WeightedGraph := [ +instance I3 : WeightedGraph := [ E := [e1, e2] ] #/ declared: I3 @@ -77,3 +77,12 @@ def I3 : WeightedGraph := [ #/ e1 : E #/ e2 : E +#(should_fail) +syn I.E +#/ result: ?0 : ?1 +#/ expected errors: +#/ error[elab]: cannot project a field out of an instance; an instance is eliminated by mapping out of it, not by projection +#/ --> examples/tt/text/test_instances.dbltt:44:5 +#/ 44| syn I.E +#/ 44| ^^^ + diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index a4105ffe1..9035a2451 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -130,7 +130,10 @@ impl<'a> Evaluator<'a> { self.field_ty(ty, tm, field_name), ), TmV_::Cons(fields) => fields.get(field_name).cloned().unwrap(), - _ => panic!(), + // Instances are eliminated by map-out (the representable), never + // projection; the elaborator rejects projecting one, so reaching + // here with any other term value is an elaboration bug. + _ => unreachable!("projected field {field_name} from a non-record term value"), } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 5085d1234..96101ea45 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -577,7 +577,7 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -def I : WeightedGraph := [ +instance I : WeightedGraph := [ V := [v], E := [e], src(e) := v @@ -617,14 +617,14 @@ type WeightedGraph := [ weight : Attr[E, Weight] ] -def Loop : WeightedGraph := [ +instance Loop : WeightedGraph := [ V := [v], E := [e], src(e) := v, tgt(e) := v ] -def UseLoop : WeightedGraph := [ +instance UseLoop : WeightedGraph := [ l : Loop ] "#; diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 76efe6ee8..382b0f324 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -286,11 +286,11 @@ pub enum TmS_ { /// /// Here is an example: /// ```text - /// def Edge : WeightedGraph := [E := [e]] - /// def I : WeightedGraph := [ + /// instance Edge : WeightedGraph := [E := [e]] + /// instance I : WeightedGraph := [ /// V := [v1], /// we : Edge, - /// we.src(e) := v1, + /// src(we.e) := v1, /// ] /// ``` /// The LHS of the mapping clause elaborates to diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index d42eac94f..3ccc9816f 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -24,7 +24,7 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( ("==", Prec::nonassoc(30)), ], &[":", ":=", "&", "Unit", "Hom", "*", "=="], - &["type", "def", "syn", "chk", "norm", "generate", "uwd", "set_theory"], + &["type", "def", "instance", "syn", "chk", "norm", "generate", "uwd", "set_theory"], ); /// The result of elaborating a top-level statement. @@ -162,9 +162,9 @@ impl TopElaborator { let mut elab = self.elaborator(&theory, toplevel); let (_, ret_ty_v) = elab.ty(ty_n); let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); - // A term in the empty context. The body may be an - // ordinary constant or a [`TmV_::Instance`]. - // Instance-ness is later read off the term value. + // A term in the empty context — a tight transformation + // / generalized element (e.g. record construction). + // Instances are declared with `instance`, not here. Some(TopElabResult::Declaration( name, TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), @@ -172,6 +172,39 @@ impl TopElaborator { } } } + "instance" => { + let theory = self.get_theory(tn.loc)?; + let (name, args_n, ty_n, tm_n) = self.annotated_def(tn.body).or_else(|| { + self.error( + tn.loc, + "unknown syntax for instance declaration, expected : := [...]", + ) + })?; + if args_n.is_some() { + return self.error( + tn.loc, + "an instance takes no arguments; for a parameterized map between \ + models, use `def`", + ); + } + let mut elab = self.elaborator(&theory, toplevel); + let (_, ret_ty_v) = elab.ty(ty_n); + // An instance body is checked against its codomain model, a + // record (sketch) type — the same `Record` shape `chk` requires + // for any `[...]`, matched here since instance dispatch lives at + // this keyword rather than in `chk`. + let TyV_::Record(r) = &*ret_ty_v else { + return self.error( + tn.loc, + "an instance must be declared against a record (sketch) type", + ); + }; + let (tm_s, tm_v) = elab.instance_body(r, tm_n); + Some(TopElabResult::Declaration( + name, + TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), + )) + } "syn" => { let theory = self.get_theory(tn.loc)?; let (ctx_ns, n) = self.expr_with_context(tn.body); @@ -425,7 +458,7 @@ impl<'a> Elaborator<'a> { result } - /// Elaborate the clauses of an instance body (the tuple `n`) into the + /// Elaborate the clauses of an instance body (the f-notation `n`) into the /// payload of an [`InstanceBodyS`]/[`InstanceBodyV`]. The codomain is /// already set on the context by [`Self::instance_body`]. /// @@ -937,8 +970,6 @@ impl<'a> Elaborator<'a> { } else if let Some(d) = self.toplevel.lookup(name) { match d { TopDecl::Type(_) => self.syn_error(format!("{name} refers type, not term")), - // An instance is just a term (its `val` is a `TmV_::Instance`), - // so it resolves here like any other constant. TopDecl::DefConst(d) => (TmS::topvar(name), d.val.clone(), d.ty.clone()), TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")), } @@ -957,6 +988,12 @@ impl<'a> Elaborator<'a> { let TyV_::Record(r) = &*ty_v else { return elab.syn_error("can only project from record type"); }; + if matches!(&*tm_v, TmV_::Instance(_)) { + return elab.syn_error( + "cannot project a field out of an instance; an instance is \ + eliminated by mapping out of it, not by projection", + ); + } let label = label_seg(*f); let f = name_seg(*f); if !r.fields.has(f) { @@ -987,6 +1024,12 @@ impl<'a> Elaborator<'a> { let TyV_::Record(r) = &*recv_ty else { return elab.syn_error("can only project from record type"); }; + if matches!(&*recv_v, TmV_::Instance(_)) { + return elab.syn_error( + "cannot project a field out of an instance; an instance is \ + eliminated by mapping out of it, not by projection", + ); + } let arg_name = name_seg(*arg_field); let arg_label = label_seg(*arg_field); if !r.fields.has(arg_name) { @@ -1102,50 +1145,11 @@ impl<'a> Elaborator<'a> { let mut elab = self.enter(n.loc()); match (&**ty, n.ast0()) { (TyV_::Record(r), Tuple(field_ns)) => { - // Dispatch by clause shape. An instance body has at - // least one clause that doesn't fit the - // `field := value` record-construction shape — either a - // `:`-typed slot, a `mor(arg) := target` mapping entry - // (LHS is an application), a `field := [names]` - // set-literal assignment (RHS is a tuple of bare names), - // or a `field := [key := target, ...]` mapping-literal - // assignment (RHS is a tuple of `:=`-clauses). The - // remaining `field := value` shape — and a tuple of - // `:=`-clauses where each LHS matches an inner field of - // a nested record type — is ordinary record construction. - if field_ns.iter().any(|f| match f.ast0() { - App2(L(_, Keyword(":")), _, _) => true, - App2(L(_, Keyword(":=")), L(_, App1(_, _)), _) => true, - App2(L(_, Keyword(":=")), L(_, Var(_)), L(_, Tuple(elems))) => { - // Set-literal: tuple of bare names. - elems.iter().all(|e| matches!(e.ast0(), Var(_))) - } - _ => false, - }) { - return elab.instance_body(r, n); - } - // Mapping-literal disambiguation: `field := [k := v, ...]` - // where the LHS field is a *morphism* of the enclosing - // record (sketch). If the field is object- or - // record-typed, treat as nested record construction - // instead — this distinction can only be made by - // consulting the record's field type. - if field_ns.iter().any(|f| { - let App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(elems))) = - f.ast0() - else { - return false; - }; - if !elems.iter().all(|e| matches!(e.ast0(), App2(L(_, Keyword(":=")), _, _))) { - return false; - } - let Some(field_ty_s) = r.fields.get(name_seg(*field_name)) else { - return false; - }; - matches!(&**field_ty_s, TyS_::Morphism(_, _, _)) - }) { - return elab.instance_body(r, n); - } + // Ordinary record construction (a tight transformation / + // generalized element). Instance bodies are *not* dispatched + // here — they are introduced by the `instance` keyword, which + // calls `instance_body` directly — so this arm has no clause + // shape to disambiguate. if r.fields.len() != field_ns.len() { return elab.chk_error(format!( "wrong number of fields provided, expected {}, got {}", diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index ee6f97a6b..14d9547b8 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -42,6 +42,9 @@ pub struct Type { /// An *instance* of a model is just such a term whose [`val`](Self::val) is a /// [`TmV_::Instance`](super::val::TmV_::Instance): a generator/equation/ /// sub-instance body packaged as an introduction value of a record type. +/// Both kinds arise as a `DefConst`, but from different surface declarations: +/// `def NAME : T := ` for a plain term, `instance NAME : T := [...]` for +/// an instance. /// When an instance name is used in *type* position (for a /// sub-instance import), its type is the record type synthesized from that body /// by [`synth_instance_body_ty`](super::eval::Evaluator::synth_instance_body_ty). From 723a89184511ee86a06a0bff7158513d18a42b3f Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 16 Jun 2026 18:24:10 -0700 Subject: [PATCH 28/53] cleanup --- packages/catlog/src/dbl/model_instance.rs | 2 +- packages/catlog/src/tt/eval.rs | 13 +++++++++++++ packages/catlog/src/tt/toplevel.rs | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/catlog/src/dbl/model_instance.rs b/packages/catlog/src/dbl/model_instance.rs index 41642e700..ff8748c59 100644 --- a/packages/catlog/src/dbl/model_instance.rs +++ b/packages/catlog/src/dbl/model_instance.rs @@ -1,6 +1,6 @@ //! Instances of models of a double theory. //! -//! An **instance** of a model (see [Carlson-Patterson 2025][https://arxiv.org/abs/2510.08861]) +//! An **instance** of a model (see [Carlson-Patterson 2025](https://arxiv.org/abs/2510.08861)) //! is here presented via //! a set of generators living over each object of the model, together with //! equations between terms built from morphisms of the model applied to diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 9035a2451..1207c9ce2 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -193,6 +193,19 @@ impl<'a> Evaluator<'a> { /// (not this type). An import that contradicts `D`'s fiber equations is /// therefore not rejected here — consistency of the resulting equation /// set is a model-layer concern, not checked during elaboration. + /// + /// Because these `Id` fields are inert, the *concrete* way of mapping out + /// of `D` — building a record literal (`Cons`) of this type by hand — only + /// works when `D` is equation-free. For an equational `D`, record + /// construction demands the `_eq{i}` fields too, and they can't be + /// discharged through the surface: projecting `l._eqN` fails an `Id`/`Id` + /// convertibility check (the two sides print alike but don't convert), and + /// the unit `'tt` is rejected (`Unit` is not the `Id` type). The supported + /// map-out is the *abstract* one — a neutral of this type, as `we : D` + /// imports produce — for which the witnesses never need constructing. A + /// concrete map-out of an equational `D` would require auto-discharging the + /// `Id` fields during record construction (they are extensional, so η to + /// `tt`); not yet done. pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { let mut fields: Row = Row::empty(); for (name, (label, path)) in &body.generators { diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 14d9547b8..585a8496a 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -40,7 +40,7 @@ pub struct Type { /// context, this is OK to use in any other context as well. /// /// An *instance* of a model is just such a term whose [`val`](Self::val) is a -/// [`TmV_::Instance`](super::val::TmV_::Instance): a generator/equation/ +/// [`TmV_::Instance`]: a generator/equation/ /// sub-instance body packaged as an introduction value of a record type. /// Both kinds arise as a `DefConst`, but from different surface declarations: /// `def NAME : T := ` for a plain term, `instance NAME : T := [...]` for From 40204538768cf1508c883165dbb06c5c3fce473f Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 17 Jun 2026 11:20:49 -0700 Subject: [PATCH 29/53] Make unit an empty record --- packages/catlog/src/tt/eval.rs | 25 ++++++----------- packages/catlog/src/tt/modelgen.rs | 1 - packages/catlog/src/tt/notebook_elab.rs | 2 +- packages/catlog/src/tt/stx.rs | 23 ---------------- packages/catlog/src/tt/text_elab.rs | 36 ++++++++++++++++--------- packages/catlog/src/tt/val.rs | 21 ++++++++------- 6 files changed, 43 insertions(+), 65 deletions(-) diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 1207c9ce2..e8331ae12 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -73,7 +73,6 @@ impl<'a> Evaluator<'a> { ty_v.add_specialization(path, self.eval_ty(s)) }) } - TyS_::Unit => TyV::unit(), TyS_::Meta(mv) => TyV::meta(*mv), TyS_::Over(path) => TyV::over(path.clone()), } @@ -96,7 +95,6 @@ impl<'a> Evaluator<'a> { TmS_::Var(i, _, _) => self.env.get(**i).cloned().unwrap(), TmS_::Cons(fields) => TmV::cons(fields.map(|tm| self.eval_tm(tm))), TmS_::Proj(tm, field, label) => self.proj(&self.eval_tm(tm), *field, *label), - TmS_::Tt => TmV::tt(), TmS_::Id(x) => TmV::id(self.eval_tm(x)), TmS_::Tab(mor) => TmV::tab(self.eval_tm(mor)), TmS_::Compose(f, g) => TmV::compose(self.eval_tm(f), self.eval_tm(g)), @@ -200,12 +198,12 @@ impl<'a> Evaluator<'a> { /// construction demands the `_eq{i}` fields too, and they can't be /// discharged through the surface: projecting `l._eqN` fails an `Id`/`Id` /// convertibility check (the two sides print alike but don't convert), and - /// the unit `'tt` is rejected (`Unit` is not the `Id` type). The supported - /// map-out is the *abstract* one — a neutral of this type, as `we : D` - /// imports produce — for which the witnesses never need constructing. A - /// concrete map-out of an equational `D` would require auto-discharging the - /// `Id` fields during record construction (they are extensional, so η to - /// `tt`); not yet done. + /// the empty record `'tt` (`[]`) is rejected (a record is not the `Id` + /// type). The supported map-out is the *abstract* one — a neutral of this + /// type, as `we : D` imports produce — for which the witnesses never need + /// constructing. A concrete map-out of an equational `D` would require + /// auto-discharging the `Id` fields during record construction (they are + /// extensional, so η to the empty cons); not yet done. pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { let mut fields: Row = Row::empty(); for (name, (label, path)) in &body.generators { @@ -226,7 +224,7 @@ impl<'a> Evaluator<'a> { // a neutral ignores it — so a placeholder suffices. Quoting in `ev` // (one binder deeper) sends `self` to de Bruijn index 0, matching // how `field_ty` snocs the record value when projecting. - let (self_n, ev) = self.bind_self(TyV::unit()); + let (self_n, ev) = self.bind_self(TyV::empty_record()); for (i, (lhs_v, rhs_v)) in body.equations.iter().enumerate() { let Some(over_ty) = fiber_equation_ty(lhs_v) else { continue; @@ -288,7 +286,6 @@ impl<'a> Evaluator<'a> { TyV_::Id(ty, tm1, tm2) => { TyS::id(self.quote_ty(ty), self.quote_tm(tm1), self.quote_tm(tm2)) } - TyV_::Unit => TyS::unit(), TyV_::Meta(mv) => TyS::meta(*mv), TyV_::Over(path) => TyS::over(path.clone()), } @@ -329,7 +326,6 @@ impl<'a> Evaluator<'a> { }), TmV_::List(elems) => TmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()), TmV_::Cons(fields) => TmS::cons(fields.map(|tm| self.quote_tm(tm))), - TmV_::Tt => TmS::tt(), TmV_::Id(x) => TmS::id(self.quote_tm(x)), TmV_::Tab(mor) => TmS::tab(self.quote_tm(mor)), TmV_::Compose(f, g) => TmS::compose(self.quote_tm(f), self.quote_tm(g)), @@ -368,7 +364,6 @@ impl<'a> Evaluator<'a> { } TyV_::Sing(_, x) => self.equal_tm(tm, x), TyV_::Id(_, _, _) => Ok(()), - TyV_::Unit => Ok(()), TyV_::Meta(_) => Ok(()), TyV_::Over(_) => Ok(()), } @@ -414,7 +409,6 @@ impl<'a> Evaluator<'a> { } (TyV_::Sing(ty1, _), _) => self.convertible_ty(ty1, ty2), (_, TyV_::Sing(ty2, _)) => self.convertible_ty(ty1, ty2), - (TyV_::Unit, TyV_::Unit) => Ok(()), (TyV_::Over(p1), TyV_::Over(p2)) => { if p1 == p2 { Ok(()) @@ -441,8 +435,7 @@ impl<'a> Evaluator<'a> { TmV::cons(fields) } TyV_::Sing(_, x) => x.clone(), - TyV_::Id(_, _, _) => TmV::tt(), // Extensional equality at a 100% discount! - TyV_::Unit => TmV::tt(), + TyV_::Id(_, _, _) => TmV::empty_cons(), // Extensional equality at a 100% discount! TyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), TyV_::Over(_) => TmV::neu(n.clone(), ty.clone()), } @@ -475,7 +468,6 @@ impl<'a> Evaluator<'a> { v.clone() } } - TmV_::Tt => TmV::tt(), TmV_::Id(x) => TmV::id(self.eta(x, None)), TmV_::Tab(mor) => TmV::tab(self.eta(mor, None)), TmV_::Compose(f, g) => TmV::compose(self.eta(f, None), self.eta(g, None)), @@ -529,7 +521,6 @@ impl<'a> Evaluator<'a> { } Ok(()) } - (TmV_::Tt, TmV_::Tt) => Ok(()), (TmV_::Meta(mv1), TmV_::Meta(mv2)) => { if mv1 == mv2 { Ok(()) diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 96101ea45..b567e78e4 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -399,7 +399,6 @@ impl<'a> ModelGenerator<'a> { } None } - TyV_::Unit => None, TyV_::Meta(_) => None, TyV_::Over(_) => None, } diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index f84623bb8..2f68687eb 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -70,7 +70,7 @@ impl<'a> Elaborator<'a> { fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { let v = TmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), - ty.clone().unwrap_or(TyV::unit()), + ty.clone().unwrap_or(TyV::empty_record()), ); let v = if ty.is_some() { self.evaluator().eta(&v, ty.as_ref()) diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 382b0f324..f1ab5cb62 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -82,13 +82,6 @@ pub enum TyS_ { /// the type of the field at path `p`. Specialize(TyS, Vec<(Vec<(FieldName, LabelSegment)>, TyS)>), - /// Type constructor for the unit type. - /// - /// Example syntax: `Unit`. - /// - /// All terms of this type are convertible with `tt : Unit`. - Unit, - /// A metavar. /// /// Currently, this is only used for handling elaboration errors, we might @@ -158,11 +151,6 @@ impl TyS { Self(Rc::new(TyS_::Specialize(ty, specializations))) } - /// Smart constructor for [TyS], [TyS_::Unit] case. - pub fn unit() -> Self { - Self(Rc::new(TyS_::Unit)) - } - /// Smart constructor for [TyS], [TyS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TyS_::Meta(mv))) @@ -194,7 +182,6 @@ impl ToDoc for TyS { d.iter().map(|(name, ty)| binop(t(":"), t(path_to_string(name)), ty.to_doc())), ), ), - TyS_::Unit => t("Unit"), TyS_::Meta(mv) => t(format!("?{}", mv.id)), TyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), } @@ -255,10 +242,6 @@ pub enum TmS_ { Cons(Row), /// Record elimination. Proj(TmS, FieldName, LabelSegment), - /// Unit introduction. - /// - /// Note that eta-expansion takes care of elimination for units. - Tt, /// Identity morphism at an object. Id(TmS), /// Tabulation of a morphism. @@ -364,11 +347,6 @@ impl TmS { Self(Rc::new(TmS_::Proj(tm_s, field_name, label))) } - /// Smart constructor for [TmS], [TmS_::Tt] case. - pub fn tt() -> Self { - Self(Rc::new(TmS_::Tt)) - } - /// Smart constructor for [TmS], [TmS_::Id] case. pub fn id(ob: TmS) -> Self { Self(Rc::new(TmS_::Id(ob))) @@ -434,7 +412,6 @@ impl ToDoc for TmS { TmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), TmS_::OverApp(_, mor_label, _, inner) => inner.to_doc() + t(format!(".{mor_label}")), TmS_::Instance(body) => instance_body_to_doc(body), - TmS_::Tt => t("tt"), TmS_::Meta(mv) => t(format!("?{}", mv.id)), } } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 3ccc9816f..f971c13ae 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -162,9 +162,12 @@ impl TopElaborator { let mut elab = self.elaborator(&theory, toplevel); let (_, ret_ty_v) = elab.ty(ty_n); let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); - // A term in the empty context — a tight transformation - // / generalized element (e.g. record construction). - // Instances are declared with `instance`, not here. + // A closed (empty-context) term: a tight transformation + // S -> Unit. Unit is the empty record, i.e. the empty model. + // A tight map into the empty model exists only when S is itself empty, + // so the sole closed `def` is the identity on the empty + // model, `tt : Unit`. Closed loose terms are instances, + // declared with `instance`. Some(TopElabResult::Declaration( name, TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), @@ -190,14 +193,10 @@ impl TopElaborator { let mut elab = self.elaborator(&theory, toplevel); let (_, ret_ty_v) = elab.ty(ty_n); // An instance body is checked against its codomain model, a - // record (sketch) type — the same `Record` shape `chk` requires - // for any `[...]`, matched here since instance dispatch lives at - // this keyword rather than in `chk`. + // record type. let TyV_::Record(r) = &*ret_ty_v else { - return self.error( - tn.loc, - "an instance must be declared against a record (sketch) type", - ); + return self + .error(tn.loc, "an instance must be declared against a record type"); }; let (tm_s, tm_v) = elab.instance_body(r, tm_n); Some(TopElabResult::Declaration( @@ -383,7 +382,7 @@ impl<'a> Elaborator<'a> { fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { let v = TmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), - ty.clone().unwrap_or(TyV::unit()), + ty.clone().unwrap_or(TyV::empty_record()), ); let v = if ty.is_some() { self.evaluator().eta(&v, ty.as_ref()) @@ -395,6 +394,13 @@ impl<'a> Elaborator<'a> { v } + /// The unit type, elaborated as the empty record — i.e. the empty + /// model. `Unit` and `tt` are surface sugar for the empty record type + /// and its unique element, the empty cons `[]`. + fn empty_record_ty(&self) -> (TyS, TyV) { + (TyS::record(Row::empty()), TyV::empty_record()) + } + /// Apply a codomain morphism `f` to an already-elaborated argument /// of fiber type. Shared by the bare `f(x)` and `f(receiver.fld)` /// arms of [`Self::syn`]. @@ -854,7 +860,7 @@ impl<'a> Elaborator<'a> { let mut elab = self.enter(n.loc()); match n.ast0() { Var(name) => elab.lookup_ty(name_seg(*name)), - Keyword("Unit") => (TyS::unit(), TyV::unit()), + Keyword("Unit") => elab.empty_record_ty(), App1(L(_, Prim("sing")), tm_n) => { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); (TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v)) @@ -1133,7 +1139,11 @@ impl<'a> Elaborator<'a> { let eval = elab.evaluator().with_env(env.clone()); (TmS::topapp(tv, arg_stxs), eval.eval_tm(&d.body), eval.eval_ty(&d.ret_ty)) } - Tag("tt") => (TmS::tt(), TmV::tt(), TyV::unit()), + Tag("tt") => { + // `tt` is the unique element of `Unit`, i.e. the empty record `[]`. + let (_, ty_v) = elab.empty_record_ty(); + (TmS::cons(Row::empty()), TmV::cons(Row::empty()), ty_v) + } Tuple(_) => elab.syn_error("must check against a type in order to construct a record"), Prim("hole") => elab.syn_error("explicit hole"), _ => elab.syn_error("unexpected notation for term"), diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 0dc4f672f..59fa5d0d4 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -94,8 +94,6 @@ pub enum TyV_ { Sing(TyV, TmV), /// Type constructor for identity types, also see [TyS_::Id]. Id(TyV, TmV, TmV), - /// Type constructor for unit types, also see [TyS_::Unit]. - Unit, /// A metavariable, also see [TyS_::Meta]. Meta(MetaVar), /// The type of terms in a fiber over an object generator of some @@ -174,9 +172,12 @@ impl TyV { } } - /// Smart constructor for [TyV], [TyV_::Unit] case. - pub fn unit() -> Self { - Self(Rc::new(TyV_::Unit)) + /// The empty record type — the unit type / empty model. Its + /// environment is empty because the type has no fields, so the + /// environment is never consulted. Also used as a throwaway type for + /// untyped placeholder binders (whose type is discarded). + pub fn empty_record() -> Self { + Self(Rc::new(TyV_::Record(RecordV::new(Env::nil(), Row::empty(), Dtry::empty())))) } /// Smart constructor for [TyV], [TyV_::Meta] case. @@ -260,8 +261,6 @@ pub enum TmV_ { List(Vec), /// Records. Cons(Row), - /// The unique element of the unit type. - Tt, /// The identity morphism of an object. Id(TmV), /// The tabulation of a morphism. @@ -313,9 +312,11 @@ impl TmV { TmV(Rc::new(TmV_::Cons(fields))) } - /// Smart constructor for [TmV], [TmV_::Tt] case. - pub fn tt() -> Self { - TmV(Rc::new(TmV_::Tt)) + /// The empty record value `[]` — the unique element of the empty + /// record type. Also serves as the (proof-irrelevant) canonical + /// inhabitant of `Id` types under eta. + pub fn empty_cons() -> Self { + TmV(Rc::new(TmV_::Cons(Row::empty()))) } /// Smart constructor for [TmV], [TmV_::Id] case. From 2056e6946da4b08ff4788f3ea9e403eafce2aaa8 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 17 Jun 2026 11:37:26 -0700 Subject: [PATCH 30/53] fmt --- packages/catlog/src/tt/text_elab.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index f971c13ae..dcec207a3 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -163,7 +163,7 @@ impl TopElaborator { let (_, ret_ty_v) = elab.ty(ty_n); let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); // A closed (empty-context) term: a tight transformation - // S -> Unit. Unit is the empty record, i.e. the empty model. + // S -> Unit. Unit is the empty record, i.e. the empty model. // A tight map into the empty model exists only when S is itself empty, // so the sole closed `def` is the identity on the empty // model, `tt : Unit`. Closed loose terms are instances, From ab9a7ec12fc3b8e8f3f5bb8bb52ad2086a410e2e Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 17 Jun 2026 11:57:54 -0700 Subject: [PATCH 31/53] Cleanup spurious support for morphism equations in instance declarations --- packages/catlog/src/tt/stx.rs | 9 ++++----- packages/catlog/src/tt/text_elab.rs | 18 +++++++++++++++--- packages/catlog/src/tt/val.rs | 5 ++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index f1ab5cb62..c92f67ebe 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -92,7 +92,7 @@ pub enum TyS_ { /// instance's codomain model. Internal-only; no surface syntax — /// inhabitants are introduced via set-literal clauses `field := /// [...]` inside an instance body, and applications via the - /// `mor(arg)` and `tm.mor(arg)` syn arms. + /// `mor(arg)` and `mor(receiver.fld)` syn arms. /// /// The path identifies the object generator in the codomain. /// Instance identity is contextual rather than part of the type: @@ -257,7 +257,7 @@ pub enum TmS_ { /// /// Arguments, in order: /// 1. `mor` — name of the codomain morphism being applied - /// (e.g. `src` in `we.src(e)`). + /// (e.g. `src` in `src(we.e)`). /// 2. `mor_label` — display label for that name. /// 3. `tgt_path` — the codomain object-path that `mor` lands at. /// Stored on the node so [`super::eval::Evaluator::eval_tm`] @@ -298,9 +298,8 @@ pub enum TmS_ { /// instance) to a path into the model identifying which object /// generator's fiber it lives over. Generators are introduced by /// surface set-literal clauses `field := [...]` in the instance body. -/// - `equations` is a list of `(lhs, rhs)` pairs over either fiber -/// ([`TyS_::Over`]) or morphism types. Order is preserved but identity -/// isn't semantically significant. +/// - `equations` is a list of `(lhs, rhs)` pairs over fiber +/// ([`TyS_::Over`]) types. /// - `sub_instances` maps each sub-instance import's local name to a /// nested instance term. This is what surface `we : Edge` lowers to. #[derive(Default)] diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index dcec207a3..3222d8978 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -557,7 +557,18 @@ impl<'a> Elaborator<'a> { subs_v.insert(n_seg, (label, sub_v)); elab.intro(n_seg, label, Some(ty_v)); } - TyV_::Id(_, lhs, rhs) => { + TyV_::Id(eq_ty, lhs, rhs) => { + if !matches!(&**eq_ty, TyV_::Over(_)) { + let quoted = elab.evaluator().quote_ty(eq_ty); + elab.error::<()>(format!( + "instance equation {name_str} is over {quoted}, but an \ + instance's equations must be between elements over an \ + object (fiber elements); morphism equations constrain \ + the model, not an instance", + )); + failed = true; + continue; + } let evaluator = elab.evaluator(); let lhs_s = evaluator.quote_tm(lhs); let rhs_s = evaluator.quote_tm(rhs); @@ -697,11 +708,12 @@ impl<'a> Elaborator<'a> { // an equation witness. App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { let (lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); - if !matches!(&*lhs_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { + if !matches!(&*lhs_ty, TyV_::Over(_)) { elab.loc = Some(lhs_n.loc()); elab.error::<()>( "mapping-entry clause `mor(arg) := target` requires the LHS \ - to be a morphism or an element over an object", + to be an element over an object (a fiber element); morphism \ + equations constrain the model, not an instance", ); failed = true; continue; diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 59fa5d0d4..23db80cbd 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -172,9 +172,8 @@ impl TyV { } } - /// The empty record type — the unit type / empty model. Its - /// environment is empty because the type has no fields, so the - /// environment is never consulted. Also used as a throwaway type for + /// The empty record type — the unit type / empty model. + /// Also used as a throwaway type for /// untyped placeholder binders (whose type is discarded). pub fn empty_record() -> Self { Self(Rc::new(TyV_::Record(RecordV::new(Env::nil(), Row::empty(), Dtry::empty())))) From 39cc234a48acdbd3a27998d9ed70f46e4e8c8f01 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 17 Jun 2026 12:05:25 -0700 Subject: [PATCH 32/53] FMT --- packages/catlog/src/tt/stx.rs | 2 +- packages/catlog/src/tt/val.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index c92f67ebe..48cb95f70 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -299,7 +299,7 @@ pub enum TmS_ { /// generator's fiber it lives over. Generators are introduced by /// surface set-literal clauses `field := [...]` in the instance body. /// - `equations` is a list of `(lhs, rhs)` pairs over fiber -/// ([`TyS_::Over`]) types. +/// ([`TyS_::Over`]) types. /// - `sub_instances` maps each sub-instance import's local name to a /// nested instance term. This is what surface `we : Edge` lowers to. #[derive(Default)] diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 23db80cbd..12e07ffc0 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -172,7 +172,7 @@ impl TyV { } } - /// The empty record type — the unit type / empty model. + /// The empty record type — the unit type / empty model. /// Also used as a throwaway type for /// untyped placeholder binders (whose type is discarded). pub fn empty_record() -> Self { From 960dd635455ad90dd55e39fee0025d9fbacdf2e3 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 17 Jun 2026 17:48:59 -0700 Subject: [PATCH 33/53] replace DefConst with Instance and keyword type with model --- .../catlog/examples/tt/text/test_base.dbltt | 4 +- .../examples/tt/text/test_base.dbltt.snapshot | 4 +- .../tt/text/test_discrete_theories.dbltt | 28 +++--- .../test_discrete_theories.dbltt.snapshot | 28 +++--- .../examples/tt/text/test_equality.dbltt | 2 +- .../tt/text/test_equality.dbltt.snapshot | 2 +- .../examples/tt/text/test_instances.dbltt | 2 +- .../tt/text/test_instances.dbltt.snapshot | 2 +- .../tt/text/test_modal_theories.dbltt | 10 +- .../text/test_modal_theories.dbltt.snapshot | 10 +- .../examples/tt/text/test_tabulators.dbltt | 12 +-- .../tt/text/test_tabulators.dbltt.snapshot | 12 +-- .../catlog/examples/tt/text/test_uwd.dbltt | 6 +- .../examples/tt/text/test_uwd.dbltt.snapshot | 6 +- packages/catlog/src/tt/batch.rs | 11 +-- packages/catlog/src/tt/eval.rs | 61 ++++-------- packages/catlog/src/tt/mod.rs | 4 +- packages/catlog/src/tt/modelgen.rs | 20 ++-- packages/catlog/src/tt/stx.rs | 12 +-- packages/catlog/src/tt/text_elab.rs | 99 ++++++++++++------- packages/catlog/src/tt/toplevel.rs | 56 +++++------ 21 files changed, 187 insertions(+), 204 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_base.dbltt b/packages/catlog/examples/tt/text/test_base.dbltt index ffcd774d1..9631e8118 100644 --- a/packages/catlog/examples/tt/text/test_base.dbltt +++ b/packages/catlog/examples/tt/text/test_base.dbltt @@ -1,8 +1,8 @@ set_theory ThCategory -type u := Unit +model u := Unit -type u2 := u +model u2 := u #/ The unique element of Unit def t : u := 'tt diff --git a/packages/catlog/examples/tt/text/test_base.dbltt.snapshot b/packages/catlog/examples/tt/text/test_base.dbltt.snapshot index f91eeace8..63b48b6b5 100644 --- a/packages/catlog/examples/tt/text/test_base.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_base.dbltt.snapshot @@ -1,10 +1,10 @@ set_theory ThCategory #/ result: set theory to ThCategory -type u := Unit +model u := Unit #/ declared: u -type u2 := u +model u2 := u #/ declared: u2 def t : u := 'tt diff --git a/packages/catlog/examples/tt/text/test_discrete_theories.dbltt b/packages/catlog/examples/tt/text/test_discrete_theories.dbltt index a7aab4e0b..fafd9d927 100644 --- a/packages/catlog/examples/tt/text/test_discrete_theories.dbltt +++ b/packages/catlog/examples/tt/text/test_discrete_theories.dbltt @@ -1,6 +1,6 @@ set_theory ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, @@ -11,13 +11,13 @@ type WeightedGraph := [ generate WeightedGraph -type EntityArr := [ +model EntityArr := [ dom : Entity, cod : Entity, arr : (Hom Entity)[dom, cod] ] -type Graph1 := [ +model Graph1 := [ V : Entity, E : Entity, src : EntityArr & [ .dom := E, .cod := V ], @@ -36,13 +36,13 @@ chk [G : Graph1] (G.src : EntityArr & [ .dom := G.V ] ) norm [x : [a : Entity, b : @sing a]] x.b #(should_fail) -type Graph1 := [ +model Graph1 := [ V : Entity, E : Entity, src : EntityArr & [ .dom := E, .dom := V ] ] -type Graph := [ +model Graph := [ V : Entity, E : Entity, src : (Hom Entity)[E, V], @@ -67,7 +67,7 @@ def reverse1[G : Graph] : Graph := [ norm [G : Graph] (reverse[G]).E syn [G : Graph] (reverse[G]).src -type ReflexiveGraph := [ +model ReflexiveGraph := [ V : Entity, E : Entity, src : (Hom Entity)[E, V], @@ -79,7 +79,7 @@ def edge_id[G : ReflexiveGraph] : (Hom Entity)[G.E, G.E] := @id G.E def edge_src[G : ReflexiveGraph] : (Hom Entity)[G.E, G.E] := G.src * G.refl -type Graph2 := [ +model Graph2 := [ V : Entity, g1 : Graph & [ .V := V], g2 : Graph & [ .V := V] @@ -91,7 +91,7 @@ norm [g : Graph2] g.g1.V generate Graph2 -type ProfunctorGraph := [ +model ProfunctorGraph := [ g1 : Graph, g2 : Graph, het : (Hom Entity)[g1.V, g2.V] @@ -99,7 +99,7 @@ type ProfunctorGraph := [ generate ProfunctorGraph -type WeightedGraph2 := [ +model WeightedGraph2 := [ V : Entity, g1 : WeightedGraph & [ .V := V ], g2 : WeightedGraph & [ .V := V ] @@ -110,22 +110,22 @@ generate WeightedGraph2 set_theory ThCategory #(should_fail) -type Set := [ +model Set := [ A : Entity, ] -type Set := [ +model Set := [ A : Object ] -type DDS := [ +model DDS := [ X : Object, φ : (Hom Object)[X, X] ] set_theory ThSignedCategory -type NegFeedback := [ +model NegFeedback := [ X : Object, Y : Object, f : (Hom Object)[X, Y], @@ -135,7 +135,7 @@ type NegFeedback := [ generate NegFeedback #(should_fail) -type NegFeedback1 := [ +model NegFeedback1 := [ X : Object, Y : Object, f : (Hom Object)[@hole, Y], diff --git a/packages/catlog/examples/tt/text/test_discrete_theories.dbltt.snapshot b/packages/catlog/examples/tt/text/test_discrete_theories.dbltt.snapshot index 7e9b26e3b..36470b8a2 100644 --- a/packages/catlog/examples/tt/text/test_discrete_theories.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_discrete_theories.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThSchema #/ result: set theory to ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, @@ -20,14 +20,14 @@ generate WeightedGraph #/ tgt : E -> V : Hom Entity #/ weight : E -> Weight : Attr -type EntityArr := [ +model EntityArr := [ dom : Entity, cod : Entity, arr : (Hom Entity)[dom, cod] ] #/ declared: EntityArr -type Graph1 := [ +model Graph1 := [ V : Entity, E : Entity, src : EntityArr & [ .dom := E, .cod := V ], @@ -59,7 +59,7 @@ norm [x : [a : Entity, b : @sing a]] x.b #/ result: x.a #(should_fail) -type Graph1 := [ +model Graph1 := [ V : Entity, E : Entity, src : EntityArr & [ .dom := E, .dom := V ] @@ -73,7 +73,7 @@ type Graph1 := [ #/ 42| src : EntityArr & [ .dom := E, .dom := V ] #/ 42| ^^^^^^^^^ -type Graph := [ +model Graph := [ V : Entity, E : Entity, src : (Hom Entity)[E, V], @@ -115,7 +115,7 @@ norm [G : Graph] (reverse[G]).E syn [G : Graph] (reverse[G]).src #/ result: reverse[G].src : (Hom Entity)[G.E, G.V] -type ReflexiveGraph := [ +model ReflexiveGraph := [ V : Entity, E : Entity, src : (Hom Entity)[E, V], @@ -130,7 +130,7 @@ def edge_id[G : ReflexiveGraph] : (Hom Entity)[G.E, G.E] := @id G.E def edge_src[G : ReflexiveGraph] : (Hom Entity)[G.E, G.E] := G.src * G.refl #/ declared: edge_src -type Graph2 := [ +model Graph2 := [ V : Entity, g1 : Graph & [ .V := V], g2 : Graph & [ .V := V] @@ -153,7 +153,7 @@ generate Graph2 #/ g2.src : g2.E -> V : Hom Entity #/ g2.tgt : g2.E -> V : Hom Entity -type ProfunctorGraph := [ +model ProfunctorGraph := [ g1 : Graph, g2 : Graph, het : (Hom Entity)[g1.V, g2.V] @@ -172,7 +172,7 @@ generate ProfunctorGraph #/ g2.tgt : g2.E -> g2.V : Hom Entity #/ het : g1.V -> g2.V : Hom Entity -type WeightedGraph2 := [ +model WeightedGraph2 := [ V : Entity, g1 : WeightedGraph & [ .V := V ], g2 : WeightedGraph & [ .V := V ] @@ -197,7 +197,7 @@ set_theory ThCategory #/ result: set theory to ThCategory #(should_fail) -type Set := [ +model Set := [ A : Entity, ] #/ declared: Set @@ -207,12 +207,12 @@ type Set := [ #/ 114| A : Entity, #/ 114| ^^^^^^ -type Set := [ +model Set := [ A : Object ] #/ declared: Set -type DDS := [ +model DDS := [ X : Object, φ : (Hom Object)[X, X] ] @@ -221,7 +221,7 @@ type DDS := [ set_theory ThSignedCategory #/ result: set theory to ThSignedCategory -type NegFeedback := [ +model NegFeedback := [ X : Object, Y : Object, f : (Hom Object)[X, Y], @@ -237,7 +237,7 @@ generate NegFeedback #/ g : Y -> X : Negative #(should_fail) -type NegFeedback1 := [ +model NegFeedback1 := [ X : Object, Y : Object, f : (Hom Object)[@hole, Y], diff --git a/packages/catlog/examples/tt/text/test_equality.dbltt b/packages/catlog/examples/tt/text/test_equality.dbltt index 43c9004c7..e36b24b61 100644 --- a/packages/catlog/examples/tt/text/test_equality.dbltt +++ b/packages/catlog/examples/tt/text/test_equality.dbltt @@ -1,6 +1,6 @@ set_theory ThSchema -type CommutativeSquare := [ +model CommutativeSquare := [ NW : Entity, NE : Entity, SW : Entity, diff --git a/packages/catlog/examples/tt/text/test_equality.dbltt.snapshot b/packages/catlog/examples/tt/text/test_equality.dbltt.snapshot index 9bbb7b089..4ab53a6dd 100644 --- a/packages/catlog/examples/tt/text/test_equality.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_equality.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThSchema #/ result: set theory to ThSchema -type CommutativeSquare := [ +model CommutativeSquare := [ NW : Entity, NE : Entity, SW : Entity, diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 6aa50e7b8..5af2eeb4c 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -1,6 +1,6 @@ set_theory ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 51285c9ad..b1a8d9946 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThSchema #/ result: set theory to ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, diff --git a/packages/catlog/examples/tt/text/test_modal_theories.dbltt b/packages/catlog/examples/tt/text/test_modal_theories.dbltt index 945873511..fcdf0f59d 100644 --- a/packages/catlog/examples/tt/text/test_modal_theories.dbltt +++ b/packages/catlog/examples/tt/text/test_modal_theories.dbltt @@ -1,6 +1,6 @@ set_theory ThMulticategory -type SigMonoid := [ +model SigMonoid := [ M : Object, op : Multihom[[M, M], M], unit : Multihom[[], M] @@ -8,7 +8,7 @@ type SigMonoid := [ generate SigMonoid -type SigRig := [ +model SigRig := [ R : Object, Add : SigMonoid & [ .M := R ], Mul : SigMonoid & [ .M := R ] @@ -18,7 +18,7 @@ generate SigRig set_theory ThSymMonoidalCategory -type SIR := [ +model SIR := [ S : Object, I : Object, R : Object, @@ -30,7 +30,7 @@ generate SIR syn [P: SIR] (@tensor [P.S, P.I]) -type SIRV := [ +model SIRV := [ Unvax : SIR, V : Object, vaccinate : (Hom Object)[Unvax.S, V] @@ -39,7 +39,7 @@ type SIRV := [ generate SIRV #(should_fail) -type BadOpApp := [ +model BadOpApp := [ X : Object, f : (Hom Object)[@tensor X, X], ] diff --git a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot index 2e33aadfe..cf1f3cdf0 100644 --- a/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_theories.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThMulticategory #/ result: set theory to ThMulticategory -type SigMonoid := [ +model SigMonoid := [ M : Object, op : Multihom[[M, M], M], unit : Multihom[[], M] @@ -14,7 +14,7 @@ generate SigMonoid #/ op : [M, M] -> M : Multihom #/ unit : [] -> M : Multihom -type SigRig := [ +model SigRig := [ R : Object, Add : SigMonoid & [ .M := R ], Mul : SigMonoid & [ .M := R ] @@ -32,7 +32,7 @@ generate SigRig set_theory ThSymMonoidalCategory #/ result: set theory to ThSymMonoidalCategory -type SIR := [ +model SIR := [ S : Object, I : Object, R : Object, @@ -52,7 +52,7 @@ generate SIR syn [P: SIR] (@tensor [P.S, P.I]) #/ result: @tensor [P.S, P.I] : Object -type SIRV := [ +model SIRV := [ Unvax : SIR, V : Object, vaccinate : (Hom Object)[Unvax.S, V] @@ -70,7 +70,7 @@ generate SIRV #/ vaccinate : Unvax.S -> V : Hom Object #(should_fail) -type BadOpApp := [ +model BadOpApp := [ X : Object, f : (Hom Object)[@tensor X, X], ] diff --git a/packages/catlog/examples/tt/text/test_tabulators.dbltt b/packages/catlog/examples/tt/text/test_tabulators.dbltt index 0c21e395c..8031d3a8d 100644 --- a/packages/catlog/examples/tt/text/test_tabulators.dbltt +++ b/packages/catlog/examples/tt/text/test_tabulators.dbltt @@ -1,6 +1,6 @@ set_theory ThCategoryLinks -type SIR := [ +model SIR := [ S : Object, I : Object, R : Object, @@ -11,7 +11,7 @@ type SIR := [ generate SIR -type Endo := [ +model Endo := [ A : Object, f : (Hom Object)[A,A], ] @@ -19,12 +19,12 @@ type Endo := [ generate Endo #(should_fail) -type LinkFlows := [ +model LinkFlows := [ E : Endo, l : Link[@tab E.f,@tab E.f] ] -type WalkingLink := [ +model WalkingLink := [ x : Object, y : Object, z : Object, @@ -32,7 +32,7 @@ type WalkingLink := [ l : Link[z,@tab f] ] -type SI := [ +model SI := [ S : Object, I : Object, inf : WalkingLink & [.x := S, .y := I, .z := I] @@ -51,7 +51,7 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l -type Triangle := [ +model Triangle := [ x : Object, y : Object, z : Object, diff --git a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot index b33a77fa5..6f9233caf 100644 --- a/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_tabulators.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThCategoryLinks #/ result: set theory to ThCategoryLinks -type SIR := [ +model SIR := [ S : Object, I : Object, R : Object, @@ -20,7 +20,7 @@ generate SIR #/ rec : I -> R : Hom Object #/ _ : I -> inf : Link -type Endo := [ +model Endo := [ A : Object, f : (Hom Object)[A,A], ] @@ -32,7 +32,7 @@ generate Endo #/ f : A -> A : Hom Object #(should_fail) -type LinkFlows := [ +model LinkFlows := [ E : Endo, l : Link[@tab E.f,@tab E.f] ] @@ -44,7 +44,7 @@ type LinkFlows := [ #/ 24| l : Link[@tab E.f,@tab E.f] #/ 24| ^^^^^^^^ -type WalkingLink := [ +model WalkingLink := [ x : Object, y : Object, z : Object, @@ -53,7 +53,7 @@ type WalkingLink := [ ] #/ declared: WalkingLink -type SI := [ +model SI := [ S : Object, I : Object, inf : WalkingLink & [.x := S, .y := I, .z := I] @@ -95,7 +95,7 @@ chk [sf : SI] (sf.inf : WalkingLink & [.x := sf.I]) def act_on_link[d : SI] : Link[d.S,@tab d.inf.f] := d.inf.f * d.inf.l #/ declared: act_on_link -type Triangle := [ +model Triangle := [ x : Object, y : Object, z : Object, diff --git a/packages/catlog/examples/tt/text/test_uwd.dbltt b/packages/catlog/examples/tt/text/test_uwd.dbltt index 5b117f41c..66900c271 100644 --- a/packages/catlog/examples/tt/text/test_uwd.dbltt +++ b/packages/catlog/examples/tt/text/test_uwd.dbltt @@ -1,13 +1,13 @@ set_theory ThSignedCategory -type PredPrey := [ +model PredPrey := [ Pred : Object, Prey : Object, eats : Negative[Pred, Prey], feeds : (Hom Object)[Prey, Pred] ] -type TwoLevelFoodChain := [ +model TwoLevelFoodChain := [ Grass : Object, Rabbit : Object, Fox : Object, @@ -17,7 +17,7 @@ type TwoLevelFoodChain := [ uwd TwoLevelFoodChain -type TwoPredSystem := [ +model TwoPredSystem := [ main: PredPrey, secondary: PredPrey & [ .Prey := main.Prey ] ] diff --git a/packages/catlog/examples/tt/text/test_uwd.dbltt.snapshot b/packages/catlog/examples/tt/text/test_uwd.dbltt.snapshot index 7020118e2..1bb72f461 100644 --- a/packages/catlog/examples/tt/text/test_uwd.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_uwd.dbltt.snapshot @@ -1,7 +1,7 @@ set_theory ThSignedCategory #/ result: set theory to ThSignedCategory -type PredPrey := [ +model PredPrey := [ Pred : Object, Prey : Object, eats : Negative[Pred, Prey], @@ -9,7 +9,7 @@ type PredPrey := [ ] #/ declared: PredPrey -type TwoLevelFoodChain := [ +model TwoLevelFoodChain := [ Grass : Object, Rabbit : Object, Fox : Object, @@ -24,7 +24,7 @@ uwd TwoLevelFoodChain #/ level1 [Prey : Object := Grass, Pred : Object := Rabbit], #/ level2 [Prey : Object := Rabbit, Pred : Object := Fox] -type TwoPredSystem := [ +model TwoPredSystem := [ main: PredPrey, secondary: PredPrey & [ .Prey := main.Prey ] ] diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 9f9590fef..e11811bc4 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -11,9 +11,7 @@ use scopeguard::guard; use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; -use super::{ - modelgen::instance_from_def, text_elab::*, theory::std_theories, toplevel::*, val::TmV_, -}; +use super::{modelgen::instance_from_def, text_elab::*, theory::std_theories, toplevel::*}; use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; use crate::one::path::Path; use crate::zero::NameSegment; @@ -219,14 +217,11 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result { - let is_instance = matches!( - &top_decl, - TopDecl::DefConst(d) if matches!(&*d.val, TmV_::Instance(_)) - ); + let is_instance = matches!(&top_decl, TopDecl::Instance(_)); toplevel.declarations.insert(name_segment, top_decl); output.declared(name_segment); if is_instance - && let Some(TopDecl::DefConst(def)) = + && let Some(TopDecl::Instance(def)) = toplevel.declarations.get(&name_segment) { match instance_from_def(&toplevel, &def.theory.definition, def) { diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index e8331ae12..b39c15108 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -51,11 +51,12 @@ impl<'a> Evaluator<'a> { match &**ty { TyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), - // An instance term used in type position evaluates to the - // record type synthesized from its body (see `lookup_ty`). - TopDecl::DefConst(d) => match &*d.val { + // An instance used in type position evaluates to the + // representable record type synthesized from its body (see + // `lookup_ty`). + TopDecl::Instance(i) => match &*i.val { TmV_::Instance(body) => self.synth_instance_body_ty(body), - _ => panic!("top-level {tv} should be a type or instance declaration"), + _ => panic!("instance {tv} should have an instance body"), }, _ => panic!("top-level {tv} should be a type or instance declaration"), }, @@ -84,9 +85,6 @@ impl<'a> Evaluator<'a> { /// to self.env. pub fn eval_tm(&self, tm: &TmS) -> TmV { match &**tm { - TmS_::TopVar(tv) => { - self.toplevel.declarations.get(tv).unwrap().clone().unwrap_const().val - } TmS_::TopApp(tv, args_s) => { let env = Env::nil().extend_by(args_s.iter().map(|arg_s| self.eval_tm(arg_s))); let def = self.toplevel.declarations.get(tv).unwrap().clone().unwrap_def(); @@ -128,9 +126,6 @@ impl<'a> Evaluator<'a> { self.field_ty(ty, tm, field_name), ), TmV_::Cons(fields) => fields.get(field_name).cloned().unwrap(), - // Instances are eliminated by map-out (the representable), never - // projection; the elaborator rejects projecting one, so reaching - // here with any other term value is an elaboration bug. _ => unreachable!("projected field {field_name} from a non-record term value"), } } @@ -174,36 +169,18 @@ impl<'a> Evaluator<'a> { /// *type* position — i.e. the representable `Hom_Inst(D, self)`, the /// type of instance morphisms out of the instance `D`. /// - /// By Yoneda such a morphism is determined by where `D`'s generators + /// Such a morphism is determined by where `D`'s generators /// land, so each generator becomes an `Over`-typed field and each - /// sub-instance recurses. A *fiber* equation (`mor(gen) := target`) - /// is exactly a condition cutting this down from the free product of - /// `Over` fields to the presented representable — the equalizer — so - /// each becomes an `Id`-typed field witnessing that the constraint - /// holds for the morphism's images. Morphism equations (`==`) - /// constrain the model rather than the generator images, so they are - /// correctly *not* equalizer conditions and are omitted. - /// - /// WARNING: these `Id` fields make the type *honest* but are not - /// *enforced*. An import (`we : D`) introduces a neutral of this type - /// rather than constructing one, so nothing checks the equalizer - /// conditions, and equation extraction reads the inlined instance body - /// (not this type). An import that contradicts `D`'s fiber equations is - /// therefore not rejected here — consistency of the resulting equation - /// set is a model-layer concern, not checked during elaboration. + /// sub-instance recurses. A fiber equation (`mor(gen) := target`) + /// becomes an `Id`-typed field witnessing that the constraint + /// holds for the morphism's images. /// + /// Note that these `Id` fields make the type *honest* but are not + /// *enforced*. An import that contradicts `D`'s fiber equations is + /// not rejected here at this time. /// Because these `Id` fields are inert, the *concrete* way of mapping out /// of `D` — building a record literal (`Cons`) of this type by hand — only - /// works when `D` is equation-free. For an equational `D`, record - /// construction demands the `_eq{i}` fields too, and they can't be - /// discharged through the surface: projecting `l._eqN` fails an `Id`/`Id` - /// convertibility check (the two sides print alike but don't convert), and - /// the empty record `'tt` (`[]`) is rejected (a record is not the `Id` - /// type). The supported map-out is the *abstract* one — a neutral of this - /// type, as `we : D` imports produce — for which the witnesses never need - /// constructing. A concrete map-out of an equational `D` would require - /// auto-discharging the `Id` fields during record construction (they are - /// extensional, so η to the empty cons); not yet done. + /// currently works when `D` is equation-free. pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { let mut fields: Row = Row::empty(); for (name, (label, path)) in &body.generators { @@ -220,14 +197,14 @@ impl<'a> Evaluator<'a> { // The equation terms reference this body's generators as local // binders; re-root them at `self` (the record being built) so they // read as field projections, exactly as sibling references do in an - // ordinary record type. The self var's type is irrelevant — quoting - // a neutral ignores it — so a placeholder suffices. Quoting in `ev` + // ordinary record type. The self var's type is irrelevant, + // so a placeholder suffices. Quoting in `ev` // (one binder deeper) sends `self` to de Bruijn index 0, matching // how `field_ty` snocs the record value when projecting. let (self_n, ev) = self.bind_self(TyV::empty_record()); for (i, (lhs_v, rhs_v)) in body.equations.iter().enumerate() { let Some(over_ty) = fiber_equation_ty(lhs_v) else { - continue; + unreachable!("instance equation LHS is not a fiber element"); }; let lhs_s = ev.quote_tm(&reroot_at_self(lhs_v, &self_n, body)); let rhs_s = ev.quote_tm(&reroot_at_self(rhs_v, &self_n, body)); @@ -449,8 +426,6 @@ impl<'a> Evaluator<'a> { TmV_::OverApp(mor, mor_label, tgt_path, inner) => { TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eta(inner, None)) } - // An [`Instance`](TmV_::Instance) is already in normal form - // — its structure isn't subject to η-expansion. TmV_::Instance(_) => v.clone(), TmV_::List(elems) => TmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()), TmV_::Cons(row) => { @@ -646,9 +621,7 @@ impl<'a> Evaluator<'a> { /// The `Over` type of a *fiber* equation's side, if it is one. /// /// Fiber equations (`mor(gen) := target`) relate `Over`-typed terms; their -/// common type is recoverable from the left-hand side. Returns `None` for -/// morphism (`==`) equations, which are not equalizer conditions on an -/// instance's generator images (see [`Evaluator::synth_instance_body_ty`]). +/// common type is recoverable from the left-hand side. fn fiber_equation_ty(tm: &TmV) -> Option { match &**tm { TmV_::OverApp(_, _, tgt_path, _) => Some(TyV::over(tgt_path.clone())), diff --git a/packages/catlog/src/tt/mod.rs b/packages/catlog/src/tt/mod.rs index b0aedd6d4..76a637ba4 100644 --- a/packages/catlog/src/tt/mod.rs +++ b/packages/catlog/src/tt/mod.rs @@ -134,7 +134,7 @@ //! the following double models. //! //! ```text -//! type Graph := [ +//! model Graph := [ //! E : Entity, //! V : Entity, //! src : (Id Entity)[E, V], @@ -142,7 +142,7 @@ //! ] //! /# declared: Graph //! -//! type Graph2 := [ +//! model Graph2 := [ //! V : Entity, //! g1 : Graph & [ .V := V ], //! g2 : Graph & [ .V := V ] diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index b567e78e4..60d1fc28d 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -405,8 +405,8 @@ impl<'a> ModelGenerator<'a> { } } -/// Generates a [`DiscreteDblModelInstance`] from an elaborated instance -/// term — a [`DefConst`] whose value is a [`TmV_::Instance`]. +/// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Instance`] +/// declaration. /// /// Walks the instance body's [`TmV_::Instance`] payload, registering /// each generator with its fiber, each equation as a pair of @@ -417,17 +417,17 @@ impl<'a> ModelGenerator<'a> { pub fn instance_from_def( toplevel: &Toplevel, th: &TheoryDef, - def: &DefConst, + inst: &Instance, ) -> Result<(DiscreteDblModelInstance, Namespace), String> { let TheoryDef::Discrete(_) = th else { return Err("instance generation only supports discrete double theories".into()); }; - let (cod_model, _) = Model::from_ty(toplevel, th, &def.ty); + let (cod_model, _) = Model::from_ty(toplevel, th, &inst.codomain); let cod_model = cod_model .as_discrete() .ok_or_else(|| "expected a discrete codomain model".to_string())?; let mut instance = DiscreteDblModelInstance::new(Rc::new(cod_model)); - let TmV_::Instance(body) = &*def.val else { + let TmV_::Instance(body) = &*inst.val else { return Err("expected a TmV::Instance body".into()); }; let mut namespace = Namespace::new_for_uuid(); @@ -567,7 +567,7 @@ mod tests { let src = r#" set_theory ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, @@ -584,7 +584,7 @@ instance I : WeightedGraph := [ "#; let toplevel = elaborate_to_toplevel(src); let def = match toplevel.declarations.get(&name_seg("I")) { - Some(TopDecl::DefConst(d)) if matches!(&*d.val, TmV_::Instance(_)) => d.clone(), + Some(TopDecl::Instance(i)) => i.clone(), _ => panic!("expected I to be an instance declaration"), }; let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); @@ -607,7 +607,7 @@ instance I : WeightedGraph := [ let src = r#" set_theory ThSchema -type WeightedGraph := [ +model WeightedGraph := [ V : Entity, E : Entity, Weight : AttrType, @@ -634,7 +634,7 @@ instance UseLoop : WeightedGraph := [ // (which re-evaluates every field type against a bound `self`) must // not panic. let loop_def = match toplevel.declarations.get(&name_seg("Loop")) { - Some(TopDecl::DefConst(d)) => d.clone(), + Some(TopDecl::Instance(i)) => i.clone(), _ => panic!("expected Loop to be an instance declaration"), }; let TmV_::Instance(loop_body) = &*loop_def.val else { @@ -655,7 +655,7 @@ instance UseLoop : WeightedGraph := [ // The importer still extracts both generators and the imported // copy's two equations. let use_def = match toplevel.declarations.get(&name_seg("UseLoop")) { - Some(TopDecl::DefConst(d)) => d.clone(), + Some(TopDecl::Instance(i)) => i.clone(), _ => panic!("expected UseLoop to be an instance declaration"), }; let (instance, _ns) = diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 48cb95f70..72e079a1e 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -229,9 +229,10 @@ impl fmt::Display for TyS { /// Inner enum for [TmS]. pub enum TmS_ { - /// A reference to a top-level constant def. - TopVar(TopVarName), /// An application of a top-level term judgment to arguments. + /// + /// A closed term (a nullary `def`, e.g. `tt : Unit`) is the empty-argument + /// case `TopApp(name, [])`. TopApp(TopVarName, Vec), /// Variable syntax. /// @@ -321,11 +322,6 @@ pub struct InstanceBodyS { pub struct TmS(Rc); impl TmS { - /// Smart constructor for [TmS], [TmS_::TopVar] case. - pub fn topvar(var_name: VarName) -> Self { - Self(Rc::new(TmS_::TopVar(var_name))) - } - /// Smart constructor for [TmS], [TmS_::TopApp] case. pub fn topapp(var_name: VarName, args: Vec) -> Self { Self(Rc::new(TmS_::TopApp(var_name, args))) @@ -395,7 +391,7 @@ impl TmS { impl ToDoc for TmS { fn to_doc<'a>(&self) -> D<'a> { match &**self { - TmS_::TopVar(name) => t(format!("{}", name)), + TmS_::TopApp(name, args) if args.is_empty() => t(format!("{}", name)), TmS_::TopApp(name, args) => { t(format!("{}", name)) + tuple(args.iter().map(|arg| arg.to_doc())) } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 3222d8978..12549002b 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -24,7 +24,17 @@ pub const TT_PARSE_CONFIG: ParseConfig = ParseConfig::new( ("==", Prec::nonassoc(30)), ], &[":", ":=", "&", "Unit", "Hom", "*", "=="], - &["type", "def", "instance", "syn", "chk", "norm", "generate", "uwd", "set_theory"], + &[ + "model", + "def", + "instance", + "syn", + "chk", + "norm", + "generate", + "uwd", + "set_theory", + ], ); /// The result of elaborating a top-level statement. @@ -115,12 +125,12 @@ impl TopElaborator { }, _ => self.error(tn.loc, "expected a theory name"), }, - "type" => { + "model" => { let theory = self.get_theory(tn.loc)?; let (name, ty_n) = self.bare_def(tn.body).or_else(|| { self.error( tn.loc, - "unknown syntax for type declaration, expected := ", + "unknown syntax for model declaration, expected := ", ) })?; let (ty_s, ty_v) = self.elaborator(&theory, toplevel).ty(ty_n); @@ -160,17 +170,17 @@ impl TopElaborator { } None => { let mut elab = self.elaborator(&theory, toplevel); - let (_, ret_ty_v) = elab.ty(ty_n); - let (tm_s, tm_v) = elab.chk(&ret_ty_v, tm_n); + let (ret_ty_s, ret_ty_v) = elab.ty(ty_n); + let (body_s, _) = elab.chk(&ret_ty_v, tm_n); // A closed (empty-context) term: a tight transformation // S -> Unit. Unit is the empty record, i.e. the empty model. // A tight map into the empty model exists only when S is itself empty, // so the sole closed `def` is the identity on the empty - // model, `tt : Unit`. Closed loose terms are instances, - // declared with `instance`. + // model, `tt : Unit`. Such a closed term is just a nullary + // `Def` (empty argument context). Some(TopElabResult::Declaration( name, - TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), + TopDecl::Def(Def::new(theory.clone(), Row::empty(), ret_ty_s, body_s)), )) } } @@ -201,7 +211,7 @@ impl TopElaborator { let (tm_s, tm_v) = elab.instance_body(r, tm_n); Some(TopElabResult::Declaration( name, - TopDecl::DefConst(DefConst::new(theory.clone(), tm_s, tm_v, ret_ty_v)), + TopDecl::Instance(Instance::new(theory.clone(), tm_s, tm_v, ret_ty_v)), )) } "syn" => { @@ -530,10 +540,8 @@ impl<'a> Elaborator<'a> { Var(sub_name) => { let topvar = name_seg(*sub_name); match elab.toplevel.declarations.get(&topvar) { - Some(TopDecl::DefConst(d)) - if matches!(&*d.val, TmV_::Instance(_)) => - { - (d.stx.clone(), d.val.clone()) + Some(TopDecl::Instance(i)) => { + (i.stx.clone(), i.val.clone()) } _ => { elab.error::<()>(format!( @@ -781,26 +789,24 @@ impl<'a> Elaborator<'a> { )) } } - // An instance term used in type position yields the record - // type synthesized from its body, allowing sub-instance + // An instance used in type position yields the representable + // record type synthesized from its body, allowing sub-instance // imports (`we : Edge`) to project into Edge's fields. - TopDecl::DefConst(d) if matches!(&*d.val, TmV_::Instance(_)) => { - if d.theory == self.theory { - let TmV_::Instance(body) = &*d.val else { - unreachable!("guarded by the match arm above") + TopDecl::Instance(i) => { + if i.theory == self.theory { + let TmV_::Instance(body) = &*i.val else { + unreachable!("an Instance always has an instance body") }; let body_ty = self.evaluator().synth_instance_body_ty(body); (TyS::topvar(name), body_ty) } else { self.ty_error(format!( "{name} refers to an instance in theory {}, expected theory {}", - d.theory, self.theory + i.theory, self.theory )) } } - TopDecl::Def(_) | TopDecl::DefConst(_) => { - self.ty_error(format!("{name} refers to a term not a type")) - } + TopDecl::Def(_) => self.ty_error(format!("{name} refers to a term not a type")), } } else { self.ty_error(format!("no such type {name} defined")) @@ -988,8 +994,19 @@ impl<'a> Elaborator<'a> { } else if let Some(d) = self.toplevel.lookup(name) { match d { TopDecl::Type(_) => self.syn_error(format!("{name} refers type, not term")), - TopDecl::DefConst(d) => (TmS::topvar(name), d.val.clone(), d.ty.clone()), + // A nullary `Def` (a closed term, e.g. `tt : Unit`) used as a + // bare name; evaluate its body and return type in the empty + // context. + TopDecl::Def(d) if d.args.is_empty() => { + let def = d.clone(); + let eval = self.evaluator(); + (TmS::topapp(name, vec![]), eval.eval_tm(&def.body), eval.eval_ty(&def.ret_ty)) + } TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")), + TopDecl::Instance(_) => self.syn_error(format!( + "{name} refers to an instance; use it in type position to import it, \ + not as a term" + )), } } else { self.syn_error(format!("no such variable {name}")) @@ -1002,16 +1019,25 @@ impl<'a> Elaborator<'a> { match n.ast0() { Var(name) => elab.lookup_tm(ustr(name)), App1(tm_n, L(_, Field(f))) => { - let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - let TyV_::Record(r) = &*ty_v else { - return elab.syn_error("can only project from record type"); - }; - if matches!(&*tm_v, TmV_::Instance(_)) { + // A top-level instance has no term-position use, so projecting + // a field out of one would otherwise produce a confusing + // "not a term"/"not a record" cascade; catch it here with the + // targeted elimination message. + if let Var(inst) = tm_n.ast0() + && matches!( + elab.toplevel.declarations.get(&name_seg(*inst)), + Some(TopDecl::Instance(_)) + ) + { return elab.syn_error( "cannot project a field out of an instance; an instance is \ eliminated by mapping out of it, not by projection", ); } + let (tm_s, tm_v, ty_v) = elab.syn(tm_n); + let TyV_::Record(r) = &*ty_v else { + return elab.syn_error("can only project from record type"); + }; let label = label_seg(*f); let f = name_seg(*f); if !r.fields.has(f) { @@ -1038,16 +1064,21 @@ impl<'a> Elaborator<'a> { elab.apply_codomain_morphism(f, inner_s, inner_v, inner_ty, x) } App1(L(_, Var(f)), L(_, App1(recv_n, L(_, Field(arg_field))))) => { - let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); - let TyV_::Record(r) = &*recv_ty else { - return elab.syn_error("can only project from record type"); - }; - if matches!(&*recv_v, TmV_::Instance(_)) { + if let Var(inst) = recv_n.ast0() + && matches!( + elab.toplevel.declarations.get(&name_seg(*inst)), + Some(TopDecl::Instance(_)) + ) + { return elab.syn_error( "cannot project a field out of an instance; an instance is \ eliminated by mapping out of it, not by projection", ); } + let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); + let TyV_::Record(r) = &*recv_ty else { + return elab.syn_error("can only project from record type"); + }; let arg_name = name_seg(*arg_field); let arg_label = label_seg(*arg_field); if !r.fields.has(arg_name) { diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 585a8496a..60db72548 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -1,7 +1,9 @@ //! Data structures for managing toplevel declarations in the type theory. //! -//! Specifically, notebooks will produce [TopDecl::Type] declarations, or -//! [TopDecl::DefConst] declarations. +//! The three kinds mirror the comprehension category of `D`-models: a [Type] +//! is a model (a context, i.e. an object of the base), a [Def] is a tight +//! transformation (a substitution, i.e. a morphism of the base), and an +//! [Instance] is an object of a fiber (a type in context). use derive_more::Constructor; @@ -13,10 +15,10 @@ use crate::zero::QualifiedName; pub enum TopDecl { /// See [Type]. Type(Type), - /// See [DefConst]. - DefConst(DefConst), /// See [Def]. Def(Def), + /// See [Instance]. + Instance(Instance), } /// A toplevel declaration of a type. @@ -33,31 +35,28 @@ pub struct Type { pub val: TyV, } -/// A toplevel declaration of a term in the empty context. +/// A toplevel declaration of an instance of a model. /// -/// Also stores the evaluation of that term, and the evaluation of the -/// corresponding type of that term. Because this is an evaluation in the empty -/// context, this is OK to use in any other context as well. +/// An instance is an object of the fiber over its codomain model `X` in the +/// comprehension category of `D`-models: a generator/equation/sub-instance +/// body packaged as the presentation of an `X`-instance. It is declared with +/// `instance NAME : X := [...]`. /// -/// An *instance* of a model is just such a term whose [`val`](Self::val) is a -/// [`TmV_::Instance`]: a generator/equation/ -/// sub-instance body packaged as an introduction value of a record type. -/// Both kinds arise as a `DefConst`, but from different surface declarations: -/// `def NAME : T := ` for a plain term, `instance NAME : T := [...]` for -/// an instance. -/// When an instance name is used in *type* position (for a -/// sub-instance import), its type is the record type synthesized from that body -/// by [`synth_instance_body_ty`](super::eval::Evaluator::synth_instance_body_ty). +/// When an instance name is used in *type* position (for a sub-instance +/// import), its type is the representable record type synthesized from that +/// body by +/// [`synth_instance_body_ty`](super::eval::Evaluator::synth_instance_body_ty), +/// whose terms are the instance morphisms out of it. #[derive(Constructor, Clone)] -pub struct DefConst { - /// The theory that the constant is defined in. +pub struct Instance { + /// The theory that the instance is defined in. pub theory: Theory, - /// The syntax of the constant (unnormalized). + /// The syntax of the instance body (unnormalized). pub stx: TmS, - /// The value of the constant (normalized). + /// The value of the instance body (normalized); always a [`TmV_::Instance`]. pub val: TmV, - /// The type of the constant. - pub ty: TyV, + /// The codomain model `X` that this is an instance of. + pub codomain: TyV, } /// A toplevel declaration of a term judgment. @@ -87,17 +86,6 @@ impl TopDecl { } } - /// Unwraps the term for a toplevel declaration of a term, or panics. - /// - /// This should only be used after type checking, when we know that a toplevel - /// variable name does in fact point to a toplevel declaration for a term. - pub fn unwrap_const(self) -> DefConst { - match self { - TopDecl::DefConst(d) => d, - _ => panic!("top-level should be a constant declaration"), - } - } - /// Unwraps the definition for a toplevel term judgment, or panics. pub fn unwrap_def(self) -> Def { match self { From f988bb297cdb6481605daac698798ac6da7c1e5b Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 29 Jun 2026 13:26:44 -0700 Subject: [PATCH 34/53] tt: hold instance codomain as a real context variable, not a side channel --- .../examples/tt/text/test_instances.dbltt | 7 ++++ .../tt/text/test_instances.dbltt.snapshot | 21 +++++++++-- packages/catlog/src/tt/context.rs | 14 +------- packages/catlog/src/tt/text_elab.rs | 35 +++++++++++++++---- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 5af2eeb4c..81873593e 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -40,5 +40,12 @@ instance I3 : WeightedGraph := [ E := [e1, e2] ] +instance SelfNamed : WeightedGraph := [ + V := [self, v2], + E := [e], + src(e) := self, + tgt(e) := v2 +] + #(should_fail) syn I.E \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index b1a8d9946..07f4542ea 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -77,12 +77,27 @@ instance I3 : WeightedGraph := [ #/ e1 : E #/ e2 : E +instance SelfNamed : WeightedGraph := [ + V := [self, v2], + E := [e], + src(e) := self, + tgt(e) := v2 +] +#/ declared: SelfNamed +#/ instance generators: +#/ self : V +#/ v2 : V +#/ e : E +#/ instance equations: +#/ src(e) == self +#/ tgt(e) == v2 + #(should_fail) syn I.E #/ result: ?0 : ?1 #/ expected errors: #/ error[elab]: cannot project a field out of an instance; an instance is eliminated by mapping out of it, not by projection -#/ --> examples/tt/text/test_instances.dbltt:44:5 -#/ 44| syn I.E -#/ 44| ^^^ +#/ --> examples/tt/text/test_instances.dbltt:51:5 +#/ 51| syn I.E +#/ 51| ^^^ diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 2d31d7510..0e313d478 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -27,18 +27,12 @@ pub struct Context { pub env: Env, /// Stores the names and types of each of the variables in context. pub scope: Vec, - /// The codomain model of the instance body currently being - /// elaborated, if any. Its fields are the model's generators, - /// looked up by name — a separate namespace from `scope`, but - /// scoped state restored alongside it. - pub codomain: Option>, } /// A checkpoint that we can return the context to. pub struct ContextCheckpoint { env: Env, scope: usize, - codomain: Option>, } impl Default for Context { @@ -50,11 +44,7 @@ impl Default for Context { impl Context { /// Create an empty context. pub fn new() -> Self { - Self { - env: Env::Nil, - scope: Vec::new(), - codomain: None, - } + Self { env: Env::Nil, scope: Vec::new() } } /// Create a checkpoint from the current state of the context. @@ -62,7 +52,6 @@ impl Context { ContextCheckpoint { env: self.env.clone(), scope: self.scope.len(), - codomain: self.codomain.clone(), } } @@ -70,7 +59,6 @@ impl Context { pub fn reset_to(&mut self, c: ContextCheckpoint) { self.env = c.env; self.scope.truncate(c.scope); - self.codomain = c.codomain; } /// Add a new variable to scope (note: does not add it to the environment). diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 12549002b..ae8fbd621 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -312,11 +312,27 @@ impl<'a> Elaborator<'a> { } } + /// Reserved name under which an instance's codomain model is bound + /// as a context variable (see [`Self::instance_body`]). It contains + /// a space, so the lexer — which restricts identifiers to + /// alphanumerics and `_` — can never produce it; hence a + /// user-declared generator, sub-instance, or field can never shadow + /// the codomain binding. + const CODOMAIN_BINDER: &'static str = "instance self"; + /// The codomain model of the instance body currently being /// elaborated, if any. Its fields are the codomain's generators, /// looked up by name by the instance-clause arms. + /// + /// The model is held as a record variable in the context under the + /// reserved [`Self::CODOMAIN_BINDER`] name (see + /// [`Self::instance_body`]). fn instance_codomain(&self) -> Option> { - self.ctx.codomain.clone() + let (_, _, ty) = self.ctx.lookup(name_seg(Self::CODOMAIN_BINDER))?; + match &*ty? { + TyV_::Record(r) => Some(Rc::new(r.clone())), + _ => None, + } } fn theory(&self) -> &TheoryDef { @@ -464,13 +480,20 @@ impl<'a> Elaborator<'a> { /// / [`TmV_::Instance`] pair whose payload is the instance's /// generator slots, equation witnesses, and sub-instance imports. /// - /// The codomain is set on the context (and restored on exit) so - /// that generator (fiber) clauses and applied-codomain-morphism - /// syntax inside the body resolve their generators by name. + /// The codomain model is bound into the context as a `self`-typed + /// record variable (and the binding is dropped on exit) so that + /// generator (fiber) clauses and applied-codomain-morphism syntax + /// inside the body resolve their generators by name, against a real + /// context variable. Introducing `self` + /// at the bottom of the context leaves every generator's index + /// unchanged, and the codomain is never referenced by variable (only + /// by stored path), so this perturbs no index arithmetic. fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (TmS, TmV) { - let saved = self.ctx.codomain.replace(Rc::new(codomain.clone())); + let c = self.checkpoint(); + let binder = name_seg(Self::CODOMAIN_BINDER); + self.intro(binder, label_seg(Self::CODOMAIN_BINDER), Some(TyV::record(codomain.clone()))); let result = self.instance_body_inner(n); - self.ctx.codomain = saved; + self.reset_to(c); result } From 426684c0ddaba5c0a8e8248fa6a0a48d0facbfee Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 29 Jun 2026 13:27:55 -0700 Subject: [PATCH 35/53] tt: rename TyS/TyV -> BaseTyS/BaseTyV --- packages/catlog-wasm/src/model.rs | 2 +- packages/catlog/src/tt/context.rs | 6 +- packages/catlog/src/tt/eval.rs | 148 ++++++++++++------------ packages/catlog/src/tt/mod.rs | 2 +- packages/catlog/src/tt/modelgen.rs | 28 ++--- packages/catlog/src/tt/notebook_elab.rs | 90 ++++++++------ packages/catlog/src/tt/stx.rs | 90 +++++++------- packages/catlog/src/tt/text_elab.rs | 140 ++++++++++++---------- packages/catlog/src/tt/toplevel.rs | 10 +- packages/catlog/src/tt/val.rs | 94 +++++++-------- packages/catlog/src/tt/wd.rs | 16 +-- 11 files changed, 327 insertions(+), 299 deletions(-) diff --git a/packages/catlog-wasm/src/model.rs b/packages/catlog-wasm/src/model.rs index 7dae0666c..8ea21705c 100644 --- a/packages/catlog-wasm/src/model.rs +++ b/packages/catlog-wasm/src/model.rs @@ -321,7 +321,7 @@ pub struct DblModel { /// The elaborated type for the model. #[wasm_bindgen(skip)] - pub ty: Option<(tt::stx::TyS, tt::val::TyV)>, + pub ty: Option<(tt::stx::BaseTyS, tt::val::BaseTyV)>, /// The namespace for the objects. #[wasm_bindgen(skip)] diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index 0e313d478..ad1598f29 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -18,7 +18,7 @@ pub struct VarInContext { /// /// We allow the type to be null as a hack for the `self` variable before we /// know the type of the `self` variable. - pub ty: Option, + pub ty: Option, } /// The variable context during elaboration. @@ -62,12 +62,12 @@ impl Context { } /// Add a new variable to scope (note: does not add it to the environment). - pub fn push_scope(&mut self, name: VarName, label: LabelSegment, ty: Option) { + pub fn push_scope(&mut self, name: VarName, label: LabelSegment, ty: Option) { self.scope.push(VarInContext::new(name, label, ty)) } /// Lookup a variable by name. - pub fn lookup(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, Option)> { + pub fn lookup(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, Option)> { self.scope .iter() .rev() diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index b39c15108..828c5ff6e 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -39,7 +39,7 @@ impl<'a> Evaluator<'a> { Self { env, ..self.clone() } } - fn eval_record(&self, fields: &Row) -> RecordV { + fn eval_record(&self, fields: &Row) -> RecordV { RecordV::new(self.env.clone(), fields.clone(), Dtry::empty()) } @@ -47,9 +47,9 @@ impl<'a> Evaluator<'a> { /// /// Assumes that the type syntax is well-formed and well-scoped with respect /// to self.env. - pub fn eval_ty(&self, ty: &TyS) -> TyV { + pub fn eval_ty(&self, ty: &BaseTyS) -> BaseTyV { match &**ty { - TyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { + BaseTyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), // An instance used in type position evaluates to the // representable record type synthesized from its body (see @@ -60,22 +60,22 @@ impl<'a> Evaluator<'a> { }, _ => panic!("top-level {tv} should be a type or instance declaration"), }, - TyS_::Object(ot) => TyV::object(ot.clone()), - TyS_::Morphism(pt, dom, cod) => { - TyV::morphism(pt.clone(), self.eval_tm(dom), self.eval_tm(cod)) + BaseTyS_::Object(ot) => BaseTyV::object(ot.clone()), + BaseTyS_::Morphism(pt, dom, cod) => { + BaseTyV::morphism(pt.clone(), self.eval_tm(dom), self.eval_tm(cod)) } - TyS_::Record(r) => TyV::record(self.eval_record(r)), - TyS_::Sing(ty_s, tm_s) => TyV::sing(self.eval_ty(ty_s), self.eval_tm(tm_s)), - TyS_::Id(ty_s, tm_s1, tm_s2) => { - TyV::id(self.eval_ty(ty_s), self.eval_tm(tm_s1), self.eval_tm(tm_s2)) + BaseTyS_::Record(r) => BaseTyV::record(self.eval_record(r)), + BaseTyS_::Sing(ty_s, tm_s) => BaseTyV::sing(self.eval_ty(ty_s), self.eval_tm(tm_s)), + BaseTyS_::Id(ty_s, tm_s1, tm_s2) => { + BaseTyV::id(self.eval_ty(ty_s), self.eval_tm(tm_s1), self.eval_tm(tm_s2)) } - TyS_::Specialize(ty_s, specializations) => { + BaseTyS_::Specialize(ty_s, specializations) => { specializations.iter().fold(self.eval_ty(ty_s), |ty_v, (path, s)| { ty_v.add_specialization(path, self.eval_ty(s)) }) } - TyS_::Meta(mv) => TyV::meta(*mv), - TyS_::Over(path) => TyV::over(path.clone()), + BaseTyS_::Meta(mv) => BaseTyV::meta(*mv), + BaseTyS_::Over(path) => BaseTyV::over(path.clone()), } } @@ -131,9 +131,9 @@ impl<'a> Evaluator<'a> { } /// Evaluate the type of the field `field_name` of `val : ty`. - pub fn field_ty(&self, ty: &TyV, val: &TmV, field_name: FieldName) -> TyV { + pub fn field_ty(&self, ty: &BaseTyV, val: &TmV, field_name: FieldName) -> BaseTyV { match &**ty { - TyV_::Record(r) => { + BaseTyV_::Record(r) => { let field_ty_s = r.fields.get(field_name).unwrap(); let orig_field_ty = self.with_env(r.env.snoc(val.clone())).eval_ty(field_ty_s); match r.specializations.entry(&field_name) { @@ -147,7 +147,7 @@ impl<'a> Evaluator<'a> { } /// Bind a new neutral of type `ty`. - pub fn bind_neu(&self, name: VarName, label: LabelSegment, ty: TyV) -> (TmN, Self) { + pub fn bind_neu(&self, name: VarName, label: LabelSegment, ty: BaseTyV) -> (TmN, Self) { let n = TmN::var(self.scope_length.into(), name, label); let v = TmV::neu(n.clone(), ty); ( @@ -161,7 +161,7 @@ impl<'a> Evaluator<'a> { } /// Bind a variable called "self" to `ty`. - pub fn bind_self(&self, ty: TyV) -> (TmN, Self) { + pub fn bind_self(&self, ty: BaseTyV) -> (TmN, Self) { self.bind_neu("self".into(), "self".into(), ty) } @@ -181,10 +181,10 @@ impl<'a> Evaluator<'a> { /// Because these `Id` fields are inert, the *concrete* way of mapping out /// of `D` — building a record literal (`Cons`) of this type by hand — only /// currently works when `D` is equation-free. - pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> TyV { - let mut fields: Row = Row::empty(); + pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> BaseTyV { + let mut fields: Row = Row::empty(); for (name, (label, path)) in &body.generators { - fields.insert(*name, *label, TyS::over(path.clone())); + fields.insert(*name, *label, BaseTyS::over(path.clone())); } for (name, (label, sub_v)) in &body.sub_instances { let TmV_::Instance(sub_body) = &**sub_v else { @@ -201,19 +201,19 @@ impl<'a> Evaluator<'a> { // so a placeholder suffices. Quoting in `ev` // (one binder deeper) sends `self` to de Bruijn index 0, matching // how `field_ty` snocs the record value when projecting. - let (self_n, ev) = self.bind_self(TyV::empty_record()); + let (self_n, ev) = self.bind_self(BaseTyV::empty_record()); for (i, (lhs_v, rhs_v)) in body.equations.iter().enumerate() { let Some(over_ty) = fiber_equation_ty(lhs_v) else { unreachable!("instance equation LHS is not a fiber element"); }; let lhs_s = ev.quote_tm(&reroot_at_self(lhs_v, &self_n, body)); let rhs_s = ev.quote_tm(&reroot_at_self(rhs_v, &self_n, body)); - let eq_ty = TyS::id(ev.quote_ty(&over_ty), lhs_s, rhs_s); + let eq_ty = BaseTyS::id(ev.quote_ty(&over_ty), lhs_s, rhs_s); let key = format!("_eq{i}"); fields.insert(name_seg(key.as_str()), label_seg(key.as_str()), eq_ty); } let r_v = RecordV::new(self.env.clone(), fields, Dtry::empty()); - TyV::record(r_v) + BaseTyV::record(r_v) } /// Produce type syntax from a type value. @@ -221,27 +221,27 @@ impl<'a> Evaluator<'a> { /// This is a *section* of eval, in that `self.eval_ty(self.quote_ty(ty_v)) == ty_v` /// but it is not necessarily true that `self.quote_ty(self.eval_ty(ty_s)) == ty_v`. /// - /// This is used for displaying [TyV] to the user in type errors, and for + /// This is used for displaying [BaseTyV] to the user in type errors, and for /// creating syntax that can be re-evaluated in other contexts. In theory this /// could be used for conversion checking, but it's more efficient to implement /// that directly, and it's better to *not* do eta-expansion for user-facing /// messages or for syntax that is meant to be re-evaluated. - pub fn quote_ty(&self, ty: &TyV) -> TyS { + pub fn quote_ty(&self, ty: &BaseTyV) -> BaseTyS { match &**ty { - TyV_::Object(object_type) => TyS::object(object_type.clone()), - TyV_::Morphism(morphism_type, dom, cod) => { - TyS::morphism(morphism_type.clone(), self.quote_tm(dom), self.quote_tm(cod)) + BaseTyV_::Object(object_type) => BaseTyS::object(object_type.clone()), + BaseTyV_::Morphism(morphism_type, dom, cod) => { + BaseTyS::morphism(morphism_type.clone(), self.quote_tm(dom), self.quote_tm(cod)) } - TyV_::Record(r) => { + BaseTyV_::Record(r) => { let r_eval = self.with_env(r.env.clone()).bind_self(ty.clone()).1; let fields = r .fields .map(|ty_s| self.bind_self(ty.clone()).1.quote_ty(&r_eval.eval_ty(ty_s))); - let record_ty_s = TyS::record(fields); + let record_ty_s = BaseTyS::record(fields); if r.specializations.is_empty() { record_ty_s } else { - TyS::specialize( + BaseTyS::specialize( record_ty_s, r.specializations .flatten() @@ -259,12 +259,12 @@ impl<'a> Evaluator<'a> { ) } } - TyV_::Sing(ty, tm) => TyS::sing(self.quote_ty(ty), self.quote_tm(tm)), - TyV_::Id(ty, tm1, tm2) => { - TyS::id(self.quote_ty(ty), self.quote_tm(tm1), self.quote_tm(tm2)) + BaseTyV_::Sing(ty, tm) => BaseTyS::sing(self.quote_ty(ty), self.quote_tm(tm)), + BaseTyV_::Id(ty, tm1, tm2) => { + BaseTyS::id(self.quote_ty(ty), self.quote_tm(tm1), self.quote_tm(tm2)) } - TyV_::Meta(mv) => TyS::meta(*mv), - TyV_::Over(path) => TyS::over(path.clone()), + BaseTyV_::Meta(mv) => BaseTyS::meta(*mv), + BaseTyV_::Over(path) => BaseTyS::over(path.clone()), } } @@ -314,7 +314,7 @@ impl<'a> Evaluator<'a> { /// /// This is true iff `ty1` is convertible with `ty2`, and an eta-expanded /// neutral of type `ty1` is an element of `ty2`. - pub fn subtype<'b>(&self, ty1: &TyV, ty2: &TyV) -> Result<(), D<'b>> { + pub fn subtype<'b>(&self, ty1: &BaseTyV, ty2: &BaseTyV) -> Result<(), D<'b>> { self.convertible_ty(ty1, ty2)?; let (n, _) = self.bind_self(ty1.clone()); let v = self.eta_neu(&n, ty1); @@ -329,20 +329,20 @@ impl<'a> Evaluator<'a> { /// /// Example: if `a : Entity` and `b : Entity` are neutrals, then `a` is not an /// element of `@sing b`, but `a` is an element of `@sing a`. - pub fn element_of<'b>(&self, tm: &TmV, ty: &TyV) -> Result<(), D<'b>> { + pub fn element_of<'b>(&self, tm: &TmV, ty: &BaseTyV) -> Result<(), D<'b>> { match &**ty { - TyV_::Object(_) => Ok(()), - TyV_::Morphism(_, _, _) => Ok(()), - TyV_::Record(r) => { + BaseTyV_::Object(_) => Ok(()), + BaseTyV_::Morphism(_, _, _) => Ok(()), + BaseTyV_::Record(r) => { for (name, (label, _)) in r.fields.iter() { self.element_of(&self.proj(tm, *name, *label), &self.field_ty(ty, tm, *name))? } Ok(()) } - TyV_::Sing(_, x) => self.equal_tm(tm, x), - TyV_::Id(_, _, _) => Ok(()), - TyV_::Meta(_) => Ok(()), - TyV_::Over(_) => Ok(()), + BaseTyV_::Sing(_, x) => self.equal_tm(tm, x), + BaseTyV_::Id(_, _, _) => Ok(()), + BaseTyV_::Meta(_) => Ok(()), + BaseTyV_::Over(_) => Ok(()), } } @@ -351,16 +351,16 @@ impl<'a> Evaluator<'a> { /// Ignores specializations: specializations are handled in [`Evaluator::subtype`]. /// /// On failure, returns a doc which describes the obstruction to convertibility. - pub fn convertible_ty<'b>(&self, ty1: &TyV, ty2: &TyV) -> Result<(), D<'b>> { + pub fn convertible_ty<'b>(&self, ty1: &BaseTyV, ty2: &BaseTyV) -> Result<(), D<'b>> { match (&**ty1, &**ty2) { - (TyV_::Object(ot1), TyV_::Object(ot2)) => { + (BaseTyV_::Object(ot1), BaseTyV_::Object(ot2)) => { if ot1 == ot2 { Ok(()) } else { Err(t(format!("object types {ot1} and {ot2} are not equal"))) } } - (TyV_::Morphism(mt1, dom1, cod1), TyV_::Morphism(mt2, dom2, cod2)) => { + (BaseTyV_::Morphism(mt1, dom1, cod1), BaseTyV_::Morphism(mt2, dom2, cod2)) => { if mt1 != mt2 { return Err(t(format!("morphism types {mt1} and {mt2} are not equal"))); } @@ -368,7 +368,7 @@ impl<'a> Evaluator<'a> { self.equal_tm(cod1, cod2).map_err(|d| t("could not convert codomains: ") + d)?; Ok(()) } - (TyV_::Record(r1), TyV_::Record(r2)) => { + (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) => { let mut fields = IndexMap::new(); let mut self1 = self.clone(); for ((name, (label, field_ty1_s)), (_, (_, field_ty2_s))) in @@ -384,9 +384,9 @@ impl<'a> Evaluator<'a> { } Ok(()) } - (TyV_::Sing(ty1, _), _) => self.convertible_ty(ty1, ty2), - (_, TyV_::Sing(ty2, _)) => self.convertible_ty(ty1, ty2), - (TyV_::Over(p1), TyV_::Over(p2)) => { + (BaseTyV_::Sing(ty1, _), _) => self.convertible_ty(ty1, ty2), + (_, BaseTyV_::Sing(ty2, _)) => self.convertible_ty(ty1, ty2), + (BaseTyV_::Over(p1), BaseTyV_::Over(p2)) => { if p1 == p2 { Ok(()) } else { @@ -398,11 +398,11 @@ impl<'a> Evaluator<'a> { } /// Performs eta-expansion of the neutral `n` at type `ty`. - pub fn eta_neu(&self, n: &TmN, ty: &TyV) -> TmV { + pub fn eta_neu(&self, n: &TmN, ty: &BaseTyV) -> TmV { match &**ty { - TyV_::Object(_) => TmV::neu(n.clone(), ty.clone()), - TyV_::Morphism(_, _, _) => TmV::neu(n.clone(), ty.clone()), - TyV_::Record(r) => { + BaseTyV_::Object(_) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Morphism(_, _, _) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Record(r) => { let mut fields = Row::empty(); for (name, (label, _)) in r.fields.iter() { let ty_v = self.field_ty(ty, &TmV::cons(fields.clone()), *name); @@ -411,15 +411,15 @@ impl<'a> Evaluator<'a> { } TmV::cons(fields) } - TyV_::Sing(_, x) => x.clone(), - TyV_::Id(_, _, _) => TmV::empty_cons(), // Extensional equality at a 100% discount! - TyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), - TyV_::Over(_) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Sing(_, x) => x.clone(), + BaseTyV_::Id(_, _, _) => TmV::empty_cons(), // Extensional equality at a 100% discount! + BaseTyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Over(_) => TmV::neu(n.clone(), ty.clone()), } } /// Performs eta-expansion of the term `v` at type `ty`. - pub fn eta(&self, v: &TmV, ty: Option<&TyV>) -> TmV { + pub fn eta(&self, v: &TmV, ty: Option<&BaseTyV>) -> TmV { match &**v { TmV_::Neu(tm_n, ty_v) => self.eta_neu(tm_n, ty_v), TmV_::App(name, x) => TmV::app(*name, self.eta(x, None)), @@ -554,10 +554,10 @@ impl<'a> Evaluator<'a> { fn can_specialize( &self, - ty: &TyV, + ty: &BaseTyV, val: &TmV, path: &[(FieldName, LabelSegment)], - field_ty: TyV, + field_ty: BaseTyV, ) -> Result<(), String> { assert!(!path.is_empty()); let orig_field_ty = self.path_ty(ty, val, path)?; @@ -578,14 +578,14 @@ impl<'a> Evaluator<'a> { /// current type to be a record containing the named field. pub fn path_ty( &self, - ty: &TyV, + ty: &BaseTyV, val: &TmV, path: &[(FieldName, LabelSegment)], - ) -> Result { + ) -> Result { let mut ty = ty.clone(); let mut val = val.clone(); for &(name, label) in path { - let TyV_::Record(r) = &*ty.clone() else { + let BaseTyV_::Record(r) = &*ty.clone() else { return Err(format!("expected a record type at .{label}")); }; if !r.fields.has(name) { @@ -604,17 +604,17 @@ impl<'a> Evaluator<'a> { /// Precondition: `path` is non-empty. pub fn try_specialize( &self, - ty: &TyV, + ty: &BaseTyV, path: &[(FieldName, LabelSegment)], - field_ty: TyV, - ) -> Result { + field_ty: BaseTyV, + ) -> Result { let (self_var, _) = self.bind_self(ty.clone()); let self_val = self.eta_neu(&self_var, ty); self.can_specialize(ty, &self_val, path, field_ty.clone())?; - let TyV_::Record(r) = &**ty else { + let BaseTyV_::Record(r) = &**ty else { panic!("Input to `try_specialize` should be a record type") }; - Ok(TyV::record(r.add_specialization(path, field_ty))) + Ok(BaseTyV::record(r.add_specialization(path, field_ty))) } } @@ -622,10 +622,10 @@ impl<'a> Evaluator<'a> { /// /// Fiber equations (`mor(gen) := target`) relate `Over`-typed terms; their /// common type is recoverable from the left-hand side. -fn fiber_equation_ty(tm: &TmV) -> Option { +fn fiber_equation_ty(tm: &TmV) -> Option { match &**tm { - TmV_::OverApp(_, _, tgt_path, _) => Some(TyV::over(tgt_path.clone())), - TmV_::Neu(_, ty) => matches!(&**ty, TyV_::Over(_)).then(|| ty.clone()), + TmV_::OverApp(_, _, tgt_path, _) => Some(BaseTyV::over(tgt_path.clone())), + TmV_::Neu(_, ty) => matches!(&**ty, BaseTyV_::Over(_)).then(|| ty.clone()), _ => None, } } diff --git a/packages/catlog/src/tt/mod.rs b/packages/catlog/src/tt/mod.rs index 76a637ba4..7c7da95b6 100644 --- a/packages/catlog/src/tt/mod.rs +++ b/packages/catlog/src/tt/mod.rs @@ -19,7 +19,7 @@ //! | | Syntax | Value | //! |------|--------|-------| //! | Term | [TmS] | [TmV] | -//! | Type | [TyS] | [TyV] | +//! | Type | [BaseTyS] | [BaseTyV] | //! //! Evaluation is the process of going from syntax to values. Evaluation is used to //! *normalize types*. We need to normalize types because there are many different diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 60d1fc28d..f076763f5 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -94,7 +94,7 @@ impl Model { /// Generates a model from a type. /// /// Precondition: `ty` must be valid in the empty context. - pub fn from_ty(toplevel: &Toplevel, th: &TheoryDef, ty: &TyV) -> (Self, Namespace) { + pub fn from_ty(toplevel: &Toplevel, th: &TheoryDef, ty: &BaseTyV) -> (Self, Namespace) { let mut generator = ModelGenerator::new(toplevel, th); let namespace = generator.generate(ty); (generator.model, namespace) @@ -218,7 +218,7 @@ impl<'a> ModelGenerator<'a> { Self { eval, theory, model } } - fn generate(&mut self, ty: &TyV) -> Namespace { + fn generate(&mut self, ty: &BaseTyV) -> Namespace { let tm_n; (tm_n, self.eval) = self.eval.bind_self(ty.clone()); let tm_v = self.eval.eta_neu(&tm_n, ty); @@ -285,7 +285,7 @@ impl<'a> ModelGenerator<'a> { fn make_ob_synth_type(&self, val: &TmV) -> Option<(Ob, ObType)> { match &**val { TmV_::Neu(n, ty_v) => { - let TyV_::Object(ob_type) = &**ty_v else { + let BaseTyV_::Object(ob_type) = &**ty_v else { return None; }; let name = n.to_qualified_name(); @@ -332,7 +332,7 @@ impl<'a> ModelGenerator<'a> { fn synth_mor(&self, val: &TmV) -> Option<(Mor, MorType)> { match &**val { TmV_::Neu(n, ty_v) => { - let TyV_::Morphism(mor_type, _, _) = &**ty_v else { + let BaseTyV_::Morphism(mor_type, _, _) = &**ty_v else { return None; }; let name = n.to_qualified_name(); @@ -361,19 +361,19 @@ impl<'a> ModelGenerator<'a> { (mt == *mor_type).then_some(mor) } - fn extract(&mut self, prefix: Vec, val: &TmV, ty: &TyV) -> Option { + fn extract(&mut self, prefix: Vec, val: &TmV, ty: &BaseTyV) -> Option { match &**ty { - TyV_::Object(ot) => { + BaseTyV_::Object(ot) => { self.model.add_ob(prefix.into(), ot.clone()); None } - TyV_::Morphism(mt, dom, cod) => { + BaseTyV_::Morphism(mt, dom, cod) => { let dom = self.make_ob_check_type(dom, &self.theory.src_type(mt))?; let cod = self.make_ob_check_type(cod, &self.theory.tgt_type(mt))?; self.model.add_mor(prefix.into(), dom, cod, mt.clone()); None } - TyV_::Record(r) => { + BaseTyV_::Record(r) => { let mut namespace = Namespace::new_for_uuid(); for (name, (label, _)) in r.fields.iter() { let mut prefix = prefix.clone(); @@ -389,9 +389,9 @@ impl<'a> ModelGenerator<'a> { } Some(namespace) } - TyV_::Sing(_, _) => None, - TyV_::Id(mor_ty, lhs, rhs) => { - let TyV_::Morphism(mt, _, _) = &**mor_ty else { + BaseTyV_::Sing(_, _) => None, + BaseTyV_::Id(mor_ty, lhs, rhs) => { + let BaseTyV_::Morphism(mt, _, _) = &**mor_ty else { return None; }; if let (Some(lhs), Some(rhs)) = (self.make_mor(lhs, mt), self.make_mor(rhs, mt)) { @@ -399,8 +399,8 @@ impl<'a> ModelGenerator<'a> { } None } - TyV_::Meta(_) => None, - TyV_::Over(_) => None, + BaseTyV_::Meta(_) => None, + BaseTyV_::Over(_) => None, } } } @@ -642,7 +642,7 @@ instance UseLoop : WeightedGraph := [ }; let eval = Evaluator::empty(&toplevel); let body_ty = eval.synth_instance_body_ty(loop_body); - let TyV_::Record(r) = &*body_ty else { + let BaseTyV_::Record(r) = &*body_ty else { panic!("synthesized instance type should be a record"); }; assert!(r.fields.get(name_seg("e")).is_some(), "generator field e"); diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 2f68687eb..64458477c 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -67,10 +67,10 @@ impl<'a> Elaborator<'a> { Evaluator::new(self.toplevel, self.ctx.env.clone(), self.ctx.scope.len()) } - fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { + fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { let v = TmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), - ty.clone().unwrap_or(TyV::empty_record()), + ty.clone().unwrap_or(BaseTyV::empty_record()), ); let v = if ty.is_some() { self.evaluator().eta(&v, ty.as_ref()) @@ -88,10 +88,10 @@ impl<'a> Elaborator<'a> { MetaVar::new(Some(self.ref_id), i) } - fn ty_error(&mut self, error: InvalidDblModel) -> (TyS, TyV) { + fn ty_error(&mut self, error: InvalidDblModel) -> (BaseTyS, BaseTyV) { self.errors.push(error); let ty_m = self.fresh_meta(); - (TyS::meta(ty_m), TyV::meta(ty_m)) + (BaseTyS::meta(ty_m), BaseTyV::meta(ty_m)) } fn ob_type(&mut self, ob_type: &nb::ObType) -> Option { @@ -102,29 +102,32 @@ impl<'a> Elaborator<'a> { } } - fn object_cell(&mut self, ob_decl: &nb::ObDecl) -> (NameSegment, LabelSegment, TyS, TyV) { + fn object_cell( + &mut self, + ob_decl: &nb::ObDecl, + ) -> (NameSegment, LabelSegment, BaseTyS, BaseTyV) { let name = NameSegment::Uuid(ob_decl.id); let label = LabelSegment::Text(ustr(&ob_decl.name)); let (ty_s, ty_v) = match self.ob_type(&ob_decl.ob_type) { - Some(ob_type) => (TyS::object(ob_type.clone()), TyV::object(ob_type)), + Some(ob_type) => (BaseTyS::object(ob_type.clone()), BaseTyV::object(ob_type)), None => self.ty_error(InvalidDblModel::ObType(QualifiedName::single(name))), }; (name, label, ty_s, ty_v) } - fn lookup_tm(&self, name: VarName) -> Option<(TmS, TmV, TyV)> { + fn lookup_tm(&self, name: VarName) -> Option<(TmS, TmV, BaseTyV)> { let (i, label, ty) = self.ctx.lookup(name)?; let v = self.ctx.env.get(*i).unwrap().clone(); Some((TmS::var(i, name, label), v, ty.clone().unwrap())) } - fn resolve_name(&self, segments: &[VarName]) -> Option<(TmS, TmV, TyV)> { + fn resolve_name(&self, segments: &[VarName]) -> Option<(TmS, TmV, BaseTyV)> { let (&last, rest) = segments.split_last()?; if rest.is_empty() { self.lookup_tm(last) } else { let (tm_s, tm_v, ty_v) = self.resolve_name(rest)?; - let TyV_::Record(r) = &*ty_v else { + let BaseTyV_::Record(r) = &*ty_v else { return None; }; let &(label, _) = r.fields.get_with_label(last)?; @@ -141,7 +144,7 @@ impl<'a> Elaborator<'a> { nb::Ob::Basic(name) => { let name = QualifiedName::deserialize_str(name).unwrap(); let (stx, val, ty) = self.resolve_name(name.as_slice())?; - let TyV_::Object(ob_type) = &*ty else { + let BaseTyV_::Object(ob_type) = &*ty else { return None; }; Some((stx, val, ob_type.clone())) @@ -157,7 +160,7 @@ impl<'a> Elaborator<'a> { } nb::Ob::Tabulated(mor) => { let (mor_stx, mor_val, mor_ty) = self.mor_syn(mor)?; - let TyV_::Morphism(mt, _, _) = &*mor_ty else { + let BaseTyV_::Morphism(mt, _, _) = &*mor_ty else { return None; }; let ob_type = self.theory().tabulator(mt.clone())?; @@ -167,12 +170,12 @@ impl<'a> Elaborator<'a> { } } - fn mor_syn(&self, n: &nb::Mor) -> Option<(TmS, TmV, TyV)> { + fn mor_syn(&self, n: &nb::Mor) -> Option<(TmS, TmV, BaseTyV)> { match n { nb::Mor::Basic(name) => { let name = QualifiedName::deserialize_str(name).unwrap(); let (stx, val, ty) = self.resolve_name(name.as_slice())?; - let TyV_::Morphism(..) = &*ty else { + let BaseTyV_::Morphism(..) = &*ty else { return None; }; Some((stx, val, ty)) @@ -181,7 +184,7 @@ impl<'a> Elaborator<'a> { nb::path::Path::Id(ob) => { let (stx, val, ob_type) = self.ob_syn(ob)?; let mor_type = self.theory().hom_type(ob_type)?; - Some((stx, val.clone(), TyV::morphism(mor_type, val.clone(), val.clone()))) + Some((stx, val.clone(), BaseTyV::morphism(mor_type, val.clone(), val.clone()))) } nb::path::Path::Seq(ms) => match ms.as_slice() { [] => None, @@ -190,10 +193,11 @@ impl<'a> Elaborator<'a> { let (stx_first, val_first, type_first) = self.mor_syn(first)?; let rest = nb::Mor::Composite(Box::new(nb::path::Path::Seq(rest.to_vec()))); let (stx_rest, val_rest, type_rest) = self.mor_syn(&rest)?; - let TyV_::Morphism(mt_first, dom_first, cod_first) = &*type_first else { + let BaseTyV_::Morphism(mt_first, dom_first, cod_first) = &*type_first + else { unreachable!() }; - let TyV_::Morphism(mt_rest, dom_rest, cod_rest) = &*type_rest else { + let BaseTyV_::Morphism(mt_rest, dom_rest, cod_rest) = &*type_rest else { unreachable!() }; if mt_first != mt_rest { @@ -207,7 +211,11 @@ impl<'a> Elaborator<'a> { Some(( stx, val, - TyV::morphism(mt_first.clone(), dom_first.clone(), cod_rest.clone()), + BaseTyV::morphism( + mt_first.clone(), + dom_first.clone(), + cod_rest.clone(), + ), )) } }, @@ -243,7 +251,7 @@ impl<'a> Elaborator<'a> { } } - fn morphism_cell_ty(&mut self, mor_decl: &nb::MorDecl) -> (TyS, TyV) { + fn morphism_cell_ty(&mut self, mor_decl: &nb::MorDecl) -> (BaseTyS, BaseTyV) { let id = QualifiedName::from(mor_decl.id); let (mor_type, dom_ty, cod_ty) = match &mor_decl.mor_type { nb::MorType::Basic(name) => { @@ -275,19 +283,22 @@ impl<'a> Elaborator<'a> { return self.ty_error(InvalidDblModel::CodType(id)); }; ( - TyS::morphism(mor_type.clone(), dom_s, cod_s), - TyV::morphism(mor_type, dom_v, cod_v), + BaseTyS::morphism(mor_type.clone(), dom_s, cod_s), + BaseTyV::morphism(mor_type, dom_v, cod_v), ) } - fn morphism_cell(&mut self, mor_decl: &nb::MorDecl) -> (NameSegment, LabelSegment, TyS, TyV) { + fn morphism_cell( + &mut self, + mor_decl: &nb::MorDecl, + ) -> (NameSegment, LabelSegment, BaseTyS, BaseTyV) { let name = NameSegment::Uuid(mor_decl.id); let label = LabelSegment::Text(ustr(&mor_decl.name)); let (ty_s, ty_v) = self.morphism_cell_ty(mor_decl); (name, label, ty_s, ty_v) } - fn equation_cell_ty(&mut self, eqn_decl: &nb::EqnDecl) -> (TyS, TyV) { + fn equation_cell_ty(&mut self, eqn_decl: &nb::EqnDecl) -> (BaseTyS, BaseTyV) { let (lhs_m, rhs_m) = match (&eqn_decl.lhs, &eqn_decl.rhs) { (Some(lhs), Some(rhs)) => (lhs, rhs), _ => { @@ -312,10 +323,10 @@ impl<'a> Elaborator<'a> { }; if let (Some((_, _, lhs_ty)), Some((_, _, rhs_ty))) = (&lhs, &rhs) { - let TyV_::Morphism(mt_lhs, dom_lhs, cod_lhs) = &**lhs_ty else { + let BaseTyV_::Morphism(mt_lhs, dom_lhs, cod_lhs) = &**lhs_ty else { unreachable!() }; - let TyV_::Morphism(mt_rhs, dom_rhs, cod_rhs) = &**rhs_ty else { + let BaseTyV_::Morphism(mt_rhs, dom_rhs, cod_rhs) = &**rhs_ty else { unreachable!() }; if mt_lhs != mt_rhs { @@ -331,8 +342,8 @@ impl<'a> Elaborator<'a> { } match (NonEmpty::from_vec(errors), lhs, rhs) { (None, Some((lhs_s, lhs_v, lhs_ty)), Some((rhs_s, rhs_v, _))) => { - let ty_s = TyS::id(self.evaluator().quote_ty(&lhs_ty), lhs_s, rhs_s); - let ty_v = TyV::id(lhs_ty, lhs_v, rhs_v); + let ty_s = BaseTyS::id(self.evaluator().quote_ty(&lhs_ty), lhs_s, rhs_s); + let ty_v = BaseTyV::id(lhs_ty, lhs_v, rhs_v); (ty_s, ty_v) } (Some(errors), _, _) => { @@ -346,7 +357,10 @@ impl<'a> Elaborator<'a> { } } - fn equation_cell(&mut self, eqn_decl: &nb::EqnDecl) -> (NameSegment, LabelSegment, TyS, TyV) { + fn equation_cell( + &mut self, + eqn_decl: &nb::EqnDecl, + ) -> (NameSegment, LabelSegment, BaseTyS, BaseTyV) { // Kind of funny that the decl's id produces the cell's name // but the decl's name produces the cell's label. let name = NameSegment::Uuid(eqn_decl.id); @@ -355,7 +369,7 @@ impl<'a> Elaborator<'a> { (name, label, ty_s, ty_v) } - fn instantiation_cell_ty(&mut self, i_decl: &nb::InstantiatedModel) -> (TyS, TyV) { + fn instantiation_cell_ty(&mut self, i_decl: &nb::InstantiatedModel) -> (BaseTyS, BaseTyV) { let name = QualifiedName::single(NameSegment::Uuid(i_decl.id)); let link = match &i_decl.model { Some(l) => l, @@ -373,7 +387,7 @@ impl<'a> Elaborator<'a> { return self.ty_error(InvalidDblModel::InvalidLink(name)); } let mut specializations = Vec::new(); - let TyV_::Record(r) = &*type_def.val else { + let BaseTyV_::Record(r) = &*type_def.val else { return self.ty_error(InvalidDblModel::InvalidLink(name)); }; let mut r = r.clone(); @@ -387,7 +401,7 @@ impl<'a> Elaborator<'a> { continue; }; match &**field_ty { - TyS_::Object(expected_ob_ty) => { + BaseTyS_::Object(expected_ob_ty) => { if &ob_type != expected_ob_ty { continue; } @@ -398,26 +412,26 @@ impl<'a> Elaborator<'a> { } specializations.push(( vec![(field_name, *field_label)], - TyS::sing(TyS::object(ob_type.clone()), ob_s), + BaseTyS::sing(BaseTyS::object(ob_type.clone()), ob_s), )); r = r.add_specialization( &[(field_name, *field_label)], - TyV::sing(TyV::object(ob_type), ob_v), + BaseTyV::sing(BaseTyV::object(ob_type), ob_v), ) } } let ty_s = if specializations.is_empty() { - TyS::topvar(topname) + BaseTyS::topvar(topname) } else { - TyS::specialize(TyS::topvar(topname), specializations) + BaseTyS::specialize(BaseTyS::topvar(topname), specializations) }; - (ty_s, TyV::record(r)) + (ty_s, BaseTyV::record(r)) } fn instantiation_cell( &mut self, i_decl: &nb::InstantiatedModel, - ) -> (NameSegment, LabelSegment, TyS, TyV) { + ) -> (NameSegment, LabelSegment, BaseTyS, BaseTyV) { let name = NameSegment::Uuid(i_decl.id); let label = LabelSegment::Text(ustr(&i_decl.name)); let (ty_s, ty_v) = self.instantiation_cell_ty(i_decl); @@ -428,7 +442,7 @@ impl<'a> Elaborator<'a> { pub fn notebook<'b>( &mut self, cells: impl Iterator, - ) -> (TyS, TyV) { + ) -> (BaseTyS, BaseTyV) { // Process the cells in dependency order. This is important because the // UI allows users to reorder cells freely and that shouldn't affect the // result of elaboration. @@ -463,7 +477,7 @@ impl<'a> Elaborator<'a> { .map(|(name, (label, ty_v))| (*name, (*label, self.evaluator().quote_ty(ty_v)))) .collect(); let r_v = RecordV::new(self.ctx.env.clone(), field_tys.clone(), Dtry::empty()); - (TyS::record(field_tys), TyV::record(r_v)) + (BaseTyS::record(field_tys), BaseTyV::record(r_v)) } } diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 72e079a1e..18cd10622 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -27,8 +27,8 @@ impl fmt::Display for MetaVar { } } -/// Inner enum for [TyS]. -pub enum TyS_ { +/// Inner enum for [BaseTyS]. +pub enum BaseTyS_ { /// A reference to a top-level declaration. TopVar(TopVarName), /// Type constructor for object types. @@ -54,7 +54,7 @@ pub enum TyS_ { /// /// A term `x` of type `Record(r)` represents a record where field `f` has type /// `eval(env.snoc(eval(env, x)), r.fields1[f])`. - Record(Row), + Record(Row), /// Type constructor for singleton types. /// @@ -62,14 +62,14 @@ pub enum TyS_ { /// /// A term `x` of type `Sing(ty, tm)` is a term of `ty` that is convertible with /// `tm`. - Sing(TyS, TmS), + Sing(BaseTyS, TmS), /// Type constructor for identity types. /// /// Example syntax: `a == b` (assuming `a` and `b` are terms that synthesize the same type). /// /// A term `p` of type `a == b` is a proof that `a` and `b` are equal. - Id(TyS, TmS, TmS), + Id(BaseTyS, TmS, TmS), /// Type constructor for specialized types. /// @@ -80,7 +80,7 @@ pub enum TyS_ { /// /// In order to form this type, it must be the case that `d[p]` is a subtype of /// the type of the field at path `p`. - Specialize(TyS, Vec<(Vec<(FieldName, LabelSegment)>, TyS)>), + Specialize(BaseTyS, Vec<(Vec<(FieldName, LabelSegment)>, BaseTyS)>), /// A metavar. /// @@ -104,86 +104,86 @@ pub enum TyS_ { Over(Vec<(FieldName, LabelSegment)>), } -/// Syntax for total types, dereferences to [TyS_]. +/// Syntax for total types, dereferences to [BaseTyS_]. /// /// See [crate::tt] for an explanation of what total types are, and for an /// explanation of our approach to Rc pointers in abstract syntax trees. #[derive(Clone, Deref)] #[deref(forward)] -pub struct TyS(Rc); +pub struct BaseTyS(Rc); -impl TyS { - /// Smart constructor for [TyS], [TyS_::TopVar] case. +impl BaseTyS { + /// Smart constructor for [BaseTyS], [BaseTyS_::TopVar] case. pub fn topvar(name: TopVarName) -> Self { - Self(Rc::new(TyS_::TopVar(name))) + Self(Rc::new(BaseTyS_::TopVar(name))) } - /// Smart constructor for [TyS], [TyS_::Object] case. + /// Smart constructor for [BaseTyS], [BaseTyS_::Object] case. pub fn object(object_type: ObType) -> Self { - Self(Rc::new(TyS_::Object(object_type))) + Self(Rc::new(BaseTyS_::Object(object_type))) } - /// Smart constructor for [TyS], [TyS_::Morphism] case. + /// Smart constructor for [BaseTyS], [BaseTyS_::Morphism] case. pub fn morphism(morphism_type: MorType, dom: TmS, cod: TmS) -> Self { - Self(Rc::new(TyS_::Morphism(morphism_type, dom, cod))) + Self(Rc::new(BaseTyS_::Morphism(morphism_type, dom, cod))) } - /// Smart constructor for [TyS], [TyS_::Record] case. - pub fn record(fields: Row) -> Self { - Self(Rc::new(TyS_::Record(fields))) + /// Smart constructor for [BaseTyS], [BaseTyS_::Record] case. + pub fn record(fields: Row) -> Self { + Self(Rc::new(BaseTyS_::Record(fields))) } - /// Smart constructor for [TyS], [TyS_::Sing] case. - pub fn sing(ty: TyS, tm: TmS) -> Self { - Self(Rc::new(TyS_::Sing(ty, tm))) + /// Smart constructor for [BaseTyS], [BaseTyS_::Sing] case. + pub fn sing(ty: BaseTyS, tm: TmS) -> Self { + Self(Rc::new(BaseTyS_::Sing(ty, tm))) } - /// Smart constructor for [TyS], [TyS_::Id] case. - pub fn id(ty: TyS, tm1: TmS, tm2: TmS) -> Self { - Self(Rc::new(TyS_::Id(ty, tm1, tm2))) + /// Smart constructor for [BaseTyS], [BaseTyS_::Id] case. + pub fn id(ty: BaseTyS, tm1: TmS, tm2: TmS) -> Self { + Self(Rc::new(BaseTyS_::Id(ty, tm1, tm2))) } - /// Smart constructor for [TyS], [TyS_::Specialize] case. + /// Smart constructor for [BaseTyS], [BaseTyS_::Specialize] case. pub fn specialize( - ty: TyS, - specializations: Vec<(Vec<(FieldName, LabelSegment)>, TyS)>, + ty: BaseTyS, + specializations: Vec<(Vec<(FieldName, LabelSegment)>, BaseTyS)>, ) -> Self { - Self(Rc::new(TyS_::Specialize(ty, specializations))) + Self(Rc::new(BaseTyS_::Specialize(ty, specializations))) } - /// Smart constructor for [TyS], [TyS_::Meta] case. + /// Smart constructor for [BaseTyS], [BaseTyS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { - Self(Rc::new(TyS_::Meta(mv))) + Self(Rc::new(BaseTyS_::Meta(mv))) } - /// Smart constructor for [TyS], [TyS_::Over] case. + /// Smart constructor for [BaseTyS], [BaseTyS_::Over] case. pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(TyS_::Over(path))) + Self(Rc::new(BaseTyS_::Over(path))) } } -impl ToDoc for TyS { +impl ToDoc for BaseTyS { fn to_doc<'a>(&self) -> D<'a> { match &**self { - TyS_::TopVar(name) => t(format!("{}", name)), - TyS_::Object(ob_type) => t(format!("{}", ob_type)), - TyS_::Morphism(mor_type, dom, cod) => { + BaseTyS_::TopVar(name) => t(format!("{}", name)), + BaseTyS_::Object(ob_type) => t(format!("{}", ob_type)), + BaseTyS_::Morphism(mor_type, dom, cod) => { mor_type.to_doc().parens() + tuple([dom.to_doc(), cod.to_doc()]) } - TyS_::Record(fields) => tuple(fields.iter().map(|(_, (label, ty))| { + BaseTyS_::Record(fields) => tuple(fields.iter().map(|(_, (label, ty))| { binop(t(":"), t(format!("{}", label)).group(), ty.to_doc()) })), - TyS_::Sing(_, tm) => t("@sing") + s() + tm.to_doc(), - TyS_::Id(_, tm1, tm2) => binop(t("=="), tm1.to_doc(), tm2.to_doc()), - TyS_::Specialize(ty, d) => binop( + BaseTyS_::Sing(_, tm) => t("@sing") + s() + tm.to_doc(), + BaseTyS_::Id(_, tm1, tm2) => binop(t("=="), tm1.to_doc(), tm2.to_doc()), + BaseTyS_::Specialize(ty, d) => binop( t("&"), ty.to_doc(), tuple( d.iter().map(|(name, ty)| binop(t(":"), t(path_to_string(name)), ty.to_doc())), ), ), - TyS_::Meta(mv) => t(format!("?{}", mv.id)), - TyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), + BaseTyS_::Meta(mv) => t(format!("?{}", mv.id)), + BaseTyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), } } } @@ -221,7 +221,7 @@ fn instance_body_to_doc<'a>(body: &InstanceBodyS) -> D<'a> { tuple(gens.chain(subs).chain(eqns)) } -impl fmt::Display for TyS { +impl fmt::Display for BaseTyS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_doc().group().pretty()) } @@ -254,7 +254,7 @@ pub enum TmS_ { /// List of objects. List(Vec), /// Application of a codomain morphism to a fiber-typed - /// ([`TyS_::Over`]) term. Only well-formed inside an instance body. + /// ([`BaseTyS_::Over`]) term. Only well-formed inside an instance body. /// /// Arguments, in order: /// 1. `mor` — name of the codomain morphism being applied @@ -300,7 +300,7 @@ pub enum TmS_ { /// generator's fiber it lives over. Generators are introduced by /// surface set-literal clauses `field := [...]` in the instance body. /// - `equations` is a list of `(lhs, rhs)` pairs over fiber -/// ([`TyS_::Over`]) types. +/// ([`BaseTyS_::Over`]) types. /// - `sub_instances` maps each sub-instance import's local name to a /// nested instance term. This is what surface `we : Edge` lowers to. #[derive(Default)] diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index ae8fbd621..de26d910a 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -204,7 +204,7 @@ impl TopElaborator { let (_, ret_ty_v) = elab.ty(ty_n); // An instance body is checked against its codomain model, a // record type. - let TyV_::Record(r) = &*ret_ty_v else { + let BaseTyV_::Record(r) = &*ret_ty_v else { return self .error(tn.loc, "an instance must be declared against a record type"); }; @@ -330,7 +330,7 @@ impl<'a> Elaborator<'a> { fn instance_codomain(&self) -> Option> { let (_, _, ty) = self.ctx.lookup(name_seg(Self::CODOMAIN_BINDER))?; match &*ty? { - TyV_::Record(r) => Some(Rc::new(r.clone())), + BaseTyV_::Record(r) => Some(Rc::new(r.clone())), _ => None, } } @@ -370,23 +370,23 @@ impl<'a> Elaborator<'a> { None } - fn ty_hole(&mut self) -> (TyS, TyV) { + fn ty_hole(&mut self) -> (BaseTyS, BaseTyV) { let ty_m = self.fresh_meta(); - (TyS::meta(ty_m), TyV::meta(ty_m)) + (BaseTyS::meta(ty_m), BaseTyV::meta(ty_m)) } - fn ty_error(&mut self, msg: impl Into) -> (TyS, TyV) { + fn ty_error(&mut self, msg: impl Into) -> (BaseTyS, BaseTyV) { self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); self.ty_hole() } - fn syn_hole(&mut self) -> (TmS, TmV, TyV) { + fn syn_hole(&mut self) -> (TmS, TmV, BaseTyV) { let tm_m = self.fresh_meta(); let ty_m = self.fresh_meta(); - (TmS::meta(tm_m), TmV::meta(tm_m), TyV::meta(ty_m)) + (TmS::meta(tm_m), TmV::meta(tm_m), BaseTyV::meta(ty_m)) } - fn syn_error(&mut self, msg: impl Into) -> (TmS, TmV, TyV) { + fn syn_error(&mut self, msg: impl Into) -> (TmS, TmV, BaseTyV) { self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); self.syn_hole() } @@ -405,10 +405,10 @@ impl<'a> Elaborator<'a> { Evaluator::new(self.toplevel, self.ctx.env.clone(), self.ctx.scope.len()) } - fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { + fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { let v = TmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), - ty.clone().unwrap_or(TyV::empty_record()), + ty.clone().unwrap_or(BaseTyV::empty_record()), ); let v = if ty.is_some() { self.evaluator().eta(&v, ty.as_ref()) @@ -423,8 +423,8 @@ impl<'a> Elaborator<'a> { /// The unit type, elaborated as the empty record — i.e. the empty /// model. `Unit` and `tt` are surface sugar for the empty record type /// and its unique element, the empty cons `[]`. - fn empty_record_ty(&self) -> (TyS, TyV) { - (TyS::record(Row::empty()), TyV::empty_record()) + fn empty_record_ty(&self) -> (BaseTyS, BaseTyV) { + (BaseTyS::record(Row::empty()), BaseTyV::empty_record()) } /// Apply a codomain morphism `f` to an already-elaborated argument @@ -435,14 +435,14 @@ impl<'a> Elaborator<'a> { f: &str, arg_s: TmS, arg_v: TmV, - arg_ty: TyV, + arg_ty: BaseTyV, arg_label_str: &str, - ) -> (TmS, TmV, TyV) { + ) -> (TmS, TmV, BaseTyV) { let Some(codomain) = self.instance_codomain() else { return self .syn_error("applied codomain morphism is only allowed inside an instance body"); }; - let TyV_::Over(src_path) = &*arg_ty else { + let BaseTyV_::Over(src_path) = &*arg_ty else { let quoted = self.evaluator().quote_ty(&arg_ty); return self.syn_error(format!( "argument {arg_label_str} has type {quoted}, expected a fiber type", @@ -453,7 +453,7 @@ impl<'a> Elaborator<'a> { let Some(mor_ty_s) = codomain.fields.get(f_name) else { return self.syn_error(format!("no such codomain morphism {f_name}")); }; - let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + let BaseTyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { return self.syn_error(format!("codomain field {f_name} is not a morphism")); }; let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) else { @@ -470,7 +470,7 @@ impl<'a> Elaborator<'a> { ( TmS::over_app(f_name, f_label, cod_path.clone(), arg_s), TmV::over_app(f_name, f_label, cod_path.clone(), arg_v), - TyV::over(cod_path), + BaseTyV::over(cod_path), ) } @@ -491,7 +491,11 @@ impl<'a> Elaborator<'a> { fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (TmS, TmV) { let c = self.checkpoint(); let binder = name_seg(Self::CODOMAIN_BINDER); - self.intro(binder, label_seg(Self::CODOMAIN_BINDER), Some(TyV::record(codomain.clone()))); + self.intro( + binder, + label_seg(Self::CODOMAIN_BINDER), + Some(BaseTyV::record(codomain.clone())), + ); let result = self.instance_body_inner(n); self.reset_to(c); result @@ -554,11 +558,11 @@ impl<'a> Elaborator<'a> { let label = label_seg(name_str); let (_, ty_v) = elab.ty(ty_n); match &*ty_v { - TyV_::Over(path) => { + BaseTyV_::Over(path) => { gens.insert(n_seg, (label, path.clone())); elab.intro(n_seg, label, Some(ty_v)); } - TyV_::Record(_) => { + BaseTyV_::Record(_) => { let (sub_s, sub_v) = match ty_n.ast0() { Var(sub_name) => { let topvar = name_seg(*sub_name); @@ -588,8 +592,8 @@ impl<'a> Elaborator<'a> { subs_v.insert(n_seg, (label, sub_v)); elab.intro(n_seg, label, Some(ty_v)); } - TyV_::Id(eq_ty, lhs, rhs) => { - if !matches!(&**eq_ty, TyV_::Over(_)) { + BaseTyV_::Id(eq_ty, lhs, rhs) => { + if !matches!(&**eq_ty, BaseTyV_::Over(_)) { let quoted = elab.evaluator().quote_ty(eq_ty); elab.error::<()>(format!( "instance equation {name_str} is over {quoted}, but an \ @@ -640,7 +644,7 @@ impl<'a> Elaborator<'a> { failed = true; continue; }; - let TyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + let BaseTyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { elab.error::<()>(format!( "mapping-literal assignment requires field {field_name} to be \ morphism-typed", @@ -664,7 +668,7 @@ impl<'a> Elaborator<'a> { unreachable!("guard ensured all entries are `:=` clauses"); }; let (key_s, key_v, key_ty) = elab.syn(key_n); - let TyV_::Over(key_path) = &*key_ty else { + let BaseTyV_::Over(key_path) = &*key_ty else { let quoted = elab.evaluator().quote_ty(&key_ty); elab.error::<()>(format!( "mapping-literal key has type {quoted}, expected an element over {}", @@ -682,7 +686,7 @@ impl<'a> Elaborator<'a> { entry_failed = true; break; } - let lhs_ty = TyV::over(cod_path.clone()); + let lhs_ty = BaseTyV::over(cod_path.clone()); let lhs_s = TmS::over_app(f_seg, f_label, cod_path.clone(), key_s); let lhs_v = TmV::over_app(f_seg, f_label, cod_path.clone(), key_v); let (rhs_s, rhs_v) = elab.chk(&lhs_ty, target_n); @@ -712,7 +716,7 @@ impl<'a> Elaborator<'a> { failed = true; continue; }; - if !matches!(&**field_ty_s, TyS_::Object(_)) { + if !matches!(&**field_ty_s, BaseTyS_::Object(_)) { elab.error::<()>(format!( "set-literal assignment requires field {field_name} to be \ object-typed", @@ -721,7 +725,7 @@ impl<'a> Elaborator<'a> { continue; } let path = vec![(f_seg, f_label)]; - let gen_ty = TyV::over(path.clone()); + let gen_ty = BaseTyV::over(path.clone()); for name_n in name_ns.iter() { let Var(gen_name) = name_n.ast0() else { elab.loc = Some(name_n.loc()); @@ -739,7 +743,7 @@ impl<'a> Elaborator<'a> { // an equation witness. App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { let (lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); - if !matches!(&*lhs_ty, TyV_::Over(_)) { + if !matches!(&*lhs_ty, BaseTyV_::Over(_)) { elab.loc = Some(lhs_n.loc()); elab.error::<()>( "mapping-entry clause `mor(arg) := target` requires the LHS \ @@ -785,7 +789,7 @@ impl<'a> Elaborator<'a> { (TmS::instance(body_s), TmV::instance(body_v)) } - fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, TyS, TyV)> { + fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, BaseTyS, BaseTyV)> { let mut elab = self.enter(n.loc()); match n.ast0() { App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { @@ -796,15 +800,15 @@ impl<'a> Elaborator<'a> { } } - fn lookup_ty(&mut self, name: VarName) -> (TyS, TyV) { + fn lookup_ty(&mut self, name: VarName) -> (BaseTyS, BaseTyV) { let qname = QualifiedName::single(name); if let Some(ob_type) = self.theory().basic_ob_type(qname) { - (TyS::object(ob_type.clone()), TyV::object(ob_type)) + (BaseTyS::object(ob_type.clone()), BaseTyV::object(ob_type)) } else if let Some(d) = self.toplevel.declarations.get(&name) { match d { TopDecl::Type(t) => { if t.theory == self.theory { - (TyS::topvar(name), t.val.clone()) + (BaseTyS::topvar(name), t.val.clone()) } else { self.ty_error(format!( "{name} refers to a type in theory {}, expected a type in theory {}", @@ -821,7 +825,7 @@ impl<'a> Elaborator<'a> { unreachable!("an Instance always has an instance body") }; let body_ty = self.evaluator().synth_instance_body_ty(body); - (TyS::topvar(name), body_ty) + (BaseTyS::topvar(name), body_ty) } else { self.ty_error(format!( "{name} refers to an instance in theory {}, expected theory {}", @@ -879,7 +883,10 @@ impl<'a> Elaborator<'a> { } #[allow(clippy::type_complexity)] - fn specialization(&mut self, n: &FNtn) -> Option<(Vec<(NameSegment, LabelSegment)>, TyS, TyV)> { + fn specialization( + &mut self, + n: &FNtn, + ) -> Option<(Vec<(NameSegment, LabelSegment)>, BaseTyS, BaseTyV)> { let mut elab = self.enter(n.loc()); match n.ast0() { App2(L(_, Keyword(":")), p_n, ty_n) => { @@ -890,21 +897,25 @@ impl<'a> Elaborator<'a> { App2(L(_, Keyword(":=")), p_n, tm_n) => { let p = elab.path(p_n)?; let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - Some((p, TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v))) + Some(( + p, + BaseTyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), + BaseTyV::sing(ty_v, tm_v), + )) } _ => elab.error("unexpected notation for specialization"), } } /// Elaborates a type from notation, returning both syntax and value. - pub fn ty(&mut self, n: &FNtn) -> (TyS, TyV) { + pub fn ty(&mut self, n: &FNtn) -> (BaseTyS, BaseTyV) { let mut elab = self.enter(n.loc()); match n.ast0() { Var(name) => elab.lookup_ty(name_seg(*name)), Keyword("Unit") => elab.empty_record_ty(), App1(L(_, Prim("sing")), tm_n) => { let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - (TyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), TyV::sing(ty_v, tm_v)) + (BaseTyS::sing(elab.evaluator().quote_ty(&ty_v), tm_s), BaseTyV::sing(ty_v, tm_v)) } App1(mt_n, L(_, Tuple(domcod_n))) => { let [dom_n, cod_n] = domcod_n.as_slice() else { @@ -913,12 +924,15 @@ impl<'a> Elaborator<'a> { let Some((mt, dom_ty, cod_ty)) = elab.morphism_ty(mt_n) else { return elab.ty_hole(); }; - let (dom_s, dom_v) = elab.chk(&TyV::object(dom_ty.clone()), dom_n); - let (cod_s, cod_v) = elab.chk(&TyV::object(cod_ty.clone()), cod_n); - (TyS::morphism(mt.clone(), dom_s, cod_s), TyV::morphism(mt.clone(), dom_v, cod_v)) + let (dom_s, dom_v) = elab.chk(&BaseTyV::object(dom_ty.clone()), dom_n); + let (cod_s, cod_v) = elab.chk(&BaseTyV::object(cod_ty.clone()), cod_n); + ( + BaseTyS::morphism(mt.clone(), dom_s, cod_s), + BaseTyV::morphism(mt.clone(), dom_v, cod_v), + ) } Tuple(field_ns) => { - let mut field_ty_vs = Vec::<(FieldName, (LabelSegment, TyV))>::new(); + let mut field_ty_vs = Vec::<(FieldName, (LabelSegment, BaseTyV))>::new(); let mut failed = false; let self_var = elab.intro(name_seg("self"), label_seg("self"), None).unwrap_neu(); let c = elab.checkpoint(); @@ -949,7 +963,7 @@ impl<'a> Elaborator<'a> { .map(|(name, (label, ty_v))| (*name, (*label, elab.evaluator().quote_ty(ty_v)))) .collect(); let r_v = RecordV::new(elab.ctx.env.clone(), field_tys.clone(), Dtry::empty()); - (TyS::record(field_tys), TyV::record(r_v)) + (BaseTyS::record(field_tys), BaseTyV::record(r_v)) } App2(L(_, Keyword("&")), ty_n, L(_, Tuple(specialization_ns))) => { let (ty_s, mut ty_v) = elab.ty(ty_n); @@ -976,12 +990,12 @@ impl<'a> Elaborator<'a> { } } } - (TyS::specialize(ty_s, specializations), ty_v) + (BaseTyS::specialize(ty_s, specializations), ty_v) } App2(L(_, Keyword("==")), tm1_n, tm2_n) => { let (tm1_s, tm1_v, tm1_ty) = elab.syn(tm1_n); let (tm2_s, tm2_v, tm2_ty) = elab.syn(tm2_n); - if !matches!(&*tm1_ty, TyV_::Morphism(_, _, _) | TyV_::Over(_)) { + if !matches!(&*tm1_ty, BaseTyV_::Morphism(_, _, _) | BaseTyV_::Over(_)) { elab.loc = Some(tm1_n.loc()); return elab .ty_error( @@ -997,15 +1011,15 @@ impl<'a> Elaborator<'a> { e.pretty() )); } - let eq_ty_s = TyS::id(elab.evaluator().quote_ty(&tm1_ty), tm1_s, tm2_s); - let eq_ty_v = TyV::id(tm1_ty, tm1_v, tm2_v); + let eq_ty_s = BaseTyS::id(elab.evaluator().quote_ty(&tm1_ty), tm1_s, tm2_s); + let eq_ty_v = BaseTyV::id(tm1_ty, tm1_v, tm2_v); (eq_ty_s, eq_ty_v) } _ => elab.ty_error("unexpected notation for type"), } } - fn lookup_tm(&mut self, name: Ustr) -> (TmS, TmV, TyV) { + fn lookup_tm(&mut self, name: Ustr) -> (TmS, TmV, BaseTyV) { let label = label_seg(name); let name = name_seg(name); if let Some((i, _, ty)) = self.ctx.lookup(name) { @@ -1037,7 +1051,7 @@ impl<'a> Elaborator<'a> { } /// Elaborates a term from notation, returning syntax, value, and synthesized type. - fn syn(&mut self, n: &FNtn) -> (TmS, TmV, TyV) { + fn syn(&mut self, n: &FNtn) -> (TmS, TmV, BaseTyV) { let mut elab = self.enter(n.loc()); match n.ast0() { Var(name) => elab.lookup_tm(ustr(name)), @@ -1058,7 +1072,7 @@ impl<'a> Elaborator<'a> { ); } let (tm_s, tm_v, ty_v) = elab.syn(tm_n); - let TyV_::Record(r) = &*ty_v else { + let BaseTyV_::Record(r) = &*ty_v else { return elab.syn_error("can only project from record type"); }; let label = label_seg(*f); @@ -1099,7 +1113,7 @@ impl<'a> Elaborator<'a> { ); } let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); - let TyV_::Record(r) = &*recv_ty else { + let BaseTyV_::Record(r) = &*recv_ty else { return elab.syn_error("can only project from record type"); }; let arg_name = name_seg(*arg_field); @@ -1114,7 +1128,7 @@ impl<'a> Elaborator<'a> { } App1(L(_, Prim("id")), ob_n) => { let (ob_s, ob_v, ob_t) = elab.syn(ob_n); - let TyV_::Object(ob_type) = &*ob_t else { + let BaseTyV_::Object(ob_type) = &*ob_t else { return elab.syn_error("can only apply @id to objects"); }; let Some(mor_type) = elab.theory().hom_type(ob_type.clone()) else { @@ -1123,18 +1137,18 @@ impl<'a> Elaborator<'a> { ( TmS::id(ob_s), TmV::id(ob_v.clone()), - TyV::morphism(mor_type, ob_v.clone(), ob_v), + BaseTyV::morphism(mor_type, ob_v.clone(), ob_v), ) } App1(L(_, Prim("tab")), mor_n) => { let (mor_s, mor_v, mor_t) = elab.syn(mor_n); - let TyV_::Morphism(mor_type, _, _) = &*mor_t else { + let BaseTyV_::Morphism(mor_type, _, _) = &*mor_t else { return elab.syn_error("can only apply @tab to morphisms"); }; let Some(ob_type) = elab.theory().tabulator(mor_type.clone()) else { return elab.syn_error("theory does not have tabulators"); }; - (TmS::tab(mor_s), TmV::tab(mor_v.clone()), TyV::object(ob_type)) + (TmS::tab(mor_s), TmV::tab(mor_v.clone()), BaseTyV::object(ob_type)) } App1(L(_, Prim(name)), ob_n) => { let name = name_seg(*name); @@ -1143,18 +1157,18 @@ impl<'a> Elaborator<'a> { return elab.syn_error(format!("operation @{name} not in theory {th_name}")); }; let dom = elab.theory().ob_op_dom(&ob_op); - let (arg_s, arg_v) = elab.chk(&TyV::object(dom), ob_n); + let (arg_s, arg_v) = elab.chk(&BaseTyV::object(dom), ob_n); let cod = elab.theory().ob_op_cod(&ob_op); - (TmS::ob_app(name, arg_s), TmV::app(name, arg_v), TyV::object(cod)) + (TmS::ob_app(name, arg_s), TmV::app(name, arg_v), BaseTyV::object(cod)) } App2(L(_, Keyword("*")), f_n, g_n) => { let (f_s, f_v, f_ty) = elab.syn(f_n); let (g_s, g_v, g_ty) = elab.syn(g_n); - let TyV_::Morphism(f_mt, f_dom, f_cod) = &*f_ty else { + let BaseTyV_::Morphism(f_mt, f_dom, f_cod) = &*f_ty else { elab.loc = Some(f_n.loc()); return elab.syn_error("expected a morphism"); }; - let TyV_::Morphism(g_mt, g_dom, g_cod) = &*g_ty else { + let BaseTyV_::Morphism(g_mt, g_dom, g_cod) = &*g_ty else { elab.loc = Some(g_n.loc()); return elab.syn_error("expected a morphism"); }; @@ -1175,7 +1189,7 @@ impl<'a> Elaborator<'a> { ( TmS::compose(f_s, g_s), TmV::compose(f_v, g_v), - TyV::morphism( + BaseTyV::morphism( elab.theory().compose_types2(f_mt.clone(), g_mt.clone()).unwrap(), f_dom.clone(), g_cod.clone(), @@ -1217,10 +1231,10 @@ impl<'a> Elaborator<'a> { } /// Elaborates a term from notation, checking against an expected type, and returning syntax and value. - fn chk(&mut self, ty: &TyV, n: &FNtn) -> (TmS, TmV) { + fn chk(&mut self, ty: &BaseTyV, n: &FNtn) -> (TmS, TmV) { let mut elab = self.enter(n.loc()); match (&**ty, n.ast0()) { - (TyV_::Record(r), Tuple(field_ns)) => { + (BaseTyV_::Record(r), Tuple(field_ns)) => { // Ordinary record construction (a tight transformation / // generalized element). Instance bodies are *not* dispatched // here — they are introduced by the `instance` keyword, which @@ -1258,11 +1272,11 @@ impl<'a> Elaborator<'a> { } (TmS::cons(field_stxs.into()), TmV::cons(field_vals.into())) } - (TyV_::Object(ob_type), Tuple(ob_ns)) => { + (BaseTyV_::Object(ob_type), Tuple(ob_ns)) => { let Some(ob_type) = ob_type.clone().list_arg() else { return elab.chk_error("expected to object type to be a list"); }; - let elem_ty_v = TyV::object(ob_type); + let elem_ty_v = BaseTyV::object(ob_type); let mut elem_stxs = Vec::new(); let mut elem_vals = Vec::new(); for ob_n in ob_ns { diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 60db72548..0f38a3b3f 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -30,9 +30,9 @@ pub struct Type { /// The theory for the type. pub theory: Theory, /// The syntax of the type (unnormalized). - pub stx: TyS, + pub stx: BaseTyS, /// The value of the type (normalized). - pub val: TyV, + pub val: BaseTyV, } /// A toplevel declaration of an instance of a model. @@ -56,7 +56,7 @@ pub struct Instance { /// The value of the instance body (normalized); always a [`TmV_::Instance`]. pub val: TmV, /// The codomain model `X` that this is an instance of. - pub codomain: TyV, + pub codomain: BaseTyV, } /// A toplevel declaration of a term judgment. @@ -65,10 +65,10 @@ pub struct Def { /// The theory that the definition is defined in. pub theory: Theory, /// The arguments for the definition. - pub args: Row, + pub args: Row, /// The return type of the definition (to be evaluated in an environment /// with values for the arguments). - pub ret_ty: TyS, + pub ret_ty: BaseTyS, /// The body of the definition (to be evaluated in an environment with /// values for the arguments). pub body: TmS, diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index 12e07ffc0..fd1b6e0c3 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -17,17 +17,17 @@ pub struct RecordV { /// The closed-over environment. pub env: Env, /// The types for the fields. - pub fields: Rc>, + pub fields: Rc>, /// Specializations of the fields. /// /// When we get to actually computing the type of fields, we will look here /// to see if they have been specialized. - pub specializations: Dtry, + pub specializations: Dtry, } impl RecordV { /// Construct a record type value. - pub fn new(env: Env, fields: Row, specializations: Dtry) -> Self { + pub fn new(env: Env, fields: Row, specializations: Dtry) -> Self { Self { env, fields: Rc::new(fields), @@ -38,7 +38,7 @@ impl RecordV { /// Add a specialization a path `path` to type `ty`. /// /// Precondition: assumes that this produces a subtype. - pub fn add_specialization(&self, path: &[(FieldName, LabelSegment)], ty: TyV) -> Self { + pub fn add_specialization(&self, path: &[(FieldName, LabelSegment)], ty: BaseTyV) -> Self { Self { specializations: merge_specializations( &self.specializations, @@ -51,7 +51,7 @@ impl RecordV { /// Merge in the specializations in `specializations`. /// /// Precondition: assumes that this produces a subtype. - pub fn specialize(&self, specializations: &Dtry) -> Self { + pub fn specialize(&self, specializations: &Dtry) -> Self { Self { specializations: merge_specializations(&self.specializations, specializations), ..self.clone() @@ -60,7 +60,7 @@ impl RecordV { } /// Merge new specializations with old specializations. -pub fn merge_specializations(old: &Dtry, new: &Dtry) -> Dtry { +pub fn merge_specializations(old: &Dtry, new: &Dtry) -> Dtry { let mut result: IndexMap<_, _> = old.entries().map(|(name, e)| (*name, e.clone())).collect(); for (field, entry) in new.entries() { let new_entry = match (old.entry(field), &entry.1) { @@ -76,63 +76,63 @@ pub fn merge_specializations(old: &Dtry, new: &Dtry) -> Dtry { result.into() } -/// Inner enum for [TyV]. -pub enum TyV_ { - /// Type constructor for object types, also see [TyS_::Object]. +/// Inner enum for [BaseTyV]. +pub enum BaseTyV_ { + /// Type constructor for object types, also see [BaseTyS_::Object]. Object(ObType), - /// Type constructor for morphism types, also see [TyS_::Morphism]. + /// Type constructor for morphism types, also see [BaseTyS_::Morphism]. Morphism(MorType, TmV, TmV), /// Type constructor for specialized record types. /// - /// This is the target of both [TyS_::Specialize] and [TyS_::Record]. - /// Specifically, [TyS_::Record] evaluates to `TyV_::Record(r)` with - /// `r.specializations = Dtry::empty()`, and then `TyS_::Specialize(ty, d)` will + /// This is the target of both [BaseTyS_::Specialize] and [BaseTyS_::Record]. + /// Specifically, [BaseTyS_::Record] evaluates to `BaseTyV_::Record(r)` with + /// `r.specializations = Dtry::empty()`, and then `BaseTyS_::Specialize(ty, d)` will /// add the specializations in `d` to the evaluation of `ty` (which must - /// evaluate to a value of form `TyV_::Record(_)`). + /// evaluate to a value of form `BaseTyV_::Record(_)`). Record(RecordV), - /// Type constructor for singleton types, also see [TyS_::Sing]. - Sing(TyV, TmV), - /// Type constructor for identity types, also see [TyS_::Id]. - Id(TyV, TmV, TmV), - /// A metavariable, also see [TyS_::Meta]. + /// Type constructor for singleton types, also see [BaseTyS_::Sing]. + Sing(BaseTyV, TmV), + /// Type constructor for identity types, also see [BaseTyS_::Id]. + Id(BaseTyV, TmV, TmV), + /// A metavariable, also see [BaseTyS_::Meta]. Meta(MetaVar), /// The type of terms in a fiber over an object generator of some /// instance's codomain model. /// - /// See [TyS_::Over] for the syntactic counterpart and explanation + /// See [BaseTyS_::Over] for the syntactic counterpart and explanation /// of why instance identity is not part of this type. Over(Vec<(FieldName, LabelSegment)>), } -/// Value for total types, dereferences to [TyV_]. +/// Value for total types, dereferences to [BaseTyV_]. #[derive(Clone, Deref)] #[deref(forward)] -pub struct TyV(Rc); +pub struct BaseTyV(Rc); -impl TyV { - /// Smart constructor for [TyV], [TyV_::Object] case. +impl BaseTyV { + /// Smart constructor for [BaseTyV], [BaseTyV_::Object] case. pub fn object(object_type: ObType) -> Self { - Self(Rc::new(TyV_::Object(object_type))) + Self(Rc::new(BaseTyV_::Object(object_type))) } - /// Smart constructor for [TyV], [TyV_::Morphism] case. + /// Smart constructor for [BaseTyV], [BaseTyV_::Morphism] case. pub fn morphism(morphism_type: MorType, dom: TmV, cod: TmV) -> Self { - Self(Rc::new(TyV_::Morphism(morphism_type, dom, cod))) + Self(Rc::new(BaseTyV_::Morphism(morphism_type, dom, cod))) } - /// Smart constructor for [TyV], [TyV_::Record] case. + /// Smart constructor for [BaseTyV], [BaseTyV_::Record] case. pub fn record(record_v: RecordV) -> Self { - Self(Rc::new(TyV_::Record(record_v))) + Self(Rc::new(BaseTyV_::Record(record_v))) } - /// Smart constructor for [TyV], [TyV_::Sing] case. - pub fn sing(ty_v: TyV, tm_v: TmV) -> Self { - Self(Rc::new(TyV_::Sing(ty_v, tm_v))) + /// Smart constructor for [BaseTyV], [BaseTyV_::Sing] case. + pub fn sing(ty_v: BaseTyV, tm_v: TmV) -> Self { + Self(Rc::new(BaseTyV_::Sing(ty_v, tm_v))) } - /// Smart constructor for [TyV], [TyV_::Id] case. - pub fn id(ty_v: TyV, tm_v1: TmV, tm_v2: TmV) -> Self { - Self(Rc::new(TyV_::Id(ty_v, tm_v1, tm_v2))) + /// Smart constructor for [BaseTyV], [BaseTyV_::Id] case. + pub fn id(ty_v: BaseTyV, tm_v1: TmV, tm_v2: TmV) -> Self { + Self(Rc::new(BaseTyV_::Id(ty_v, tm_v1, tm_v2))) } /// Compute the specialization of `self` by `specializations`. @@ -155,9 +155,9 @@ impl TyV { /// /// r3 and r3' should be represented in the same way, and r3, r3' and r3'' /// should all be equivalent. - pub fn specialize(&self, specializations: &Dtry) -> Self { + pub fn specialize(&self, specializations: &Dtry) -> Self { match &**self { - TyV_::Record(r) => TyV::record(r.specialize(specializations)), + BaseTyV_::Record(r) => BaseTyV::record(r.specialize(specializations)), _ => panic!("can only specialize a record type"), } } @@ -165,9 +165,9 @@ impl TyV { /// Specializes the field at `path` to `ty`. /// /// Precondition: assumes that this produces a subtype. - pub fn add_specialization(&self, path: &[(FieldName, LabelSegment)], ty: TyV) -> Self { + pub fn add_specialization(&self, path: &[(FieldName, LabelSegment)], ty: BaseTyV) -> Self { match &**self { - TyV_::Record(r) => TyV::record(r.add_specialization(path, ty)), + BaseTyV_::Record(r) => BaseTyV::record(r.add_specialization(path, ty)), _ => panic!("can only specialize a record type"), } } @@ -176,17 +176,17 @@ impl TyV { /// Also used as a throwaway type for /// untyped placeholder binders (whose type is discarded). pub fn empty_record() -> Self { - Self(Rc::new(TyV_::Record(RecordV::new(Env::nil(), Row::empty(), Dtry::empty())))) + Self(Rc::new(BaseTyV_::Record(RecordV::new(Env::nil(), Row::empty(), Dtry::empty())))) } - /// Smart constructor for [TyV], [TyV_::Meta] case. + /// Smart constructor for [BaseTyV], [BaseTyV_::Meta] case. pub fn meta(mv: MetaVar) -> Self { - Self(Rc::new(TyV_::Meta(mv))) + Self(Rc::new(BaseTyV_::Meta(mv))) } - /// Smart constructor for [TyV], [TyV_::Over] case. + /// Smart constructor for [BaseTyV], [BaseTyV_::Over] case. pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(TyV_::Over(path))) + Self(Rc::new(BaseTyV_::Over(path))) } } @@ -245,10 +245,10 @@ pub enum TmV_ { /// Neutrals. /// /// We store the type because we need it for eta-expansion. - Neu(TmN, TyV), + Neu(TmN, BaseTyV), /// Application of an object operation in the theory. App(VarName, TmV), - /// Application of a codomain morphism to an [`Over`-typed](TyV_::Over) + /// Application of a codomain morphism to an [`Over`-typed](BaseTyV_::Over) /// term. See [`TmS_::OverApp`] for the syntactic counterpart and /// argument-by-argument documentation. OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), @@ -277,7 +277,7 @@ pub struct TmV(Rc); impl TmV { /// Smart constructor for [TmV], [TmV_::Neu] case. - pub fn neu(n: TmN, ty: TyV) -> Self { + pub fn neu(n: TmN, ty: BaseTyV) -> Self { TmV(Rc::new(TmV_::Neu(n, ty))) } diff --git a/packages/catlog/src/tt/wd.rs b/packages/catlog/src/tt/wd.rs index 50bf4c300..68d3e95ee 100644 --- a/packages/catlog/src/tt/wd.rs +++ b/packages/catlog/src/tt/wd.rs @@ -22,8 +22,8 @@ use crate::zero::QualifiedName; /// to fields of arbitrary depth. In this function, any specializations more /// than one level deep are ignored. To capture these, one might look for a /// "nested UWD" data structure. -pub fn record_to_uwd(ty: &TyV) -> Option> { - let TyV_::Record(record_v) = &**ty else { +pub fn record_to_uwd(ty: &BaseTyV) -> Option> { + let BaseTyV_::Record(record_v) = &**ty else { return None; }; @@ -37,7 +37,7 @@ pub fn record_to_uwd(ty: &TyV) -> Option> { // First pass: add a box for each field that is itself of record type. for (field_name, (field_label, _)) in record_v.fields.iter() { let field_ty = eval.field_ty(ty, &tm_v, *field_name); - let TyV_::Record(r) = &&*field_ty else { + let BaseTyV_::Record(r) = &&*field_ty else { continue; }; uwd.add_box(*field_name, *field_label); @@ -49,10 +49,10 @@ pub fn record_to_uwd(ty: &TyV) -> Option> { // depth one can be expressed in a UWD. continue; }; - let TyV_::Sing(ty, tm) = &**spec_type else { + let BaseTyV_::Sing(ty, tm) = &**spec_type else { continue; }; - let (TyV_::Object(ob_type), TmV_::Neu(n, _)) = (&**ty, &**tm) else { + let (BaseTyV_::Object(ob_type), TmV_::Neu(n, _)) = (&**ty, &**tm) else { continue; }; let qual_name = n.to_qualified_name(); @@ -67,7 +67,7 @@ pub fn record_to_uwd(ty: &TyV) -> Option> { let field_ty = eval.field_ty(ty, &tm_v, *field_name); match &&*field_ty { // Add outer port for each top-level field that is a junction. - TyV_::Object(ob_type) => { + BaseTyV_::Object(ob_type) => { let qual_name = QualifiedName::single(*field_name); if uwd.has_junction(&qual_name) { uwd.add_outer_port(*field_name, *field_label, ob_type.clone()); @@ -75,7 +75,7 @@ pub fn record_to_uwd(ty: &TyV) -> Option> { } } // Add port to box for each sub-field that is a junction. - TyV_::Record(r) => { + BaseTyV_::Record(r) => { let tm_v = eval.proj(&tm_v, *field_name, *field_label); for (port_name, (port_label, _)) in r.fields.iter() { if uwd.has_port(*field_name, *port_name) { @@ -84,7 +84,7 @@ pub fn record_to_uwd(ty: &TyV) -> Option> { let qual_name: QualifiedName = [*field_name, *port_name].into(); if uwd.has_junction(&qual_name) { let port_ty = eval.field_ty(&field_ty, &tm_v, *port_name); - let TyV_::Object(ob_type) = &*port_ty else { + let BaseTyV_::Object(ob_type) = &*port_ty else { continue; }; uwd.add_port(*field_name, *port_name, *port_label, ob_type.clone()); From f87fc92b979ab6ee6fca455c0ea45f9cbc45be80 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 29 Jun 2026 15:23:24 -0700 Subject: [PATCH 36/53] tt: represent instances as fiber types (FiberTy/FiberTm) --- packages/catlog/src/tt/context.rs | 61 +++- packages/catlog/src/tt/eval.rs | 264 +++++----------- packages/catlog/src/tt/modelgen.rs | 154 +++++---- packages/catlog/src/tt/stx.rs | 262 +++++++++------- packages/catlog/src/tt/text_elab.rs | 469 +++++++++++++++------------- packages/catlog/src/tt/toplevel.rs | 19 +- packages/catlog/src/tt/val.rs | 141 ++++++--- 7 files changed, 722 insertions(+), 648 deletions(-) diff --git a/packages/catlog/src/tt/context.rs b/packages/catlog/src/tt/context.rs index ad1598f29..1be6f6593 100644 --- a/packages/catlog/src/tt/context.rs +++ b/packages/catlog/src/tt/context.rs @@ -21,18 +21,44 @@ pub struct VarInContext { pub ty: Option, } +/// Each *fiber* variable in context — a generator or sub-instance import +/// introduced inside an instance body — with its label and fiber type. +#[derive(Constructor)] +pub struct FiberVarInContext { + /// The name of the fiber variable. + pub name: VarName, + /// The label for the fiber variable. + pub label: LabelSegment, + /// The fiber type of the variable. + pub ty: FiberTyV, +} + /// The variable context during elaboration. +/// +/// Carries two scopes: the **base** context (`env`/`scope`) of ordinary +/// terms typed by [`BaseTyV`], and a separate **fiber** context +/// (`fiber_env`/`fiber_scope`) of instance generators and sub-instance +/// imports typed by [`FiberTyV`]. The fiber scope is populated only while +/// elaborating an instance body; the two never alias, so neither lookup +/// can see the other's variables. See [`crate::tt::toplevel`] for why the +/// two worlds are distinct. pub struct Context { - /// Stores the value of each of the variables in context. + /// Stores the value of each of the base variables in context. pub env: Env, - /// Stores the names and types of each of the variables in context. + /// Stores the names and types of each of the base variables in context. pub scope: Vec, + /// Stores the value of each fiber variable in context. + pub fiber_env: FiberEnv, + /// Stores the names and fiber types of each fiber variable in context. + pub fiber_scope: Vec, } /// A checkpoint that we can return the context to. pub struct ContextCheckpoint { env: Env, scope: usize, + fiber_env: FiberEnv, + fiber_scope: usize, } impl Default for Context { @@ -44,7 +70,12 @@ impl Default for Context { impl Context { /// Create an empty context. pub fn new() -> Self { - Self { env: Env::Nil, scope: Vec::new() } + Self { + env: Env::Nil, + scope: Vec::new(), + fiber_env: FiberEnv::Nil, + fiber_scope: Vec::new(), + } } /// Create a checkpoint from the current state of the context. @@ -52,6 +83,8 @@ impl Context { ContextCheckpoint { env: self.env.clone(), scope: self.scope.len(), + fiber_env: self.fiber_env.clone(), + fiber_scope: self.fiber_scope.len(), } } @@ -59,14 +92,16 @@ impl Context { pub fn reset_to(&mut self, c: ContextCheckpoint) { self.env = c.env; self.scope.truncate(c.scope); + self.fiber_env = c.fiber_env; + self.fiber_scope.truncate(c.fiber_scope); } - /// Add a new variable to scope (note: does not add it to the environment). + /// Add a new base variable to scope (note: does not add it to the environment). pub fn push_scope(&mut self, name: VarName, label: LabelSegment, ty: Option) { self.scope.push(VarInContext::new(name, label, ty)) } - /// Lookup a variable by name. + /// Lookup a base variable by name. pub fn lookup(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, Option)> { self.scope .iter() @@ -75,4 +110,20 @@ impl Context { .find(|(_, v)| v.name == name) .map(|(i, v)| (i.into(), v.label, v.ty.clone())) } + + /// Add a new fiber variable to scope (note: does not add it to the + /// fiber environment). + pub fn push_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) { + self.fiber_scope.push(FiberVarInContext::new(name, label, ty)) + } + + /// Lookup a fiber variable by name. + pub fn lookup_fiber(&self, name: VarName) -> Option<(BwdIdx, LabelSegment, FiberTyV)> { + self.fiber_scope + .iter() + .rev() + .enumerate() + .find(|(_, v)| v.name == name) + .map(|(i, v)| (i.into(), v.label, v.ty.clone())) + } } diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 828c5ff6e..3ea512f2f 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -51,14 +51,10 @@ impl<'a> Evaluator<'a> { match &**ty { BaseTyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), - // An instance used in type position evaluates to the - // representable record type synthesized from its body (see - // `lookup_ty`). - TopDecl::Instance(i) => match &*i.val { - TmV_::Instance(body) => self.synth_instance_body_ty(body), - _ => panic!("instance {tv} should have an instance body"), - }, - _ => panic!("top-level {tv} should be a type or instance declaration"), + // Instances are fiber types, not base types, so a base + // top-var never refers to one (the elaborator rejects an + // instance name in base-type position before we get here). + _ => panic!("top-level {tv} should be a type declaration"), }, BaseTyS_::Object(ot) => BaseTyV::object(ot.clone()), BaseTyS_::Morphism(pt, dom, cod) => { @@ -75,7 +71,6 @@ impl<'a> Evaluator<'a> { }) } BaseTyS_::Meta(mv) => BaseTyV::meta(*mv), - BaseTyS_::Over(path) => BaseTyV::over(path.clone()), } } @@ -98,22 +93,6 @@ impl<'a> Evaluator<'a> { TmS_::Compose(f, g) => TmV::compose(self.eval_tm(f), self.eval_tm(g)), TmS_::ObApp(name, x) => TmV::app(*name, self.eval_tm(x)), TmS_::List(elems) => TmV::list(elems.iter().map(|tm| self.eval_tm(tm)).collect()), - TmS_::OverApp(mor, mor_label, tgt_path, inner) => { - TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eval_tm(inner)) - } - TmS_::Instance(body) => TmV::instance(InstanceBodyV { - generators: body.generators.clone(), - equations: body - .equations - .iter() - .map(|(lhs, rhs)| (self.eval_tm(lhs), self.eval_tm(rhs))) - .collect(), - sub_instances: body - .sub_instances - .iter() - .map(|(name, (label, inner))| (*name, (*label, self.eval_tm(inner)))) - .collect(), - }), TmS_::Meta(mv) => TmV::meta(*mv), } } @@ -165,57 +144,6 @@ impl<'a> Evaluator<'a> { self.bind_neu("self".into(), "self".into(), ty) } - /// Synthesize the type of an instance body when its name is used in - /// *type* position — i.e. the representable `Hom_Inst(D, self)`, the - /// type of instance morphisms out of the instance `D`. - /// - /// Such a morphism is determined by where `D`'s generators - /// land, so each generator becomes an `Over`-typed field and each - /// sub-instance recurses. A fiber equation (`mor(gen) := target`) - /// becomes an `Id`-typed field witnessing that the constraint - /// holds for the morphism's images. - /// - /// Note that these `Id` fields make the type *honest* but are not - /// *enforced*. An import that contradicts `D`'s fiber equations is - /// not rejected here at this time. - /// Because these `Id` fields are inert, the *concrete* way of mapping out - /// of `D` — building a record literal (`Cons`) of this type by hand — only - /// currently works when `D` is equation-free. - pub fn synth_instance_body_ty(&self, body: &InstanceBodyV) -> BaseTyV { - let mut fields: Row = Row::empty(); - for (name, (label, path)) in &body.generators { - fields.insert(*name, *label, BaseTyS::over(path.clone())); - } - for (name, (label, sub_v)) in &body.sub_instances { - let TmV_::Instance(sub_body) = &**sub_v else { - continue; - }; - let sub_ty_v = self.synth_instance_body_ty(sub_body); - let sub_ty_s = self.quote_ty(&sub_ty_v); - fields.insert(*name, *label, sub_ty_s); - } - // The equation terms reference this body's generators as local - // binders; re-root them at `self` (the record being built) so they - // read as field projections, exactly as sibling references do in an - // ordinary record type. The self var's type is irrelevant, - // so a placeholder suffices. Quoting in `ev` - // (one binder deeper) sends `self` to de Bruijn index 0, matching - // how `field_ty` snocs the record value when projecting. - let (self_n, ev) = self.bind_self(BaseTyV::empty_record()); - for (i, (lhs_v, rhs_v)) in body.equations.iter().enumerate() { - let Some(over_ty) = fiber_equation_ty(lhs_v) else { - unreachable!("instance equation LHS is not a fiber element"); - }; - let lhs_s = ev.quote_tm(&reroot_at_self(lhs_v, &self_n, body)); - let rhs_s = ev.quote_tm(&reroot_at_self(rhs_v, &self_n, body)); - let eq_ty = BaseTyS::id(ev.quote_ty(&over_ty), lhs_s, rhs_s); - let key = format!("_eq{i}"); - fields.insert(name_seg(key.as_str()), label_seg(key.as_str()), eq_ty); - } - let r_v = RecordV::new(self.env.clone(), fields, Dtry::empty()); - BaseTyV::record(r_v) - } - /// Produce type syntax from a type value. /// /// This is a *section* of eval, in that `self.eval_ty(self.quote_ty(ty_v)) == ty_v` @@ -264,7 +192,6 @@ impl<'a> Evaluator<'a> { BaseTyS::id(self.quote_ty(ty), self.quote_tm(tm1), self.quote_tm(tm2)) } BaseTyV_::Meta(mv) => BaseTyS::meta(*mv), - BaseTyV_::Over(path) => BaseTyS::over(path.clone()), } } @@ -285,22 +212,6 @@ impl<'a> Evaluator<'a> { match &**tm { TmV_::Neu(n, _) => self.quote_neu(n), TmV_::App(name, x) => TmS::ob_app(*name, self.quote_tm(x)), - TmV_::OverApp(mor, mor_label, tgt_path, inner) => { - TmS::over_app(*mor, *mor_label, tgt_path.clone(), self.quote_tm(inner)) - } - TmV_::Instance(body) => TmS::instance(InstanceBodyS { - generators: body.generators.clone(), - equations: body - .equations - .iter() - .map(|(lhs, rhs)| (self.quote_tm(lhs), self.quote_tm(rhs))) - .collect(), - sub_instances: body - .sub_instances - .iter() - .map(|(name, (label, inner))| (*name, (*label, self.quote_tm(inner)))) - .collect(), - }), TmV_::List(elems) => TmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()), TmV_::Cons(fields) => TmS::cons(fields.map(|tm| self.quote_tm(tm))), TmV_::Id(x) => TmS::id(self.quote_tm(x)), @@ -342,7 +253,6 @@ impl<'a> Evaluator<'a> { BaseTyV_::Sing(_, x) => self.equal_tm(tm, x), BaseTyV_::Id(_, _, _) => Ok(()), BaseTyV_::Meta(_) => Ok(()), - BaseTyV_::Over(_) => Ok(()), } } @@ -386,13 +296,6 @@ impl<'a> Evaluator<'a> { } (BaseTyV_::Sing(ty1, _), _) => self.convertible_ty(ty1, ty2), (_, BaseTyV_::Sing(ty2, _)) => self.convertible_ty(ty1, ty2), - (BaseTyV_::Over(p1), BaseTyV_::Over(p2)) => { - if p1 == p2 { - Ok(()) - } else { - Err(t("over-types refer to different paths in the codomain")) - } - } _ => Err(t("tried to convert between types of different type constructors")), } } @@ -414,7 +317,6 @@ impl<'a> Evaluator<'a> { BaseTyV_::Sing(_, x) => x.clone(), BaseTyV_::Id(_, _, _) => TmV::empty_cons(), // Extensional equality at a 100% discount! BaseTyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), - BaseTyV_::Over(_) => TmV::neu(n.clone(), ty.clone()), } } @@ -423,10 +325,6 @@ impl<'a> Evaluator<'a> { match &**v { TmV_::Neu(tm_n, ty_v) => self.eta_neu(tm_n, ty_v), TmV_::App(name, x) => TmV::app(*name, self.eta(x, None)), - TmV_::OverApp(mor, mor_label, tgt_path, inner) => { - TmV::over_app(*mor, *mor_label, tgt_path.clone(), self.eta(inner, None)) - } - TmV_::Instance(_) => v.clone(), TmV_::List(elems) => TmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()), TmV_::Cons(row) => { if let Some(ty) = ty { @@ -511,39 +409,6 @@ impl<'a> Evaluator<'a> { (TmV_::Tab(mor1), TmV_::Tab(mor2)) => { self.equal_tm_helper(mor1, mor2, strict1, strict2) } - (TmV_::OverApp(mor1, _, _, inner1), TmV_::OverApp(mor2, _, _, inner2)) => { - if mor1 != mor2 { - return Err(t(format!("OverApp morphisms {mor1} and {mor2} are not equal"))); - } - self.equal_tm_helper(inner1, inner2, strict1, strict2) - } - (TmV_::Instance(b1), TmV_::Instance(b2)) => { - if b1.generators.len() != b2.generators.len() - || b1.equations.len() != b2.equations.len() - || b1.sub_instances.len() != b2.sub_instances.len() - { - return Err(t("instance bodies have differing shapes")); - } - for ((n1, (_, p1)), (n2, (_, p2))) in b1.generators.iter().zip(b2.generators.iter()) - { - if n1 != n2 || p1 != p2 { - return Err(t(format!("instance generator {n1} differs from {n2}"))); - } - } - for ((lhs1, rhs1), (lhs2, rhs2)) in b1.equations.iter().zip(b2.equations.iter()) { - self.equal_tm_helper(lhs1, lhs2, strict1, strict2)?; - self.equal_tm_helper(rhs1, rhs2, strict1, strict2)?; - } - for ((n1, (_, t1)), (n2, (_, t2))) in - b1.sub_instances.iter().zip(b2.sub_instances.iter()) - { - if n1 != n2 { - return Err(t(format!("instance sub-instance {n1} differs from {n2}"))); - } - self.equal_tm_helper(t1, t2, strict1, strict2)?; - } - Ok(()) - } _ => Err(t(format!( "failed to match terms {} and {}", self.quote_tm(tm1), @@ -616,59 +481,88 @@ impl<'a> Evaluator<'a> { }; Ok(BaseTyV::record(r.add_specialization(path, field_ty))) } -} -/// The `Over` type of a *fiber* equation's side, if it is one. -/// -/// Fiber equations (`mor(gen) := target`) relate `Over`-typed terms; their -/// common type is recoverable from the left-hand side. -fn fiber_equation_ty(tm: &TmV) -> Option { - match &**tm { - TmV_::OverApp(_, _, tgt_path, _) => Some(BaseTyV::over(tgt_path.clone())), - TmV_::Neu(_, ty) => matches!(&**ty, BaseTyV_::Over(_)).then(|| ty.clone()), - _ => None, - } -} + // --- Fiber-world NbE and conversion --------------------------------- + // + // Fiber types/terms carry no closures or computation rules (every + // fiber term is neutral), so there is no fiber eval/quote: the + // elaborator builds [`FiberTyS`]/[`FiberTyV`] (and the term sorts) in + // parallel. What remains is conversion checking and field projection. -/// Re-root an instance-body term so its generator/sub-instance references -/// become projections of `self_n`. -/// -/// In an instance body a generator (e.g. `v1`) or sub-instance (e.g. `we`) -/// is a local binder, so it appears as the leading [`TmN_::Var`] of a -/// neutral. To reuse such a term as a field type of the representable -/// record `Hom_Inst(D, self)`, that leading variable must instead read as -/// `self.v1` / `self.we` — a projection of the record. Names not bound by -/// this body are left untouched. -fn reroot_at_self(tm: &TmV, self_n: &TmN, body: &InstanceBodyV) -> TmV { - match &**tm { - TmV_::Neu(n, ty) => TmV::neu(reroot_neu_at_self(n, self_n, body), ty.clone()), - TmV_::OverApp(mor, mor_label, tgt_path, inner) => { - TmV::over_app(*mor, *mor_label, tgt_path.clone(), reroot_at_self(inner, self_n, body)) - } - TmV_::App(name, x) => TmV::app(*name, reroot_at_self(x, self_n, body)), - TmV_::Compose(f, g) => { - TmV::compose(reroot_at_self(f, self_n, body), reroot_at_self(g, self_n, body)) - } - TmV_::Id(x) => TmV::id(reroot_at_self(x, self_n, body)), - TmV_::Tab(x) => TmV::tab(reroot_at_self(x, self_n, body)), - TmV_::List(elems) => { - TmV::list(elems.iter().map(|e| reroot_at_self(e, self_n, body)).collect()) + /// The fiber type of field `field` of a fiber record type, if present. + /// + /// Only [`Over`](FiberTyV_::Over) generator fields are ever projected + /// (as `we.e`); their types are closed, so this is a plain lookup with + /// no environment. + pub fn fiber_field_ty(&self, ty: &FiberTyV, field: FieldName) -> Option { + match &**ty { + FiberTyV_::Record(r) => r.get(field).cloned(), + _ => None, } - _ => tm.clone(), } -} -fn reroot_neu_at_self(n: &TmN, self_n: &TmN, body: &InstanceBodyV) -> TmN { - match &**n { - TmN_::Var(_, name, label) => { - if body.generators.contains_key(name) || body.sub_instances.contains_key(name) { - TmN::proj(self_n.clone(), *name, *label) - } else { - n.clone() + /// Check that two fiber types are convertible. + pub fn convertible_fiber_ty<'b>(&self, ty1: &FiberTyV, ty2: &FiberTyV) -> Result<(), D<'b>> { + match (&**ty1, &**ty2) { + (FiberTyV_::Over(p1), FiberTyV_::Over(p2)) => { + if p1 == p2 { + Ok(()) + } else { + Err(t("over-types refer to different paths in the codomain")) + } } + (FiberTyV_::Record(r1), FiberTyV_::Record(r2)) => { + if r1.iter().count() != r2.iter().count() { + return Err(t("instance records have differing shapes")); + } + for ((n1, (_, f1)), (n2, (_, f2))) in r1.iter().zip(r2.iter()) { + if n1 != n2 { + return Err(t(format!("instance field {n1} differs from {n2}"))); + } + self.convertible_fiber_ty(f1, f2)?; + } + Ok(()) + } + (FiberTyV_::Id(ty1, l1, r1), FiberTyV_::Id(ty2, l2, r2)) => { + self.convertible_fiber_ty(ty1, ty2)?; + self.equal_fiber_tm(l1, l2)?; + self.equal_fiber_tm(r1, r2) + } + _ => Err(t("tried to convert between fiber types of different constructors")), } - TmN_::Proj(inner, field, label) => { - TmN::proj(reroot_neu_at_self(inner, self_n, body), *field, *label) + } + + /// Check that two fiber terms are equal. Fiber terms are all neutral, + /// so this is structural. + pub fn equal_fiber_tm<'b>(&self, tm1: &FiberTmV, tm2: &FiberTmV) -> Result<(), D<'b>> { + match (&**tm1, &**tm2) { + (FiberTmV_::Var(i1, _, _), FiberTmV_::Var(i2, _, _)) => { + if i1 == i2 { + Ok(()) + } else { + Err(t("fiber variables are not equal")) + } + } + (FiberTmV_::Proj(t1, f1, _), FiberTmV_::Proj(t2, f2, _)) => { + if f1 != f2 { + return Err(t(format!("fiber projections {f1} and {f2} are not equal"))); + } + self.equal_fiber_tm(t1, t2) + } + (FiberTmV_::OverApp(m1, _, _, i1), FiberTmV_::OverApp(m2, _, _, i2)) => { + if m1 != m2 { + return Err(t(format!("OverApp morphisms {m1} and {m2} are not equal"))); + } + self.equal_fiber_tm(i1, i2) + } + (FiberTmV_::Meta(a), FiberTmV_::Meta(b)) => { + if a == b { + Ok(()) + } else { + Err(t(format!("Holes {a} and {b} are not equal."))) + } + } + _ => Err(t("fiber terms are not equal")), } } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index f076763f5..ece8ade08 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -400,7 +400,6 @@ impl<'a> ModelGenerator<'a> { None } BaseTyV_::Meta(_) => None, - BaseTyV_::Over(_) => None, } } } @@ -408,7 +407,7 @@ impl<'a> ModelGenerator<'a> { /// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Instance`] /// declaration. /// -/// Walks the instance body's [`TmV_::Instance`] payload, registering +/// Walks the instance's fiber [`Record`](FiberTyV_::Record), registering /// each generator with its fiber, each equation as a pair of /// [`DiscreteInstanceTerm`]s, and each sub-instance's contents under /// the appropriate prefix. @@ -427,59 +426,71 @@ pub fn instance_from_def( .as_discrete() .ok_or_else(|| "expected a discrete codomain model".to_string())?; let mut instance = DiscreteDblModelInstance::new(Rc::new(cod_model)); - let TmV_::Instance(body) = &*inst.val else { - return Err("expected a TmV::Instance body".into()); + let FiberTyV_::Record(fields) = &*inst.val else { + return Err("expected an instance (a fiber record)".into()); }; let mut namespace = Namespace::new_for_uuid(); - extract_instance_body(&mut instance, &mut namespace, &[], body)?; + extract_instance_record(&mut instance, &mut namespace, &[], fields)?; Ok((instance, namespace)) } -fn extract_instance_body( +/// Register the contents of an instance (a fiber record) into the model +/// instance under construction, recursing into sub-instance imports under +/// their prefix. +/// +/// Two passes so that every generator — including those of sub-instances +/// — is registered before any equation that may mention it: first the +/// [`Over`](FiberTyV_::Over) generator fields and [`Record`](FiberTyV_::Record) +/// sub-instances, then the [`Id`](FiberTyV_::Id) equation fields. +fn extract_instance_record( instance: &mut DiscreteDblModelInstance, namespace: &mut Namespace, prefix: &[NameSegment], - body: &InstanceBodyV, + fields: &Row, ) -> Result<(), String> { - for (name, (label, path)) in &body.generators { - if let NameSegment::Uuid(uuid) = name { - namespace.set_label(*uuid, *label); + for (name, (label, field_ty)) in fields.iter() { + match &**field_ty { + FiberTyV_::Over(path) => { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let mut qsegs = prefix.to_vec(); + qsegs.push(*name); + let qname: QualifiedName = qsegs.into(); + let fiber: QualifiedName = + path.iter().map(|(seg, _)| *seg).collect::>().into(); + instance.add_generator(qname, fiber); + } + FiberTyV_::Record(sub_fields) => { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let mut sub_prefix = prefix.to_vec(); + sub_prefix.push(*name); + extract_instance_record(instance, namespace, &sub_prefix, sub_fields)?; + } + FiberTyV_::Id(_, _, _) => {} } - let mut qsegs = prefix.to_vec(); - qsegs.push(*name); - let qname: QualifiedName = qsegs.into(); - let fiber: QualifiedName = path.iter().map(|(seg, _)| *seg).collect::>().into(); - instance.add_generator(qname, fiber); } - for (lhs, rhs) in &body.equations { - let lhs_t = tm_to_discrete_instance_term_with_prefix(instance, lhs, prefix)?; - let rhs_t = tm_to_discrete_instance_term_with_prefix(instance, rhs, prefix)?; - instance.add_equation(lhs_t, rhs_t); - } - for (name, (label, sub_v)) in &body.sub_instances { - if let NameSegment::Uuid(uuid) = name { - namespace.set_label(*uuid, *label); + for (_, (_, field_ty)) in fields.iter() { + if let FiberTyV_::Id(_, lhs, rhs) = &**field_ty { + let lhs_t = fiber_tm_to_discrete_instance_term(instance, lhs, prefix)?; + let rhs_t = fiber_tm_to_discrete_instance_term(instance, rhs, prefix)?; + instance.add_equation(lhs_t, rhs_t); } - let TmV_::Instance(sub_body) = &**sub_v else { - return Err("sub-instance is not a TmV::Instance".into()); - }; - let mut sub_prefix = prefix.to_vec(); - sub_prefix.push(*name); - extract_instance_body(instance, namespace, &sub_prefix, sub_body)?; } Ok(()) } -/// Convert an `Over`-typed term value into a [`DiscreteInstanceTerm`], -/// prefixing each generator name with the path into any enclosing -/// sub-instances. +/// Convert a fiber term into a [`DiscreteInstanceTerm`], prefixing each +/// generator name with the path into any enclosing sub-instances. /// -/// Nested [`TmV_::OverApp`]s are flattened into a single morphism path -/// applied to the generator at the leaf, matching the flat canonical -/// shape of [`DiscreteInstanceTerm`]. -fn tm_to_discrete_instance_term_with_prefix( +/// Nested [`OverApp`](FiberTmV_::OverApp)s are flattened into a single +/// morphism path applied to the generator at the leaf, matching the flat +/// canonical shape of [`DiscreteInstanceTerm`]. +fn fiber_tm_to_discrete_instance_term( instance: &DiscreteDblModelInstance, - tm: &TmV, + tm: &FiberTmV, prefix: &[NameSegment], ) -> Result { // Walk outer-to-inner, recording each applied morphism. @@ -487,16 +498,15 @@ fn tm_to_discrete_instance_term_with_prefix( let mut cur = tm; let base: QualifiedName = loop { match &**cur { - TmV_::Neu(n, _) => { - let mut segs = prefix.to_vec(); - segs.extend(neu_full_name(n)); - break segs.into(); - } - TmV_::OverApp(mor_name, _, _, inner) => { + FiberTmV_::OverApp(mor_name, _, _, inner) => { mors_outer_first.push(QualifiedName::single(*mor_name)); cur = inner; } - _ => return Err("term is not a generator or codomain-morphism application".into()), + _ => { + let mut segs = prefix.to_vec(); + segs.extend(fiber_full_name(cur)?); + break segs.into(); + } } }; // Path order is innermost-first (apply first goes first). @@ -513,29 +523,28 @@ fn tm_to_discrete_instance_term_with_prefix( Ok(DiscreteInstanceTerm { path, base }) } -/// Read off the full name of a neutral, including its leading variable -/// name and any subsequent projection segments. Unlike -/// [`TmN::to_qualified_name`] this does *not* treat the leading -/// variable as an implicit root — for instance-body terms the leading -/// variable is itself a generator name (e.g. `v1`) or sub-instance -/// name (e.g. `we`), and must contribute a segment. -fn neu_full_name(n: &TmN) -> Vec { +/// Read off the full name of a fiber term that is a generator or a chain +/// of projections out of a sub-instance import (e.g. `we.e`): the leading +/// variable contributes a segment (it is itself a generator/import name), +/// followed by each projected field. +fn fiber_full_name(tm: &FiberTmV) -> Result, String> { let mut segments = Vec::new(); - let mut cur = n; + let mut cur = tm; loop { match &**cur { - TmN_::Var(_, name, _) => { + FiberTmV_::Var(_, name, _) => { segments.push(*name); break; } - TmN_::Proj(n1, f, _) => { + FiberTmV_::Proj(inner, f, _) => { segments.push(*f); - cur = n1; + cur = inner; } + _ => return Err("expected a fiber generator or projection".into()), } } segments.reverse(); - segments + Ok(segments) } #[cfg(test)] @@ -597,11 +606,9 @@ instance I : WeightedGraph := [ /// An instance carrying fiber equations, imported as a sub-instance. /// - /// Exercises the presented-representable synthesis: `Loop`'s type (as - /// used by the import `l : Loop`) must carry an `Id`-typed field per - /// fiber equation, and that type must quote/evaluate without panicking. - /// Extraction of the importer must still recover both copies of the - /// loop's structure. + /// `Loop` is itself a fiber record carrying an `Id`-typed field per + /// fiber equation alongside its `e`/`v` generators, and extraction of + /// the importer must recover both copies of the loop's structure. #[test] fn import_of_instance_with_fiber_equations() { let src = r#" @@ -629,28 +636,19 @@ instance UseLoop : WeightedGraph := [ "#; let toplevel = elaborate_to_toplevel(src); - // `Loop`'s representable type must expose its two fiber equations as - // `Id`-typed fields alongside the `e`/`v` generators, and quoting it - // (which re-evaluates every field type against a bound `self`) must - // not panic. + // `Loop` is a fiber record exposing its two fiber equations as + // `Id`-typed fields alongside the `e`/`v` generators. let loop_def = match toplevel.declarations.get(&name_seg("Loop")) { Some(TopDecl::Instance(i)) => i.clone(), _ => panic!("expected Loop to be an instance declaration"), }; - let TmV_::Instance(loop_body) = &*loop_def.val else { - panic!("Loop should evaluate to an instance"); - }; - let eval = Evaluator::empty(&toplevel); - let body_ty = eval.synth_instance_body_ty(loop_body); - let BaseTyV_::Record(r) = &*body_ty else { - panic!("synthesized instance type should be a record"); + let FiberTyV_::Record(r) = &*loop_def.val else { + panic!("Loop should be a fiber record"); }; - assert!(r.fields.get(name_seg("e")).is_some(), "generator field e"); - assert!(r.fields.get(name_seg("v")).is_some(), "generator field v"); - assert!(r.fields.get(name_seg("_eq0")).is_some(), "first fiber equation"); - assert!(r.fields.get(name_seg("_eq1")).is_some(), "second fiber equation"); - // Round-trips through eval+quote of the dependent `Id` fields. - let _ = eval.quote_ty(&body_ty); + assert!(r.get(name_seg("e")).is_some(), "generator field e"); + assert!(r.get(name_seg("v")).is_some(), "generator field v"); + assert!(r.get(name_seg("_eq0")).is_some(), "first fiber equation"); + assert!(r.get(name_seg("_eq1")).is_some(), "second fiber equation"); // The importer still extracts both generators and the imported // copy's two equations. diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 18cd10622..8926fac77 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -87,21 +87,6 @@ pub enum BaseTyS_ { /// Currently, this is only used for handling elaboration errors, we might /// add more unification/holes later. Meta(MetaVar), - - /// The type of terms in a fiber over an object generator of some - /// instance's codomain model. Internal-only; no surface syntax — - /// inhabitants are introduced via set-literal clauses `field := - /// [...]` inside an instance body, and applications via the - /// `mor(arg)` and `mor(receiver.fld)` syn arms. - /// - /// The path identifies the object generator in the codomain. - /// Instance identity is contextual rather than part of the type: - /// any instance body in scope contributes its terms to this type, - /// so two fiber-types over the same codomain path from different - /// instance declarations are convertible. The elaborator validates - /// the path against the enclosing instance's codomain at - /// construction time. - Over(Vec<(FieldName, LabelSegment)>), } /// Syntax for total types, dereferences to [BaseTyS_]. @@ -155,11 +140,6 @@ impl BaseTyS { pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(BaseTyS_::Meta(mv))) } - - /// Smart constructor for [BaseTyS], [BaseTyS_::Over] case. - pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(BaseTyS_::Over(path))) - } } impl ToDoc for BaseTyS { @@ -183,7 +163,6 @@ impl ToDoc for BaseTyS { ), ), BaseTyS_::Meta(mv) => t(format!("?{}", mv.id)), - BaseTyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), } } } @@ -202,25 +181,6 @@ fn object_path_to_string(path: &[(FieldName, LabelSegment)]) -> String { path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") } -fn instance_body_to_doc<'a>(body: &InstanceBodyS) -> D<'a> { - let gens = body.generators.iter().map(|(_, (label, path))| { - binop( - t(":"), - t(format!("{label}")), - t(format!("Over({})", object_path_to_string(path))), - ) - }); - let eqns = body - .equations - .iter() - .map(|(lhs, rhs)| binop(t(":="), lhs.to_doc(), rhs.to_doc())); - let subs = body - .sub_instances - .iter() - .map(|(_, (label, inner))| binop(t(":"), t(format!("{label}")), inner.to_doc())); - tuple(gens.chain(subs).chain(eqns)) -} - impl fmt::Display for BaseTyS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_doc().group().pretty()) @@ -253,66 +213,12 @@ pub enum TmS_ { ObApp(VarName, TmS), /// List of objects. List(Vec), - /// Application of a codomain morphism to a fiber-typed - /// ([`BaseTyS_::Over`]) term. Only well-formed inside an instance body. - /// - /// Arguments, in order: - /// 1. `mor` — name of the codomain morphism being applied - /// (e.g. `src` in `src(we.e)`). - /// 2. `mor_label` — display label for that name. - /// 3. `tgt_path` — the codomain object-path that `mor` lands at. - /// Stored on the node so [`super::eval::Evaluator::eval_tm`] - /// can recover the result fiber type without consulting the - /// codomain. - /// 4. `inner` — the fiber-typed argument (e.g. the elaboration of - /// `we.e`, whose type is the fiber over `` where the - /// codomain morphism `mor : `). - /// - /// Here is an example: - /// ```text - /// instance Edge : WeightedGraph := [E := [e]] - /// instance I : WeightedGraph := [ - /// V := [v1], - /// we : Edge, - /// src(we.e) := v1, - /// ] - /// ``` - /// The LHS of the mapping clause elaborates to - /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of fiber-type - /// over `.V`. - OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmS), - /// An instance value: the level-shifted introduction rule for terms - /// of a model (sketch) type. See [`InstanceBodyS`] for the payload. - Instance(InstanceBodyS), /// A metavar. /// /// This only appears when we have an error in elaboration. Meta(MetaVar), } -/// Payload of [`TmS_::Instance`]. -/// -/// Captures the data needed to build a `DblModelInstance` at extraction -/// time, while staying theory-agnostic at the term-syntax level. -/// -/// - `generators` maps each generator's local name (within this -/// instance) to a path into the model identifying which object -/// generator's fiber it lives over. Generators are introduced by -/// surface set-literal clauses `field := [...]` in the instance body. -/// - `equations` is a list of `(lhs, rhs)` pairs over fiber -/// ([`BaseTyS_::Over`]) types. -/// - `sub_instances` maps each sub-instance import's local name to a -/// nested instance term. This is what surface `we : Edge` lowers to. -#[derive(Default)] -pub struct InstanceBodyS { - /// Generators introduced by this instance, with their fibers. - pub generators: IndexMap)>, - /// Equation witnesses, asserted to hold in this instance. - pub equations: Vec<(TmS, TmS)>, - /// Sub-instance imports, keyed by import name. - pub sub_instances: IndexMap, -} - /// Syntax for total terms, dereferences to [TmS_]. /// /// See [crate::tt] for an explanation of what total types are, and for an @@ -367,21 +273,6 @@ impl TmS { Self(Rc::new(TmS_::List(elems))) } - /// Smart constructor for [TmS], [TmS_::OverApp] case. - pub fn over_app( - mor: FieldName, - mor_label: LabelSegment, - tgt_path: Vec<(FieldName, LabelSegment)>, - inner: TmS, - ) -> Self { - Self(Rc::new(TmS_::OverApp(mor, mor_label, tgt_path, inner))) - } - - /// Smart constructor for [TmS], [TmS_::Instance] case. - pub fn instance(body: InstanceBodyS) -> Self { - Self(Rc::new(TmS_::Instance(body))) - } - /// Smart constructor for [TmS], [TmS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(TmS_::Meta(mv))) @@ -405,8 +296,6 @@ impl ToDoc for TmS { TmS_::Compose(f, g) => binop(t("·"), f.to_doc(), g.to_doc()), TmS_::ObApp(name, x) => unop(t(format!("@{name}")), x.to_doc()), TmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), - TmS_::OverApp(_, mor_label, _, inner) => inner.to_doc() + t(format!(".{mor_label}")), - TmS_::Instance(body) => instance_body_to_doc(body), TmS_::Meta(mv) => t(format!("?{}", mv.id)), } } @@ -417,3 +306,154 @@ impl fmt::Display for TmS { write!(f, "{}", self.to_doc().group().pretty()) } } + +/// Inner enum for [FiberTyS]. +/// +/// Fiber types type the fiber world — instances of a model and their +/// elements — mirroring how [`BaseTyS`] types the base world (models). +/// See [`crate::tt::toplevel`] for the comprehension-category picture. +/// The three constructors parallel the base world: [`Over`](Self::Over) +/// is the atomic fiber-element type, [`Record`](Self::Record) assembles +/// them into an instance, and [`Id`](Self::Id) imposes a (propositional) +/// equation, just as [`BaseTyS_::Id`] does in the base. +pub enum FiberTyS_ { + /// The type of a fiber element lying over the object generator of the + /// codomain model identified by `path`. No surface syntax — its + /// inhabitants ([`FiberTmS`]) are introduced by set-literal clauses + /// `field := [...]`, by projection out of a sub-instance import, and + /// by codomain-morphism application inside an instance body. + Over(Vec<(FieldName, LabelSegment)>), + /// An instance of a model — an object of the fiber over the codomain + /// model — presented as a record of fiber types. A generator is an + /// [`Over`](Self::Over) field, a sub-instance import is a nested + /// [`Record`](Self::Record) field, and an equation is an + /// [`Id`](Self::Id) field. This is what `instance I : X := [...]` + /// elaborates to, and also the type of a sub-instance import `we : + /// Edge` (whose generators are then projected as `we.e`). + Record(Row), + /// A propositional equation between two fiber elements of the given + /// fiber type, asserted to hold in the enclosing instance. Mirrors + /// [`BaseTyS_::Id`]; like it, these are proof-irrelevant. + Id(FiberTyS, FiberTmS, FiberTmS), +} + +/// Syntax for fiber types, dereferences to [FiberTyS_]. +#[derive(Clone, Deref)] +#[deref(forward)] +pub struct FiberTyS(Rc); + +impl FiberTyS { + /// Smart constructor for [FiberTyS], [FiberTyS_::Over] case. + pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(FiberTyS_::Over(path))) + } + + /// Smart constructor for [FiberTyS], [FiberTyS_::Record] case. + pub fn record(fields: Row) -> Self { + Self(Rc::new(FiberTyS_::Record(fields))) + } + + /// Smart constructor for [FiberTyS], [FiberTyS_::Id] case. + pub fn id(ty: FiberTyS, tm1: FiberTmS, tm2: FiberTmS) -> Self { + Self(Rc::new(FiberTyS_::Id(ty, tm1, tm2))) + } +} + +impl ToDoc for FiberTyS { + fn to_doc<'a>(&self) -> D<'a> { + match &**self { + FiberTyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), + FiberTyS_::Record(fields) => tuple(fields.iter().map(|(_, (label, ty))| { + binop(t(":"), t(format!("{}", label)).group(), ty.to_doc()) + })), + FiberTyS_::Id(_, tm1, tm2) => binop(t("=="), tm1.to_doc(), tm2.to_doc()), + } + } +} + +impl fmt::Display for FiberTyS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_doc().group().pretty()) + } +} + +/// Inner enum for [FiberTmS]: a term of a fiber type, i.e. an element of +/// an instance. +/// +/// Fiber terms reference the elaborator's *fiber* scope (generators and +/// sub-instance imports), which is separate from the base context; see +/// [`crate::tt::context::Context`]. They are all neutral — there is no +/// fiber introduction form yet (mapping out of an instance by a record +/// literal is future work), so a fiber term is always a variable, a +/// projection, or a codomain-morphism application. +pub enum FiberTmS_ { + /// A fiber-context variable: a generator or a sub-instance import. + /// Backward index into the fiber environment. + Var(BwdIdx, VarName, LabelSegment), + /// Projection of a generator out of a sub-instance import, e.g. + /// `we.e`. + Proj(FiberTmS, FieldName, LabelSegment), + /// Application of a codomain morphism to a fiber element. Arguments, + /// in order: the morphism name (e.g. `src`), its display label, the + /// codomain object-path it lands at (stored so the result fiber type + /// is recoverable without consulting the codomain), and the + /// fiber-typed argument (e.g. the elaboration of `we.e`). + /// + /// Example: in `src(we.e) := v1`, the LHS elaborates to + /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of fiber type + /// `Over(.V)`. + OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, FiberTmS), + /// A metavar (elaboration-error placeholder). + Meta(MetaVar), +} + +/// Syntax for fiber terms, dereferences to [FiberTmS_]. +#[derive(Clone, Deref)] +#[deref(forward)] +pub struct FiberTmS(Rc); + +impl FiberTmS { + /// Smart constructor for [FiberTmS], [FiberTmS_::Var] case. + pub fn var(bwd_idx: BwdIdx, var_name: VarName, label: LabelSegment) -> Self { + Self(Rc::new(FiberTmS_::Var(bwd_idx, var_name, label))) + } + + /// Smart constructor for [FiberTmS], [FiberTmS_::Proj] case. + pub fn proj(tm: FiberTmS, field_name: FieldName, label: LabelSegment) -> Self { + Self(Rc::new(FiberTmS_::Proj(tm, field_name, label))) + } + + /// Smart constructor for [FiberTmS], [FiberTmS_::OverApp] case. + pub fn over_app( + mor: FieldName, + mor_label: LabelSegment, + tgt_path: Vec<(FieldName, LabelSegment)>, + inner: FiberTmS, + ) -> Self { + Self(Rc::new(FiberTmS_::OverApp(mor, mor_label, tgt_path, inner))) + } + + /// Smart constructor for [FiberTmS], [FiberTmS_::Meta] case. + pub fn meta(mv: MetaVar) -> Self { + Self(Rc::new(FiberTmS_::Meta(mv))) + } +} + +impl ToDoc for FiberTmS { + fn to_doc<'a>(&self) -> D<'a> { + match &**self { + FiberTmS_::Var(_, _, label) => t(format!("{}", label)), + FiberTmS_::Proj(tm, _, label) => tm.to_doc() + t(format!(".{}", label)), + FiberTmS_::OverApp(_, mor_label, _, inner) => { + inner.to_doc() + t(format!(".{mor_label}")) + } + FiberTmS_::Meta(mv) => t(format!("?{}", mv.id)), + } + } +} + +impl fmt::Display for FiberTmS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_doc().group().pretty()) + } +} diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index de26d910a..29e4c6c0f 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -420,6 +420,132 @@ impl<'a> Elaborator<'a> { v } + /// Introduce a fiber variable (a generator or sub-instance import) + /// into the fiber scope, returning its neutral value. + fn intro_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) -> FiberTmV { + let v = FiberTmV::var(self.ctx.fiber_scope.len().into(), name, label); + self.ctx.fiber_env = self.ctx.fiber_env.snoc(v.clone()); + self.ctx.push_fiber(name, label, ty); + v + } + + /// Look up a fiber variable by name, returning its syntax, value, and + /// fiber type. + fn lookup_fiber_tm(&self, name: VarName) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { + let (i, label, ty) = self.ctx.lookup_fiber(name)?; + Some((FiberTmS::var(i, name, label), self.ctx.fiber_env.get(*i).unwrap().clone(), ty)) + } + + fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { + let tm_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(Vec::new())) + } + + fn fiber_syn_error(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV, FiberTyV) { + self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); + self.fiber_syn_hole() + } + + fn fiber_chk_error(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV) { + self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); + let tm_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m)) + } + + /// Synthesize a fiber term and its fiber type. A fiber term is a + /// generator/import variable, a projection out of a sub-instance + /// (`we.e`), or a codomain-morphism application (`src(we.e)`). + fn fiber_syn(&mut self, n: &FNtn) -> (FiberTmS, FiberTmV, FiberTyV) { + let mut elab = self.enter(n.loc()); + match n.ast0() { + Var(name) => match elab.lookup_fiber_tm(name_seg(*name)) { + Some(r) => r, + None => elab.fiber_syn_error(format!("no such fiber element {name}")), + }, + // Projection of a generator out of a sub-instance import: `we.e`. + App1(recv_n, L(_, Field(f))) => { + let (recv_s, recv_v, recv_ty) = elab.fiber_syn(recv_n); + let FiberTyV_::Record(r) = &*recv_ty else { + return elab + .fiber_syn_error("can only project a generator out of a sub-instance"); + }; + let fname = name_seg(*f); + let flabel = label_seg(*f); + let Some(field_ty) = r.get(fname).cloned() else { + return elab + .fiber_syn_error(format!("no such generator {fname} in sub-instance")); + }; + ( + FiberTmS::proj(recv_s, fname, flabel), + FiberTmV::proj(recv_v, fname, flabel), + field_ty, + ) + } + // Codomain-morphism application: `f(arg)`. + App1(L(_, Var(f)), arg_n) => { + // A display label for the argument, used only in errors. + let label = match arg_n.ast0() { + Var(x) => x.to_string(), + App1(_, L(_, Field(fld))) => fld.to_string(), + _ => "argument".to_string(), + }; + let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n); + elab.apply_codomain_morphism(f, arg_s, arg_v, arg_ty, &label) + } + _ => elab.fiber_syn_error( + "expected a fiber element: a generator, a projection `we.e`, or a \ + codomain-morphism application `f(..)`", + ), + } + } + + /// Check a fiber term against an expected fiber type. Fiber terms are + /// all synthesizing, so this synthesizes and checks convertibility. + fn fiber_chk(&mut self, expected: &FiberTyV, n: &FNtn) -> (FiberTmS, FiberTmV) { + let (s, v, ty) = self.fiber_syn(n); + if let Err(e) = self.evaluator().convertible_fiber_ty(&ty, expected) { + return self + .fiber_chk_error(format!("fiber element has the wrong type:\n{}", e.pretty())); + } + (s, v) + } + + /// Elaborate a fiber-type annotation. Used for sub-instance imports + /// (`we : Edge`, where `Edge` names a top-level instance) and anonymous + /// equations (`name : (a == b)`). + fn fiber_ty(&mut self, n: &FNtn) -> Option<(FiberTyS, FiberTyV)> { + match n.ast0() { + Var(name) => { + let topvar = name_seg(*name); + match self.toplevel.declarations.get(&topvar) { + Some(TopDecl::Instance(i)) => Some((i.stx.clone(), i.val.clone())), + _ => self + .error(format!("{name} must reference a top-level instance declaration")), + } + } + App2(L(_, Keyword("==")), a_n, b_n) => { + let (a_s, a_v, a_ty) = self.fiber_syn(a_n); + let (b_s, b_v, b_ty) = self.fiber_syn(b_n); + if let Err(e) = self.evaluator().convertible_fiber_ty(&a_ty, &b_ty) { + return self.error(format!( + "equation sides have inconvertible fiber types:\n{}", + e.pretty() + )); + } + let FiberTyV_::Over(path) = &*a_ty else { + return self.error( + "instance equations must be between elements over an object \ + (fiber elements); morphism equations constrain the model, not \ + an instance", + ); + }; + let over_s = FiberTyS::over(path.clone()); + Some((FiberTyS::id(over_s, a_s, b_s), FiberTyV::id(a_ty.clone(), a_v, b_v))) + } + _ => self.error("expected an instance name or an equation `a == b`"), + } + } + /// The unit type, elaborated as the empty record — i.e. the empty /// model. `Unit` and `tt` are surface sugar for the empty record type /// and its unique element, the empty cons `[]`. @@ -433,62 +559,61 @@ impl<'a> Elaborator<'a> { fn apply_codomain_morphism( &mut self, f: &str, - arg_s: TmS, - arg_v: TmV, - arg_ty: BaseTyV, + arg_s: FiberTmS, + arg_v: FiberTmV, + arg_ty: FiberTyV, arg_label_str: &str, - ) -> (TmS, TmV, BaseTyV) { + ) -> (FiberTmS, FiberTmV, FiberTyV) { let Some(codomain) = self.instance_codomain() else { - return self - .syn_error("applied codomain morphism is only allowed inside an instance body"); + return self.fiber_syn_error( + "applied codomain morphism is only allowed inside an instance body", + ); }; - let BaseTyV_::Over(src_path) = &*arg_ty else { - let quoted = self.evaluator().quote_ty(&arg_ty); - return self.syn_error(format!( - "argument {arg_label_str} has type {quoted}, expected a fiber type", + let FiberTyV_::Over(src_path) = &*arg_ty else { + return self.fiber_syn_error(format!( + "argument {arg_label_str} is not an element over an object", )); }; let f_label = label_seg(f); let f_name = name_seg(f); let Some(mor_ty_s) = codomain.fields.get(f_name) else { - return self.syn_error(format!("no such codomain morphism {f_name}")); + return self.fiber_syn_error(format!("no such codomain morphism {f_name}")); }; let BaseTyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { - return self.syn_error(format!("codomain field {f_name} is not a morphism")); + return self.fiber_syn_error(format!("codomain field {f_name} is not a morphism")); }; let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) else { - return self.syn_error(format!( + return self.fiber_syn_error(format!( "codomain morphism {f_name} has non-path dom/cod; \ applied-morphism syntax requires both to be paths", )); }; if dom_path != *src_path { - return self.syn_error(format!( + return self.fiber_syn_error(format!( "codomain morphism {f_name} has source path differing from the argument", )); } ( - TmS::over_app(f_name, f_label, cod_path.clone(), arg_s), - TmV::over_app(f_name, f_label, cod_path.clone(), arg_v), - BaseTyV::over(cod_path), + FiberTmS::over_app(f_name, f_label, cod_path.clone(), arg_s), + FiberTmV::over_app(f_name, f_label, cod_path.clone(), arg_v), + FiberTyV::over(cod_path), ) } /// Elaborate an instance body — a tuple of `name : type`, `field /// := [names]`, and `mor(arg) := target` clauses — against the - /// enclosing sketch type `codomain`. Produces a [`TmS_::Instance`] - /// / [`TmV_::Instance`] pair whose payload is the instance's - /// generator slots, equation witnesses, and sub-instance imports. + /// enclosing codomain model. Produces the instance as a fiber + /// [`Record`](FiberTyS_::Record): generators become + /// [`Over`](FiberTyS_::Over) fields, sub-instance imports nested + /// [`Record`](FiberTyS_::Record) fields, and equations + /// [`Id`](FiberTyS_::Id) fields. /// - /// The codomain model is bound into the context as a `self`-typed + /// The codomain model is bound into the *base* context as a `self`-typed /// record variable (and the binding is dropped on exit) so that - /// generator (fiber) clauses and applied-codomain-morphism syntax - /// inside the body resolve their generators by name, against a real - /// context variable. Introducing `self` - /// at the bottom of the context leaves every generator's index - /// unchanged, and the codomain is never referenced by variable (only - /// by stored path), so this perturbs no index arithmetic. - fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (TmS, TmV) { + /// applied-codomain-morphism syntax resolves morphisms by name. The + /// instance's own generators and imports live in the separate *fiber* + /// scope. + fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (FiberTyS, FiberTyV) { let c = self.checkpoint(); let binder = name_seg(Self::CODOMAIN_BINDER); self.intro( @@ -501,9 +626,9 @@ impl<'a> Elaborator<'a> { result } - /// Elaborate the clauses of an instance body (the f-notation `n`) into the - /// payload of an [`InstanceBodyS`]/[`InstanceBodyV`]. The codomain is - /// already set on the context by [`Self::instance_body`]. + /// Elaborate the clauses of an instance body (the f-notation `n`) into a + /// fiber [`Record`](FiberTyS_::Record). The codomain is already set on + /// the context by [`Self::instance_body`]. /// /// Steps: /// 1. Set up empty accumulators (see below) for the clauses to fill. @@ -525,105 +650,60 @@ impl<'a> Elaborator<'a> { /// - `field := [n1, n2, ...]` — set-literal: declares generators in /// the fiber over a codomain *object* `field`. /// - `mor(arg) := target` — a single equation witness. - fn instance_body_inner(&mut self, n: &FNtn) -> (TmS, TmV) { + fn instance_body_inner(&mut self, n: &FNtn) -> (FiberTyS, FiberTyV) { let mut elab = self.enter(n.loc()); + let empty = || (FiberTyS::record(Row::empty()), FiberTyV::record(Row::empty())); let Tuple(field_ns) = n.ast0() else { elab.error::<()>("expected a tuple instance body"); - return ( - TmS::instance(InstanceBodyS::default()), - TmV::instance(InstanceBodyV::default()), - ); + return empty(); }; - // Accumulators, assembled into the instance payload at the end: - // generators (with the codomain fiber path each lives over), - // equation witnesses (quoted lhs/rhs pairs), and imported - // sub-instances. `_s`/`_v` hold the syntactic / value forms. - let mut gens: IndexMap)> = - IndexMap::new(); - let mut eqns_s: Vec<(TmS, TmS)> = Vec::new(); - let mut eqns_v: Vec<(TmV, TmV)> = Vec::new(); - let mut subs_s: IndexMap = IndexMap::new(); - let mut subs_v: IndexMap = IndexMap::new(); + // The instance is assembled as a fiber record: a generator is an + // `Over` field, a sub-instance import a nested `Record` field, and + // an equation an `Id` field (with a synthetic `_eqN` name). + // `fields_s`/`fields_v` hold the syntactic / value rows; `eq_count` + // names successive equation fields. + let mut fields_s: Row = Row::empty(); + let mut fields_v: Row = Row::empty(); + let mut eq_count = 0usize; let mut failed = false; for field_n in field_ns.iter() { elab.loc = Some(field_n.loc()); match field_n.ast0() { - // `name : type` — generator, sub-instance, or - // anonymous equation clause (dispatched on the - // elaborated type's shape). + // `name : type` — a sub-instance import (`we : Edge`) or an + // anonymous equation (`name : (a == b)`), dispatched on the + // elaborated fiber type's shape. App2(L(_, Keyword(":")), L(_, Var(name)), ty_n) => { - let name_str = *name; - let n_seg = name_seg(name_str); - let label = label_seg(name_str); - let (_, ty_v) = elab.ty(ty_n); + let n_seg = name_seg(*name); + let label = label_seg(*name); + let Some((ty_s, ty_v)) = elab.fiber_ty(ty_n) else { + failed = true; + continue; + }; match &*ty_v { - BaseTyV_::Over(path) => { - gens.insert(n_seg, (label, path.clone())); - elab.intro(n_seg, label, Some(ty_v)); - } - BaseTyV_::Record(_) => { - let (sub_s, sub_v) = match ty_n.ast0() { - Var(sub_name) => { - let topvar = name_seg(*sub_name); - match elab.toplevel.declarations.get(&topvar) { - Some(TopDecl::Instance(i)) => { - (i.stx.clone(), i.val.clone()) - } - _ => { - elab.error::<()>(format!( - "sub-instance {sub_name} must reference a \ - top-level instance declaration", - )); - failed = true; - continue; - } - } - } - _ => { - elab.error::<()>( - "sub-instance type must be a top-level def name", - ); - failed = true; - continue; - } - }; - subs_s.insert(n_seg, (label, sub_s)); - subs_v.insert(n_seg, (label, sub_v)); - elab.intro(n_seg, label, Some(ty_v)); + // A sub-instance import: bind it in the fiber scope + // (so `name.gen` projections resolve) and record it. + FiberTyV_::Record(_) => { + elab.intro_fiber(n_seg, label, ty_v.clone()); + fields_s.insert(n_seg, label, ty_s); + fields_v.insert(n_seg, label, ty_v); } - BaseTyV_::Id(eq_ty, lhs, rhs) => { - if !matches!(&**eq_ty, BaseTyV_::Over(_)) { - let quoted = elab.evaluator().quote_ty(eq_ty); - elab.error::<()>(format!( - "instance equation {name_str} is over {quoted}, but an \ - instance's equations must be between elements over an \ - object (fiber elements); morphism equations constrain \ - the model, not an instance", - )); - failed = true; - continue; - } - let evaluator = elab.evaluator(); - let lhs_s = evaluator.quote_tm(lhs); - let rhs_s = evaluator.quote_tm(rhs); - eqns_s.push((lhs_s, rhs_s)); - eqns_v.push((lhs.clone(), rhs.clone())); + // A named equation (e.g. `eq : (.src(e) == .src(f))`). + FiberTyV_::Id(_, _, _) => { + fields_s.insert(n_seg, label, ty_s); + fields_v.insert(n_seg, label, ty_v); } - _ => { - let quoted = elab.evaluator().quote_ty(&ty_v); + FiberTyV_::Over(_) => { elab.error::<()>(format!( - "instance clause {name_str} has type {quoted}, expected \ - an element over an object generator, a sub-sketch, or an equation", + "instance clause {name} cannot be annotated with a bare \ + element type", )); failed = true; } } } - // `field := [k1 := t1, k2 := t2, ...]` — mapping-literal - // assignment to a morphism-typed field of the codomain. - // Equivalent to a sequence of per-entry mapping clauses - // `field(k1) := t1`, `field(k2) := t2`, ... + // `field := [k1 := t1, ...]` — mapping-literal: a batch of + // per-key equations against a morphism-typed codomain field. App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(entries))) if !entries.is_empty() && entries @@ -667,11 +747,10 @@ impl<'a> Elaborator<'a> { let App2(L(_, Keyword(":=")), key_n, target_n) = entry_n.ast0() else { unreachable!("guard ensured all entries are `:=` clauses"); }; - let (key_s, key_v, key_ty) = elab.syn(key_n); - let BaseTyV_::Over(key_path) = &*key_ty else { - let quoted = elab.evaluator().quote_ty(&key_ty); + let (key_s, key_v, key_ty) = elab.fiber_syn(key_n); + let FiberTyV_::Over(key_path) = &*key_ty else { elab.error::<()>(format!( - "mapping-literal key has type {quoted}, expected an element over {}", + "mapping-literal key is not an element over {}", object_path_str(&dom_path), )); entry_failed = true; @@ -686,20 +765,25 @@ impl<'a> Elaborator<'a> { entry_failed = true; break; } - let lhs_ty = BaseTyV::over(cod_path.clone()); - let lhs_s = TmS::over_app(f_seg, f_label, cod_path.clone(), key_s); - let lhs_v = TmV::over_app(f_seg, f_label, cod_path.clone(), key_v); - let (rhs_s, rhs_v) = elab.chk(&lhs_ty, target_n); - eqns_s.push((lhs_s, rhs_s)); - eqns_v.push((lhs_v, rhs_v)); + let lhs_ty_v = FiberTyV::over(cod_path.clone()); + let lhs_s = FiberTmS::over_app(f_seg, f_label, cod_path.clone(), key_s); + let lhs_v = FiberTmV::over_app(f_seg, f_label, cod_path.clone(), key_v); + let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty_v, target_n); + let (eqn, eql) = next_eq_field(&mut eq_count); + fields_s.insert( + eqn, + eql, + FiberTyS::id(FiberTyS::over(cod_path.clone()), lhs_s, rhs_s), + ); + fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty_v, lhs_v, rhs_v)); } if entry_failed { failed = true; continue; } } - // `field := [n1, n2, ...]` — set-literal assignment to - // an object-typed field of the codomain. + // `field := [n1, n2, ...]` — set-literal: declare generators + // in the fiber over an object-typed codomain field. App2(L(_, Keyword(":=")), L(_, Var(field_name)), L(_, Tuple(name_ns))) => { let Some(codomain) = elab.instance_codomain() else { elab.error::<()>( @@ -725,7 +809,6 @@ impl<'a> Elaborator<'a> { continue; } let path = vec![(f_seg, f_label)]; - let gen_ty = BaseTyV::over(path.clone()); for name_n in name_ns.iter() { let Var(gen_name) = name_n.ast0() else { elab.loc = Some(name_n.loc()); @@ -735,15 +818,15 @@ impl<'a> Elaborator<'a> { }; let gen_seg = name_seg(*gen_name); let gen_label = label_seg(*gen_name); - gens.insert(gen_seg, (gen_label, path.clone())); - elab.intro(gen_seg, gen_label, Some(gen_ty.clone())); + elab.intro_fiber(gen_seg, gen_label, FiberTyV::over(path.clone())); + fields_s.insert(gen_seg, gen_label, FiberTyS::over(path.clone())); + fields_v.insert(gen_seg, gen_label, FiberTyV::over(path.clone())); } } - // `mor(arg) := target` — mapping-entry clause: - // an equation witness. + // `mor(arg) := target` — a single equation witness. App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { - let (lhs_s, lhs_v, lhs_ty) = elab.syn(lhs_n); - if !matches!(&*lhs_ty, BaseTyV_::Over(_)) { + let (lhs_s, lhs_v, lhs_ty) = elab.fiber_syn(lhs_n); + let FiberTyV_::Over(over_path) = &*lhs_ty else { elab.loc = Some(lhs_n.loc()); elab.error::<()>( "mapping-entry clause `mor(arg) := target` requires the LHS \ @@ -752,10 +835,20 @@ impl<'a> Elaborator<'a> { ); failed = true; continue; - } - let (rhs_s, rhs_v) = elab.chk(&lhs_ty, rhs_n); - eqns_s.push((lhs_s, rhs_s)); - eqns_v.push((lhs_v, rhs_v)); + }; + let over_path = over_path.clone(); + let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, rhs_n); + let (eqn, eql) = next_eq_field(&mut eq_count); + fields_s.insert( + eqn, + eql, + FiberTyS::id(FiberTyS::over(over_path.clone()), lhs_s, rhs_s), + ); + fields_v.insert( + eqn, + eql, + FiberTyV::id(FiberTyV::over(over_path), lhs_v, rhs_v), + ); } _ => { elab.error::<()>( @@ -767,26 +860,12 @@ impl<'a> Elaborator<'a> { } } - // Assemble the accumulators into the instance payload, unless a - // clause failed — then errors are already reported, so bail with - // an empty instance rather than a half-built one. + // On any failure, errors are already reported, so bail with an + // empty instance rather than a half-built one. if failed { - return ( - TmS::instance(InstanceBodyS::default()), - TmV::instance(InstanceBodyV::default()), - ); + return empty(); } - let body_s = InstanceBodyS { - generators: gens.clone(), - equations: eqns_s, - sub_instances: subs_s, - }; - let body_v = InstanceBodyV { - generators: gens, - equations: eqns_v, - sub_instances: subs_v, - }; - (TmS::instance(body_s), TmV::instance(body_v)) + (FiberTyS::record(fields_s), FiberTyV::record(fields_v)) } fn binding(&mut self, n: &FNtn) -> Option<(VarName, LabelSegment, BaseTyS, BaseTyV)> { @@ -816,23 +895,14 @@ impl<'a> Elaborator<'a> { )) } } - // An instance used in type position yields the representable - // record type synthesized from its body, allowing sub-instance - // imports (`we : Edge`) to project into Edge's fields. - TopDecl::Instance(i) => { - if i.theory == self.theory { - let TmV_::Instance(body) = &*i.val else { - unreachable!("an Instance always has an instance body") - }; - let body_ty = self.evaluator().synth_instance_body_ty(body); - (BaseTyS::topvar(name), body_ty) - } else { - self.ty_error(format!( - "{name} refers to an instance in theory {}, expected theory {}", - i.theory, self.theory - )) - } - } + // An instance is a fiber type, not a base type. It can only + // appear as the annotation of a sub-instance import inside an + // instance body (handled by `fiber_ty`), not in base-type + // position. + TopDecl::Instance(_) => self.ty_error(format!( + "{name} refers to an instance, which is not a base type; \ + an instance can only be imported inside another instance body" + )), TopDecl::Def(_) => self.ty_error(format!("{name} refers to a term not a type")), } } else { @@ -995,12 +1065,12 @@ impl<'a> Elaborator<'a> { App2(L(_, Keyword("==")), tm1_n, tm2_n) => { let (tm1_s, tm1_v, tm1_ty) = elab.syn(tm1_n); let (tm2_s, tm2_v, tm2_ty) = elab.syn(tm2_n); - if !matches!(&*tm1_ty, BaseTyV_::Morphism(_, _, _) | BaseTyV_::Over(_)) { + if !matches!(&*tm1_ty, BaseTyV_::Morphism(_, _, _)) { elab.loc = Some(tm1_n.loc()); - return elab - .ty_error( - "Equality types are only supported for morphisms and elements over an object", - ); + return elab.ty_error( + "Equality types are only supported for morphisms; equations \ + between instance elements live inside an instance body", + ); } if let Err(e) = elab.evaluator().convertible_ty(&tm1_ty, &tm2_ty) { let eval = elab.evaluator(); @@ -1086,46 +1156,9 @@ impl<'a> Elaborator<'a> { elab.evaluator().field_ty(&ty_v, &tm_v, f), ) } - // Applied codomain-morphism syntax. Two shapes: - // - // `f(x)` — `x` is a variable of fiber type in - // scope. - // `f(receiver.fld)` — argument is a record projection - // (e.g. `src(we.e)`). - // - // Both elaborate to a [`TmS_::OverApp`] applying `f` (a - // codomain morphism in the enclosing instance) to the - // resolved argument term. - App1(L(_, Var(f)), L(_, Var(x))) => { - let (inner_s, inner_v, inner_ty) = elab.lookup_tm(ustr(x)); - elab.apply_codomain_morphism(f, inner_s, inner_v, inner_ty, x) - } - App1(L(_, Var(f)), L(_, App1(recv_n, L(_, Field(arg_field))))) => { - if let Var(inst) = recv_n.ast0() - && matches!( - elab.toplevel.declarations.get(&name_seg(*inst)), - Some(TopDecl::Instance(_)) - ) - { - return elab.syn_error( - "cannot project a field out of an instance; an instance is \ - eliminated by mapping out of it, not by projection", - ); - } - let (recv_s, recv_v, recv_ty) = elab.syn(recv_n); - let BaseTyV_::Record(r) = &*recv_ty else { - return elab.syn_error("can only project from record type"); - }; - let arg_name = name_seg(*arg_field); - let arg_label = label_seg(*arg_field); - if !r.fields.has(arg_name) { - return elab.syn_error(format!("no such field {arg_name}")); - } - let arg_ty = elab.evaluator().field_ty(&recv_ty, &recv_v, arg_name); - let arg_s = TmS::proj(recv_s, arg_name, arg_label); - let arg_v = elab.evaluator().proj(&recv_v, arg_name, arg_label); - elab.apply_codomain_morphism(f, arg_s, arg_v, arg_ty, arg_field) - } + // Codomain-morphism application (`src(we.e)`, `f(x)`) is fiber + // syntax, elaborated by `fiber_syn` inside an instance body — it + // is not a base term, so base `syn` does not handle it. App1(L(_, Prim("id")), ob_n) => { let (ob_s, ob_v, ob_t) = elab.syn(ob_n); let BaseTyV_::Object(ob_type) = &*ob_t else { @@ -1328,6 +1361,14 @@ fn object_path_str(path: &[(FieldName, LabelSegment)]) -> String { path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") } +/// The synthetic field name/label `_eqN` for the next auto-named equation +/// field of an instance record, advancing the counter. +fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { + let key = format!("_eq{}", *eq_count); + *eq_count += 1; + (name_seg(key.as_str()), label_seg(key.as_str())) +} + fn tms_to_path(tm: &TmS) -> Option> { match &**tm { TmS_::Var(_, _, _) => Some(vec![]), diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index 0f38a3b3f..bb97bdde8 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -42,19 +42,20 @@ pub struct Type { /// body packaged as the presentation of an `X`-instance. It is declared with /// `instance NAME : X := [...]`. /// -/// When an instance name is used in *type* position (for a sub-instance -/// import), its type is the representable record type synthesized from that -/// body by -/// [`synth_instance_body_ty`](super::eval::Evaluator::synth_instance_body_ty), -/// whose terms are the instance morphisms out of it. +/// The instance is represented directly as a fiber type — a fiber +/// [`Record`](super::stx::FiberTyS_::Record) whose fields are its +/// generators ([`Over`](super::stx::FiberTyS_::Over)), sub-instance +/// imports (nested records), and equations +/// ([`Id`](super::stx::FiberTyS_::Id)). A sub-instance import `we : Edge` +/// uses this fiber type directly. #[derive(Constructor, Clone)] pub struct Instance { /// The theory that the instance is defined in. pub theory: Theory, - /// The syntax of the instance body (unnormalized). - pub stx: TmS, - /// The value of the instance body (normalized); always a [`TmV_::Instance`]. - pub val: TmV, + /// The syntax of the instance, as a fiber record type. + pub stx: FiberTyS, + /// The value of the instance, as a fiber record type. + pub val: FiberTyV, /// The codomain model `X` that this is an instance of. pub codomain: BaseTyV, } diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index fd1b6e0c3..ab4e26390 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -11,6 +11,11 @@ use crate::zero::{LabelSegment, QualifiedName}; /// A way of resolving [BwdIdx] found in [TmS_::Var] to values. pub type Env = Bwd; +/// The fiber environment: resolves [BwdIdx] found in +/// [`super::stx::FiberTmS_::Var`] to fiber-term values. Separate from +/// [Env], the base environment. +pub type FiberEnv = Bwd; + /// The content of a record type value. #[derive(Clone)] pub struct RecordV { @@ -96,12 +101,6 @@ pub enum BaseTyV_ { Id(BaseTyV, TmV, TmV), /// A metavariable, also see [BaseTyS_::Meta]. Meta(MetaVar), - /// The type of terms in a fiber over an object generator of some - /// instance's codomain model. - /// - /// See [BaseTyS_::Over] for the syntactic counterpart and explanation - /// of why instance identity is not part of this type. - Over(Vec<(FieldName, LabelSegment)>), } /// Value for total types, dereferences to [BaseTyV_]. @@ -183,11 +182,6 @@ impl BaseTyV { pub fn meta(mv: MetaVar) -> Self { Self(Rc::new(BaseTyV_::Meta(mv))) } - - /// Smart constructor for [BaseTyV], [BaseTyV_::Over] case. - pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(BaseTyV_::Over(path))) - } } /// Inner enum for [TmN]. @@ -228,18 +222,6 @@ impl TmN { } } -/// Value-level payload of [`TmV_::Instance`]. Parallels -/// [`super::stx::InstanceBodyS`]. -#[derive(Default)] -pub struct InstanceBodyV { - /// Generators introduced by this instance, with their fibers. - pub generators: IndexMap)>, - /// Equation witnesses, asserted to hold in this instance. - pub equations: Vec<(TmV, TmV)>, - /// Sub-instance imports, keyed by import name. - pub sub_instances: IndexMap, -} - /// Inner enum for [TmV]. pub enum TmV_ { /// Neutrals. @@ -248,14 +230,6 @@ pub enum TmV_ { Neu(TmN, BaseTyV), /// Application of an object operation in the theory. App(VarName, TmV), - /// Application of a codomain morphism to an [`Over`-typed](BaseTyV_::Over) - /// term. See [`TmS_::OverApp`] for the syntactic counterpart and - /// argument-by-argument documentation. - OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, TmV), - /// An instance value of a model (sketch) type. See - /// [`super::stx::InstanceBodyS`] for the payload description; this - /// is its value-level counterpart. - Instance(InstanceBodyV), /// Lists of objects. List(Vec), /// Records. @@ -286,21 +260,6 @@ impl TmV { TmV(Rc::new(TmV_::App(name, x))) } - /// Smart constructor for [TmV], [TmV_::OverApp] case. - pub fn over_app( - mor: FieldName, - mor_label: LabelSegment, - tgt_path: Vec<(FieldName, LabelSegment)>, - inner: TmV, - ) -> Self { - TmV(Rc::new(TmV_::OverApp(mor, mor_label, tgt_path, inner))) - } - - /// Smart constructor for [TmV], [TmV_::Instance] case. - pub fn instance(body: InstanceBodyV) -> Self { - TmV(Rc::new(TmV_::Instance(body))) - } - /// Smart constructor for [TmV], [TmV_::List] case. pub fn list(elems: Vec) -> Self { TmV(Rc::new(TmV_::List(elems))) @@ -346,3 +305,93 @@ impl TmV { } } } + +/// Inner enum for [FiberTyV]; value counterpart of [`super::stx::FiberTyS_`]. +/// +/// A fiber record stores its evaluated field types directly (no captured +/// environment, unlike [`RecordV`]): the only fields ever projected are +/// the closed [`Over`](Self::Over) generators, and the dependent +/// [`Id`](Self::Id) equation fields are read off by name downstream +/// (conversion and model generation) rather than re-evaluated. +pub enum FiberTyV_ { + /// The type of a fiber element over the codomain object at `path`. + /// See [`super::stx::FiberTyS_::Over`]. + Over(Vec<(FieldName, LabelSegment)>), + /// An instance presented as a record of fiber types. See + /// [`super::stx::FiberTyS_::Record`]. + Record(Row), + /// A propositional equation between fiber elements. See + /// [`super::stx::FiberTyS_::Id`]. + Id(FiberTyV, FiberTmV, FiberTmV), +} + +/// Values for fiber types, dereferences to [FiberTyV_]. +#[derive(Clone, Deref)] +#[deref(forward)] +pub struct FiberTyV(Rc); + +impl FiberTyV { + /// Smart constructor for [FiberTyV], [FiberTyV_::Over] case. + pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { + Self(Rc::new(FiberTyV_::Over(path))) + } + + /// Smart constructor for [FiberTyV], [FiberTyV_::Record] case. + pub fn record(fields: Row) -> Self { + Self(Rc::new(FiberTyV_::Record(fields))) + } + + /// Smart constructor for [FiberTyV], [FiberTyV_::Id] case. + pub fn id(ty: FiberTyV, tm1: FiberTmV, tm2: FiberTmV) -> Self { + Self(Rc::new(FiberTyV_::Id(ty, tm1, tm2))) + } +} + +/// Inner enum for [FiberTmV]; value counterpart of [`super::stx::FiberTmS_`]. +/// +/// Every fiber term is neutral, so — unlike [`TmV_`] — there is no +/// closure/neutral split and no stored type for eta. Variables carry a +/// forward index into the fiber environment. +pub enum FiberTmV_ { + /// A fiber-context variable (generator or sub-instance import). + Var(FwdIdx, VarName, LabelSegment), + /// Projection of a generator out of a sub-instance import (`we.e`). + Proj(FiberTmV, FieldName, LabelSegment), + /// Application of a codomain morphism to a fiber element. See + /// [`super::stx::FiberTmS_::OverApp`]. + OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, FiberTmV), + /// A metavariable. + Meta(MetaVar), +} + +/// Values for fiber terms, dereferences to [FiberTmV_]. +#[derive(Clone, Deref)] +#[deref(forward)] +pub struct FiberTmV(Rc); + +impl FiberTmV { + /// Smart constructor for [FiberTmV], [FiberTmV_::Var] case. + pub fn var(fwd_idx: FwdIdx, var_name: VarName, label: LabelSegment) -> Self { + Self(Rc::new(FiberTmV_::Var(fwd_idx, var_name, label))) + } + + /// Smart constructor for [FiberTmV], [FiberTmV_::Proj] case. + pub fn proj(tm: FiberTmV, field_name: FieldName, label: LabelSegment) -> Self { + Self(Rc::new(FiberTmV_::Proj(tm, field_name, label))) + } + + /// Smart constructor for [FiberTmV], [FiberTmV_::OverApp] case. + pub fn over_app( + mor: FieldName, + mor_label: LabelSegment, + tgt_path: Vec<(FieldName, LabelSegment)>, + inner: FiberTmV, + ) -> Self { + Self(Rc::new(FiberTmV_::OverApp(mor, mor_label, tgt_path, inner))) + } + + /// Smart constructor for [FiberTmV], [FiberTmV_::Meta] case. + pub fn meta(mv: MetaVar) -> Self { + Self(Rc::new(FiberTmV_::Meta(mv))) + } +} From 6ecc274ac745d9aa7eb02a534c13f4d3e34fea01 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 29 Jun 2026 15:24:54 -0700 Subject: [PATCH 37/53] tt: rename TmS/TmV -> BaseTmS/BaseTmV --- packages/catlog/src/tt/eval.rs | 128 +++++++++++++----------- packages/catlog/src/tt/mod.rs | 52 +++++----- packages/catlog/src/tt/modelgen.rs | 33 +++--- packages/catlog/src/tt/notebook_elab.rs | 32 +++--- packages/catlog/src/tt/stx.rs | 120 +++++++++++----------- packages/catlog/src/tt/text_elab.rs | 66 ++++++------ packages/catlog/src/tt/toplevel.rs | 2 +- packages/catlog/src/tt/val.rs | 90 ++++++++--------- packages/catlog/src/tt/wd.rs | 2 +- 9 files changed, 271 insertions(+), 254 deletions(-) diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 3ea512f2f..5036846ad 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -78,39 +78,41 @@ impl<'a> Evaluator<'a> { /// /// Assumes that the term syntax is well-formed and well-scoped with respect /// to self.env. - pub fn eval_tm(&self, tm: &TmS) -> TmV { + pub fn eval_tm(&self, tm: &BaseTmS) -> BaseTmV { match &**tm { - TmS_::TopApp(tv, args_s) => { + BaseTmS_::TopApp(tv, args_s) => { let env = Env::nil().extend_by(args_s.iter().map(|arg_s| self.eval_tm(arg_s))); let def = self.toplevel.declarations.get(tv).unwrap().clone().unwrap_def(); self.with_env(env).eval_tm(&def.body) } - TmS_::Var(i, _, _) => self.env.get(**i).cloned().unwrap(), - TmS_::Cons(fields) => TmV::cons(fields.map(|tm| self.eval_tm(tm))), - TmS_::Proj(tm, field, label) => self.proj(&self.eval_tm(tm), *field, *label), - TmS_::Id(x) => TmV::id(self.eval_tm(x)), - TmS_::Tab(mor) => TmV::tab(self.eval_tm(mor)), - TmS_::Compose(f, g) => TmV::compose(self.eval_tm(f), self.eval_tm(g)), - TmS_::ObApp(name, x) => TmV::app(*name, self.eval_tm(x)), - TmS_::List(elems) => TmV::list(elems.iter().map(|tm| self.eval_tm(tm)).collect()), - TmS_::Meta(mv) => TmV::meta(*mv), + BaseTmS_::Var(i, _, _) => self.env.get(**i).cloned().unwrap(), + BaseTmS_::Cons(fields) => BaseTmV::cons(fields.map(|tm| self.eval_tm(tm))), + BaseTmS_::Proj(tm, field, label) => self.proj(&self.eval_tm(tm), *field, *label), + BaseTmS_::Id(x) => BaseTmV::id(self.eval_tm(x)), + BaseTmS_::Tab(mor) => BaseTmV::tab(self.eval_tm(mor)), + BaseTmS_::Compose(f, g) => BaseTmV::compose(self.eval_tm(f), self.eval_tm(g)), + BaseTmS_::ObApp(name, x) => BaseTmV::app(*name, self.eval_tm(x)), + BaseTmS_::List(elems) => { + BaseTmV::list(elems.iter().map(|tm| self.eval_tm(tm)).collect()) + } + BaseTmS_::Meta(mv) => BaseTmV::meta(*mv), } } /// Compute the projection of a field from a term value. - pub fn proj(&self, tm: &TmV, field_name: FieldName, field_label: LabelSegment) -> TmV { + pub fn proj(&self, tm: &BaseTmV, field_name: FieldName, field_label: LabelSegment) -> BaseTmV { match &**tm { - TmV_::Neu(n, ty) => TmV::neu( + BaseTmV_::Neu(n, ty) => BaseTmV::neu( TmN::proj(n.clone(), field_name, field_label), self.field_ty(ty, tm, field_name), ), - TmV_::Cons(fields) => fields.get(field_name).cloned().unwrap(), + BaseTmV_::Cons(fields) => fields.get(field_name).cloned().unwrap(), _ => unreachable!("projected field {field_name} from a non-record term value"), } } /// Evaluate the type of the field `field_name` of `val : ty`. - pub fn field_ty(&self, ty: &BaseTyV, val: &TmV, field_name: FieldName) -> BaseTyV { + pub fn field_ty(&self, ty: &BaseTyV, val: &BaseTmV, field_name: FieldName) -> BaseTyV { match &**ty { BaseTyV_::Record(r) => { let field_ty_s = r.fields.get(field_name).unwrap(); @@ -128,7 +130,7 @@ impl<'a> Evaluator<'a> { /// Bind a new neutral of type `ty`. pub fn bind_neu(&self, name: VarName, label: LabelSegment, ty: BaseTyV) -> (TmN, Self) { let n = TmN::var(self.scope_length.into(), name, label); - let v = TmV::neu(n.clone(), ty); + let v = BaseTmV::neu(n.clone(), ty); ( n, Self { @@ -198,26 +200,28 @@ impl<'a> Evaluator<'a> { /// Produce term syntax from a neutral term. /// /// The documentation for [Evaluator::quote_ty] is also applicable here. - pub fn quote_neu(&self, n: &TmN) -> TmS { + pub fn quote_neu(&self, n: &TmN) -> BaseTmS { match &**n { - TmN_::Var(i, name, label) => TmS::var(i.as_bwd(self.scope_length), *name, *label), - TmN_::Proj(tm, field, label) => TmS::proj(self.quote_neu(tm), *field, *label), + TmN_::Var(i, name, label) => BaseTmS::var(i.as_bwd(self.scope_length), *name, *label), + TmN_::Proj(tm, field, label) => BaseTmS::proj(self.quote_neu(tm), *field, *label), } } /// Produce term syntax from a term value. /// /// The documentation for [Evaluator::quote_ty] is also applicable here. - pub fn quote_tm(&self, tm: &TmV) -> TmS { + pub fn quote_tm(&self, tm: &BaseTmV) -> BaseTmS { match &**tm { - TmV_::Neu(n, _) => self.quote_neu(n), - TmV_::App(name, x) => TmS::ob_app(*name, self.quote_tm(x)), - TmV_::List(elems) => TmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()), - TmV_::Cons(fields) => TmS::cons(fields.map(|tm| self.quote_tm(tm))), - TmV_::Id(x) => TmS::id(self.quote_tm(x)), - TmV_::Tab(mor) => TmS::tab(self.quote_tm(mor)), - TmV_::Compose(f, g) => TmS::compose(self.quote_tm(f), self.quote_tm(g)), - TmV_::Meta(mv) => TmS::meta(*mv), + BaseTmV_::Neu(n, _) => self.quote_neu(n), + BaseTmV_::App(name, x) => BaseTmS::ob_app(*name, self.quote_tm(x)), + BaseTmV_::List(elems) => { + BaseTmS::list(elems.iter().map(|tm| self.quote_tm(tm)).collect()) + } + BaseTmV_::Cons(fields) => BaseTmS::cons(fields.map(|tm| self.quote_tm(tm))), + BaseTmV_::Id(x) => BaseTmS::id(self.quote_tm(x)), + BaseTmV_::Tab(mor) => BaseTmS::tab(self.quote_tm(mor)), + BaseTmV_::Compose(f, g) => BaseTmS::compose(self.quote_tm(f), self.quote_tm(g)), + BaseTmV_::Meta(mv) => BaseTmS::meta(*mv), } } @@ -240,7 +244,7 @@ impl<'a> Evaluator<'a> { /// /// Example: if `a : Entity` and `b : Entity` are neutrals, then `a` is not an /// element of `@sing b`, but `a` is an element of `@sing a`. - pub fn element_of<'b>(&self, tm: &TmV, ty: &BaseTyV) -> Result<(), D<'b>> { + pub fn element_of<'b>(&self, tm: &BaseTmV, ty: &BaseTyV) -> Result<(), D<'b>> { match &**ty { BaseTyV_::Object(_) => Ok(()), BaseTyV_::Morphism(_, _, _) => Ok(()), @@ -284,13 +288,13 @@ impl<'a> Evaluator<'a> { for ((name, (label, field_ty1_s)), (_, (_, field_ty2_s))) in r1.fields.iter().zip(r2.fields.iter()) { - let v = TmV::cons(fields.clone().into()); + let v = BaseTmV::cons(fields.clone().into()); let field_ty1_v = self1.with_env(r1.env.snoc(v.clone())).eval_ty(field_ty1_s); let field_ty2_v = self1.with_env(r2.env.snoc(v.clone())).eval_ty(field_ty2_s); self1.convertible_ty(&field_ty1_v, &field_ty2_v)?; let (field_val, self_next) = self.bind_neu(*name, *label, field_ty1_v.clone()); self1 = self_next; - fields.insert(*name, (*label, TmV::neu(field_val, field_ty1_v))); + fields.insert(*name, (*label, BaseTmV::neu(field_val, field_ty1_v))); } Ok(()) } @@ -301,32 +305,34 @@ impl<'a> Evaluator<'a> { } /// Performs eta-expansion of the neutral `n` at type `ty`. - pub fn eta_neu(&self, n: &TmN, ty: &BaseTyV) -> TmV { + pub fn eta_neu(&self, n: &TmN, ty: &BaseTyV) -> BaseTmV { match &**ty { - BaseTyV_::Object(_) => TmV::neu(n.clone(), ty.clone()), - BaseTyV_::Morphism(_, _, _) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Object(_) => BaseTmV::neu(n.clone(), ty.clone()), + BaseTyV_::Morphism(_, _, _) => BaseTmV::neu(n.clone(), ty.clone()), BaseTyV_::Record(r) => { let mut fields = Row::empty(); for (name, (label, _)) in r.fields.iter() { - let ty_v = self.field_ty(ty, &TmV::cons(fields.clone()), *name); + let ty_v = self.field_ty(ty, &BaseTmV::cons(fields.clone()), *name); let v = self.eta_neu(&TmN::proj(n.clone(), *name, *label), &ty_v); fields.insert(*name, *label, v); } - TmV::cons(fields) + BaseTmV::cons(fields) } BaseTyV_::Sing(_, x) => x.clone(), - BaseTyV_::Id(_, _, _) => TmV::empty_cons(), // Extensional equality at a 100% discount! - BaseTyV_::Meta(_) => TmV::neu(n.clone(), ty.clone()), + BaseTyV_::Id(_, _, _) => BaseTmV::empty_cons(), /* Extensional equality at a 100% discount! */ + BaseTyV_::Meta(_) => BaseTmV::neu(n.clone(), ty.clone()), } } /// Performs eta-expansion of the term `v` at type `ty`. - pub fn eta(&self, v: &TmV, ty: Option<&BaseTyV>) -> TmV { + pub fn eta(&self, v: &BaseTmV, ty: Option<&BaseTyV>) -> BaseTmV { match &**v { - TmV_::Neu(tm_n, ty_v) => self.eta_neu(tm_n, ty_v), - TmV_::App(name, x) => TmV::app(*name, self.eta(x, None)), - TmV_::List(elems) => TmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()), - TmV_::Cons(row) => { + BaseTmV_::Neu(tm_n, ty_v) => self.eta_neu(tm_n, ty_v), + BaseTmV_::App(name, x) => BaseTmV::app(*name, self.eta(x, None)), + BaseTmV_::List(elems) => { + BaseTmV::list(elems.iter().map(|elem| self.eta(elem, None)).collect()) + } + BaseTmV_::Cons(row) => { if let Some(ty) = ty { let row = row .iter() @@ -334,17 +340,17 @@ impl<'a> Evaluator<'a> { (*name, (*label, self.eta(field_v, Some(&self.field_ty(ty, v, *name))))) }) .collect(); - TmV::cons(row) + BaseTmV::cons(row) } // Is this right? Couldn't a cons be nested below top-level and so not get expanded right? else { v.clone() } } - TmV_::Id(x) => TmV::id(self.eta(x, None)), - TmV_::Tab(mor) => TmV::tab(self.eta(mor, None)), - TmV_::Compose(f, g) => TmV::compose(self.eta(f, None), self.eta(g, None)), - TmV_::Meta(_) => v.clone(), + BaseTmV_::Id(x) => BaseTmV::id(self.eta(x, None)), + BaseTmV_::Tab(mor) => BaseTmV::tab(self.eta(mor, None)), + BaseTmV_::Compose(f, g) => BaseTmV::compose(self.eta(f, None), self.eta(g, None)), + BaseTmV_::Meta(_) => v.clone(), } } @@ -355,7 +361,7 @@ impl<'a> Evaluator<'a> { /// Assumes that the type of tm1 is convertible with the type of tm2. First /// attempts to do conversion checking without eta-expansion (strict mode), /// and if that fails, does conversion checking with eta-expansion. - pub fn equal_tm<'b>(&self, tm1: &TmV, tm2: &TmV) -> Result<(), D<'b>> { + pub fn equal_tm<'b>(&self, tm1: &BaseTmV, tm2: &BaseTmV) -> Result<(), D<'b>> { if self.equal_tm_helper(tm1, tm2, true, true).is_err() { self.equal_tm_helper(tm1, tm2, false, false) } else { @@ -365,19 +371,19 @@ impl<'a> Evaluator<'a> { fn equal_tm_helper<'b>( &self, - tm1: &TmV, - tm2: &TmV, + tm1: &BaseTmV, + tm2: &BaseTmV, strict1: bool, strict2: bool, ) -> Result<(), D<'b>> { match (&**tm1, &**tm2) { - (TmV_::Neu(n1, ty1), _) if !strict1 => { + (BaseTmV_::Neu(n1, ty1), _) if !strict1 => { self.equal_tm_helper(&self.eta_neu(n1, ty1), tm2, true, strict2) } - (_, TmV_::Neu(n2, ty2)) if !strict2 => { + (_, BaseTmV_::Neu(n2, ty2)) if !strict2 => { self.equal_tm_helper(tm1, &self.eta_neu(n2, ty2), strict1, true) } - (TmV_::Neu(n1, _), TmV_::Neu(n2, _)) => { + (BaseTmV_::Neu(n1, _), BaseTmV_::Neu(n2, _)) => { if n1 == n2 { Ok(()) } else { @@ -388,25 +394,25 @@ impl<'a> Evaluator<'a> { ))) } } - (TmV_::Cons(fields1), TmV_::Cons(fields2)) => { + (BaseTmV_::Cons(fields1), BaseTmV_::Cons(fields2)) => { for ((_, (_, tm1)), (_, (_, tm2))) in fields1.iter().zip(fields2.iter()) { self.equal_tm_helper(tm1, tm2, strict1, strict2)? } Ok(()) } - (TmV_::Meta(mv1), TmV_::Meta(mv2)) => { + (BaseTmV_::Meta(mv1), BaseTmV_::Meta(mv2)) => { if mv1 == mv2 { Ok(()) } else { Err(t(format!("Holes {} and {} are not equal.", mv1, mv2))) } } - (TmV_::Id(x1), TmV_::Id(x2)) => self.equal_tm_helper(x1, x2, strict1, strict2), - (TmV_::Compose(f1, g1), TmV_::Compose(f2, g2)) => { + (BaseTmV_::Id(x1), BaseTmV_::Id(x2)) => self.equal_tm_helper(x1, x2, strict1, strict2), + (BaseTmV_::Compose(f1, g1), BaseTmV_::Compose(f2, g2)) => { self.equal_tm_helper(f1, f2, strict1, strict2)?; self.equal_tm_helper(g1, g2, strict1, strict2) } - (TmV_::Tab(mor1), TmV_::Tab(mor2)) => { + (BaseTmV_::Tab(mor1), BaseTmV_::Tab(mor2)) => { self.equal_tm_helper(mor1, mor2, strict1, strict2) } _ => Err(t(format!( @@ -420,7 +426,7 @@ impl<'a> Evaluator<'a> { fn can_specialize( &self, ty: &BaseTyV, - val: &TmV, + val: &BaseTmV, path: &[(FieldName, LabelSegment)], field_ty: BaseTyV, ) -> Result<(), String> { @@ -444,7 +450,7 @@ impl<'a> Evaluator<'a> { pub fn path_ty( &self, ty: &BaseTyV, - val: &TmV, + val: &BaseTmV, path: &[(FieldName, LabelSegment)], ) -> Result { let mut ty = ty.clone(); diff --git a/packages/catlog/src/tt/mod.rs b/packages/catlog/src/tt/mod.rs index 7c7da95b6..8983b48f1 100644 --- a/packages/catlog/src/tt/mod.rs +++ b/packages/catlog/src/tt/mod.rs @@ -18,7 +18,7 @@ //! //! | | Syntax | Value | //! |------|--------|-------| -//! | Term | [TmS] | [TmV] | +//! | Term | [BaseTmS] | [BaseTmV] | //! | Type | [BaseTyS] | [BaseTyV] | //! //! Evaluation is the process of going from syntax to values. Evaluation is used to @@ -36,28 +36,28 @@ //! ```ignore //! type BwdIdx = usize; //! -//! enum TmS { +//! enum BaseTmS { //! Var(BwdIdx), -//! App(TmS, TmS), -//! Lam(TmS) +//! App(BaseTmS, BaseTmS), +//! Lam(BaseTmS) //! } //! //! type Env = Bwd; //! //! struct Closure { //! env: Env, -//! body: TmS +//! body: BaseTmS //! } //! -//! fn eval(env: Env, tm_s: TmS) -> Closure { +//! fn eval(env: Env, tm_s: BaseTmS) -> Closure { //! match tm_s { -//! TmS::Var(i) => env.lookup(i), -//! TmS::App(f, x) => { +//! BaseTmS::Var(i) => env.lookup(i), +//! BaseTmS::App(f, x) => { //! let fv = eval(env, f); //! let xv = eval(env, x); //! eval(fv.env.snoc(xv), fv.body) //! } -//! TmS::Lam(body) => Closure { env, body } +//! BaseTmS::Lam(body) => Closure { env, body } //! } //! } //! ``` @@ -71,43 +71,43 @@ //! ```ignore //! type FwdIdx = usize; //! -//! enum TmV { +//! enum BaseTmV { //! // f a₁ ... aₙ -//! Neu(FwdIdx, Bwd), +//! Neu(FwdIdx, Bwd), //! Clo(Closure) //! } //! -//! impl TmV { -//! fn app(self, arg: TmV) -> TmV { +//! impl BaseTmV { +//! fn app(self, arg: BaseTmV) -> BaseTmV { //! match self { -//! TmV::Neu(head, args) => TmV::Neu(head, args.snoc(arg)), -//! TmV::Clo(clo) => eval(clo.env.snoc(arg), clo.body) +//! BaseTmV::Neu(head, args) => BaseTmV::Neu(head, args.snoc(arg)), +//! BaseTmV::Clo(clo) => eval(clo.env.snoc(arg), clo.body) //! } //! } //! } //! -//! type Env = Bwd; +//! type Env = Bwd; //! -//! fn eval(env: Env, tm_s: TmS) -> Closure { +//! fn eval(env: Env, tm_s: BaseTmS) -> Closure { //! match tm_s { -//! TmS::Var(i) => env.lookup(i), -//! TmS::App(f, x) => { +//! BaseTmS::Var(i) => env.lookup(i), +//! BaseTmS::App(f, x) => { //! let fv = eval(env, f); //! let xv = eval(env, x); //! fv.app(xv) //! } -//! TmS::Lam(body) => TmV::Clo(Closure { env, body }) +//! BaseTmS::Lam(body) => BaseTmV::Clo(Closure { env, body }) //! } //! } //! -//! fn quote(scope_len: usize, tm_v: TmV) -> TmS { +//! fn quote(scope_len: usize, tm_v: BaseTmV) -> BaseTmS { //! match tm_v { -//! TmV::Neu(f, xs) => -//! xs.iter.fold(TmS::Var(scope_len - f - 1), |f, x| TmS::App(f, x)), -//! TmV::Clo(clo) => { -//! let x_v = TmV::Neu(scope_len, Bwd::Nil); +//! BaseTmV::Neu(f, xs) => +//! xs.iter.fold(BaseTmS::Var(scope_len - f - 1), |f, x| BaseTmS::App(f, x)), +//! BaseTmV::Clo(clo) => { +//! let x_v = BaseTmV::Neu(scope_len, Bwd::Nil); //! let body_v = eval(clo.env.snoc(x_v), clo.body); -//! TmS::Lam(quote(scope_len + 1, body_v)); +//! BaseTmS::Lam(quote(scope_len + 1, body_v)); //! } //! } //! } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index ece8ade08..24b5a3832 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -239,7 +239,7 @@ impl<'a> ModelGenerator<'a> { /// Constructs an application of an object operation, if the theory is modal. /// /// Returns the constructed object along with the expected object type of the result. - fn ob_app(&self, name: &NameSegment, tm_v: &TmV) -> Option<(Ob, ObType)> { + fn ob_app(&self, name: &NameSegment, tm_v: &BaseTmV) -> Option<(Ob, ObType)> { let name: QualifiedName = [*name].into(); match &self.model { Model::Discrete(_) | Model::DiscreteTab(_) => None, @@ -252,7 +252,7 @@ impl<'a> ModelGenerator<'a> { &self, model: &modal::ModalDblModel, name: QualifiedName, - tm_v: &TmV, + tm_v: &BaseTmV, ) -> Option<(Ob, ObType)> { let theory = model.theory(); let op = modal::ModalObOp::generator(name.clone()); @@ -282,9 +282,9 @@ impl<'a> ModelGenerator<'a> { /// Attempts to make an object of a model from a term. /// /// Returns the object together with the appropriate type. - fn make_ob_synth_type(&self, val: &TmV) -> Option<(Ob, ObType)> { + fn make_ob_synth_type(&self, val: &BaseTmV) -> Option<(Ob, ObType)> { match &**val { - TmV_::Neu(n, ty_v) => { + BaseTmV_::Neu(n, ty_v) => { let BaseTyV_::Object(ob_type) = &**ty_v else { return None; }; @@ -298,8 +298,8 @@ impl<'a> ModelGenerator<'a> { }; Some((ob, ob_type.clone())) } - TmV_::App(name, tm_v) => self.ob_app(name, tm_v), - TmV_::Tab(mor_tm_v) => { + BaseTmV_::App(name, tm_v) => self.ob_app(name, tm_v), + BaseTmV_::Tab(mor_tm_v) => { let (mor, mor_type) = self.synth_mor(mor_tm_v)?; Some((self.model.tabulated(mor)?, self.theory.tabulator(mor_type)?)) } @@ -310,10 +310,10 @@ impl<'a> ModelGenerator<'a> { /// Attempts to make an object of a model from a term. /// /// Also checks that the term constructs an object of the given type, returning only the object if successful. - fn make_ob_check_type(&self, val: &TmV, ob_type: &ObType) -> Option { + fn make_ob_check_type(&self, val: &BaseTmV, ob_type: &ObType) -> Option { match &**val { // ob_type checked recursively. - TmV_::List(elems) => { + BaseTmV_::List(elems) => { let el_type = ob_type.clone().list_arg()?; let elems: Option> = elems.iter().map(|tm| self.make_ob_check_type(tm, &el_type)).collect(); @@ -329,21 +329,21 @@ impl<'a> ModelGenerator<'a> { /// Attempts to make a morphism of a model from a term. /// /// Also returns the expected morphism type of the result. - fn synth_mor(&self, val: &TmV) -> Option<(Mor, MorType)> { + fn synth_mor(&self, val: &BaseTmV) -> Option<(Mor, MorType)> { match &**val { - TmV_::Neu(n, ty_v) => { + BaseTmV_::Neu(n, ty_v) => { let BaseTyV_::Morphism(mor_type, _, _) = &**ty_v else { return None; }; let name = n.to_qualified_name(); Some((self.mor_generator(name), mor_type.clone())) } - TmV_::Id(x) => { + BaseTmV_::Id(x) => { let (dom, dom_type) = self.make_ob_synth_type(x)?; let mor_type = self.theory.hom_type(dom_type)?; Some((self.model.id(dom), mor_type)) } - TmV_::Compose(f, g) => { + BaseTmV_::Compose(f, g) => { let (mf, mtf) = self.synth_mor(f)?; let (mg, mtg) = self.synth_mor(g)?; Some((self.model.compose2(mf, mg), self.theory.compose_types2(mtf, mtg)?)) @@ -356,12 +356,17 @@ impl<'a> ModelGenerator<'a> { /// /// At this time, all morphism constructors allow for type synthesis, but /// eventually this will change. - fn make_mor(&self, val: &TmV, mor_type: &MorType) -> Option { + fn make_mor(&self, val: &BaseTmV, mor_type: &MorType) -> Option { let (mor, mt) = self.synth_mor(val)?; (mt == *mor_type).then_some(mor) } - fn extract(&mut self, prefix: Vec, val: &TmV, ty: &BaseTyV) -> Option { + fn extract( + &mut self, + prefix: Vec, + val: &BaseTmV, + ty: &BaseTyV, + ) -> Option { match &**ty { BaseTyV_::Object(ot) => { self.model.add_ob(prefix.into(), ot.clone()); diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 64458477c..a2ffb3405 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -67,8 +67,8 @@ impl<'a> Elaborator<'a> { Evaluator::new(self.toplevel, self.ctx.env.clone(), self.ctx.scope.len()) } - fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { - let v = TmV::neu( + fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> BaseTmV { + let v = BaseTmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), ty.clone().unwrap_or(BaseTyV::empty_record()), ); @@ -115,13 +115,13 @@ impl<'a> Elaborator<'a> { (name, label, ty_s, ty_v) } - fn lookup_tm(&self, name: VarName) -> Option<(TmS, TmV, BaseTyV)> { + fn lookup_tm(&self, name: VarName) -> Option<(BaseTmS, BaseTmV, BaseTyV)> { let (i, label, ty) = self.ctx.lookup(name)?; let v = self.ctx.env.get(*i).unwrap().clone(); - Some((TmS::var(i, name, label), v, ty.clone().unwrap())) + Some((BaseTmS::var(i, name, label), v, ty.clone().unwrap())) } - fn resolve_name(&self, segments: &[VarName]) -> Option<(TmS, TmV, BaseTyV)> { + fn resolve_name(&self, segments: &[VarName]) -> Option<(BaseTmS, BaseTmV, BaseTyV)> { let (&last, rest) = segments.split_last()?; if rest.is_empty() { self.lookup_tm(last) @@ -132,14 +132,14 @@ impl<'a> Elaborator<'a> { }; let &(label, _) = r.fields.get_with_label(last)?; Some(( - TmS::proj(tm_s, last, label), + BaseTmS::proj(tm_s, last, label), self.evaluator().proj(&tm_v, last, label), self.evaluator().field_ty(&ty_v, &tm_v, last), )) } } - fn ob_syn(&self, n: &nb::Ob) -> Option<(TmS, TmV, ObType)> { + fn ob_syn(&self, n: &nb::Ob) -> Option<(BaseTmS, BaseTmV, ObType)> { match n { nb::Ob::Basic(name) => { let name = QualifiedName::deserialize_str(name).unwrap(); @@ -154,8 +154,8 @@ impl<'a> Elaborator<'a> { let ob_op = self.theory().basic_ob_op([name].into())?; let arg_type = self.theory().ob_op_dom(&ob_op); let (arg_stx, arg_val) = self.ob_chk(ob, &arg_type)?; - let stx = TmS::ob_app(name, arg_stx); - let val = TmV::app(name, arg_val); + let stx = BaseTmS::ob_app(name, arg_stx); + let val = BaseTmV::app(name, arg_val); Some((stx, val, self.theory().ob_op_cod(&ob_op))) } nb::Ob::Tabulated(mor) => { @@ -164,13 +164,13 @@ impl<'a> Elaborator<'a> { return None; }; let ob_type = self.theory().tabulator(mt.clone())?; - Some((TmS::tab(mor_stx), TmV::tab(mor_val), ob_type)) + Some((BaseTmS::tab(mor_stx), BaseTmV::tab(mor_val), ob_type)) } _ => None, } } - fn mor_syn(&self, n: &nb::Mor) -> Option<(TmS, TmV, BaseTyV)> { + fn mor_syn(&self, n: &nb::Mor) -> Option<(BaseTmS, BaseTmV, BaseTyV)> { match n { nb::Mor::Basic(name) => { let name = QualifiedName::deserialize_str(name).unwrap(); @@ -206,8 +206,8 @@ impl<'a> Elaborator<'a> { if self.evaluator().equal_tm(cod_first, dom_rest).is_err() { return None; } - let stx = TmS::compose(stx_first, stx_rest); - let val = TmV::compose(val_first, val_rest); + let stx = BaseTmS::compose(stx_first, stx_rest); + let val = BaseTmV::compose(val_first, val_rest); Some(( stx, val, @@ -224,7 +224,7 @@ impl<'a> Elaborator<'a> { } } - fn ob_chk(&self, n: &nb::Ob, ob_type: &ObType) -> Option<(TmS, TmV)> { + fn ob_chk(&self, n: &nb::Ob, ob_type: &ObType) -> Option<(BaseTmS, BaseTmV)> { match n { nb::Ob::List { modality: nb_modality, objects: elems } => { let (modality, ob_type) = ob_type.clone().mode_app()?; @@ -238,7 +238,7 @@ impl<'a> Elaborator<'a> { elem_stxs.push(tm_s); elem_vals.push(tm_v); } - Some((TmS::list(elem_stxs), TmV::list(elem_vals))) + Some((BaseTmS::list(elem_stxs), BaseTmV::list(elem_vals))) } _ => { let (tm_s, tm_v, synthed) = self.ob_syn(n)?; @@ -468,7 +468,7 @@ impl<'a> Elaborator<'a> { field_ty_vs.push((name, (label, ty_v.clone()))); self.ctx.scope.push(VarInContext::new(name, label, Some(ty_v.clone()))); self.ctx.env = - self.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); + self.ctx.env.snoc(BaseTmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } self.reset_to(c); diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 8926fac77..3af8861e7 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -46,7 +46,7 @@ pub enum BaseTyS_ { /// /// A term of type `Morphism(mt, dom, cod)` represents an morphism of morphism /// type `mt` from `dom` to `cod`. - Morphism(MorType, TmS, TmS), + Morphism(MorType, BaseTmS, BaseTmS), /// Type constructor for record types. /// @@ -62,14 +62,14 @@ pub enum BaseTyS_ { /// /// A term `x` of type `Sing(ty, tm)` is a term of `ty` that is convertible with /// `tm`. - Sing(BaseTyS, TmS), + Sing(BaseTyS, BaseTmS), /// Type constructor for identity types. /// /// Example syntax: `a == b` (assuming `a` and `b` are terms that synthesize the same type). /// /// A term `p` of type `a == b` is a proof that `a` and `b` are equal. - Id(BaseTyS, TmS, TmS), + Id(BaseTyS, BaseTmS, BaseTmS), /// Type constructor for specialized types. /// @@ -109,7 +109,7 @@ impl BaseTyS { } /// Smart constructor for [BaseTyS], [BaseTyS_::Morphism] case. - pub fn morphism(morphism_type: MorType, dom: TmS, cod: TmS) -> Self { + pub fn morphism(morphism_type: MorType, dom: BaseTmS, cod: BaseTmS) -> Self { Self(Rc::new(BaseTyS_::Morphism(morphism_type, dom, cod))) } @@ -119,12 +119,12 @@ impl BaseTyS { } /// Smart constructor for [BaseTyS], [BaseTyS_::Sing] case. - pub fn sing(ty: BaseTyS, tm: TmS) -> Self { + pub fn sing(ty: BaseTyS, tm: BaseTmS) -> Self { Self(Rc::new(BaseTyS_::Sing(ty, tm))) } /// Smart constructor for [BaseTyS], [BaseTyS_::Id] case. - pub fn id(ty: BaseTyS, tm1: TmS, tm2: TmS) -> Self { + pub fn id(ty: BaseTyS, tm1: BaseTmS, tm2: BaseTmS) -> Self { Self(Rc::new(BaseTyS_::Id(ty, tm1, tm2))) } @@ -187,121 +187,121 @@ impl fmt::Display for BaseTyS { } } -/// Inner enum for [TmS]. -pub enum TmS_ { +/// Inner enum for [BaseTmS]. +pub enum BaseTmS_ { /// An application of a top-level term judgment to arguments. /// /// A closed term (a nullary `def`, e.g. `tt : Unit`) is the empty-argument /// case `TopApp(name, [])`. - TopApp(TopVarName, Vec), + TopApp(TopVarName, Vec), /// Variable syntax. /// /// We use a backward index, as when we evaluate we store the /// environment in a [bwd::Bwd], and this indexes into that. Var(BwdIdx, VarName, LabelSegment), /// Record introduction. - Cons(Row), + Cons(Row), /// Record elimination. - Proj(TmS, FieldName, LabelSegment), + Proj(BaseTmS, FieldName, LabelSegment), /// Identity morphism at an object. - Id(TmS), + Id(BaseTmS), /// Tabulation of a morphism. - Tab(TmS), + Tab(BaseTmS), /// Composite of two morphisms. - Compose(TmS, TmS), + Compose(BaseTmS, BaseTmS), /// Application of an object operation in the theory. - ObApp(VarName, TmS), + ObApp(VarName, BaseTmS), /// List of objects. - List(Vec), + List(Vec), /// A metavar. /// /// This only appears when we have an error in elaboration. Meta(MetaVar), } -/// Syntax for total terms, dereferences to [TmS_]. +/// Syntax for total terms, dereferences to [BaseTmS_]. /// /// See [crate::tt] for an explanation of what total types are, and for an /// explanation of our approach to Rc pointers in abstract syntax trees. #[derive(Clone, Deref)] #[deref(forward)] -pub struct TmS(Rc); +pub struct BaseTmS(Rc); -impl TmS { - /// Smart constructor for [TmS], [TmS_::TopApp] case. - pub fn topapp(var_name: VarName, args: Vec) -> Self { - Self(Rc::new(TmS_::TopApp(var_name, args))) +impl BaseTmS { + /// Smart constructor for [BaseTmS], [BaseTmS_::TopApp] case. + pub fn topapp(var_name: VarName, args: Vec) -> Self { + Self(Rc::new(BaseTmS_::TopApp(var_name, args))) } - /// Smart constructor for [TmS], [TmS_::Var] case. + /// Smart constructor for [BaseTmS], [BaseTmS_::Var] case. pub fn var(bwd_idx: BwdIdx, var_name: VarName, label: LabelSegment) -> Self { - Self(Rc::new(TmS_::Var(bwd_idx, var_name, label))) + Self(Rc::new(BaseTmS_::Var(bwd_idx, var_name, label))) } - /// Smart constructor for [TmS], [TmS_::Cons] case. - pub fn cons(row: Row) -> Self { - Self(Rc::new(TmS_::Cons(row))) + /// Smart constructor for [BaseTmS], [BaseTmS_::Cons] case. + pub fn cons(row: Row) -> Self { + Self(Rc::new(BaseTmS_::Cons(row))) } - /// Smart constructor for [TmS], [TmS_::Proj] case. - pub fn proj(tm_s: TmS, field_name: FieldName, label: LabelSegment) -> Self { - Self(Rc::new(TmS_::Proj(tm_s, field_name, label))) + /// Smart constructor for [BaseTmS], [BaseTmS_::Proj] case. + pub fn proj(tm_s: BaseTmS, field_name: FieldName, label: LabelSegment) -> Self { + Self(Rc::new(BaseTmS_::Proj(tm_s, field_name, label))) } - /// Smart constructor for [TmS], [TmS_::Id] case. - pub fn id(ob: TmS) -> Self { - Self(Rc::new(TmS_::Id(ob))) + /// Smart constructor for [BaseTmS], [BaseTmS_::Id] case. + pub fn id(ob: BaseTmS) -> Self { + Self(Rc::new(BaseTmS_::Id(ob))) } - /// Smart constructor for [TmS], [TmS_::Tab] case. - pub fn tab(mor: TmS) -> Self { - Self(Rc::new(TmS_::Tab(mor))) + /// Smart constructor for [BaseTmS], [BaseTmS_::Tab] case. + pub fn tab(mor: BaseTmS) -> Self { + Self(Rc::new(BaseTmS_::Tab(mor))) } - /// Smart constructor for [TmS], [TmS_::Compose] case. - pub fn compose(f: TmS, g: TmS) -> Self { - Self(Rc::new(TmS_::Compose(f, g))) + /// Smart constructor for [BaseTmS], [BaseTmS_::Compose] case. + pub fn compose(f: BaseTmS, g: BaseTmS) -> Self { + Self(Rc::new(BaseTmS_::Compose(f, g))) } - /// Smart constructor for [TmS], [TmS_::ObApp] case. - pub fn ob_app(name: VarName, x: TmS) -> Self { - Self(Rc::new(TmS_::ObApp(name, x))) + /// Smart constructor for [BaseTmS], [BaseTmS_::ObApp] case. + pub fn ob_app(name: VarName, x: BaseTmS) -> Self { + Self(Rc::new(BaseTmS_::ObApp(name, x))) } - /// Smart constructor for [TmS], [TmS_::List] case. - pub fn list(elems: Vec) -> Self { - Self(Rc::new(TmS_::List(elems))) + /// Smart constructor for [BaseTmS], [BaseTmS_::List] case. + pub fn list(elems: Vec) -> Self { + Self(Rc::new(BaseTmS_::List(elems))) } - /// Smart constructor for [TmS], [TmS_::Meta] case. + /// Smart constructor for [BaseTmS], [BaseTmS_::Meta] case. pub fn meta(mv: MetaVar) -> Self { - Self(Rc::new(TmS_::Meta(mv))) + Self(Rc::new(BaseTmS_::Meta(mv))) } } -impl ToDoc for TmS { +impl ToDoc for BaseTmS { fn to_doc<'a>(&self) -> D<'a> { match &**self { - TmS_::TopApp(name, args) if args.is_empty() => t(format!("{}", name)), - TmS_::TopApp(name, args) => { + BaseTmS_::TopApp(name, args) if args.is_empty() => t(format!("{}", name)), + BaseTmS_::TopApp(name, args) => { t(format!("{}", name)) + tuple(args.iter().map(|arg| arg.to_doc())) } - TmS_::Var(_, _, label) => t(format!("{}", label)), - TmS_::Proj(tm, _, label) => tm.to_doc() + t(format!(".{}", label)), - TmS_::Cons(fields) => tuple(fields.iter().map(|(_, (label, field))| { + BaseTmS_::Var(_, _, label) => t(format!("{}", label)), + BaseTmS_::Proj(tm, _, label) => tm.to_doc() + t(format!(".{}", label)), + BaseTmS_::Cons(fields) => tuple(fields.iter().map(|(_, (label, field))| { binop(t(":="), t(format!("{}", label)), field.to_doc()) })), - TmS_::Id(ob) => (t("@id") + s() + ob.to_doc()).parens(), - TmS_::Tab(mor) => (t("@tab") + s() + mor.to_doc()).parens(), - TmS_::Compose(f, g) => binop(t("·"), f.to_doc(), g.to_doc()), - TmS_::ObApp(name, x) => unop(t(format!("@{name}")), x.to_doc()), - TmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), - TmS_::Meta(mv) => t(format!("?{}", mv.id)), + BaseTmS_::Id(ob) => (t("@id") + s() + ob.to_doc()).parens(), + BaseTmS_::Tab(mor) => (t("@tab") + s() + mor.to_doc()).parens(), + BaseTmS_::Compose(f, g) => binop(t("·"), f.to_doc(), g.to_doc()), + BaseTmS_::ObApp(name, x) => unop(t(format!("@{name}")), x.to_doc()), + BaseTmS_::List(elems) => tuple(elems.iter().map(|elem| elem.to_doc())), + BaseTmS_::Meta(mv) => t(format!("?{}", mv.id)), } } } -impl fmt::Display for TmS { +impl fmt::Display for BaseTmS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_doc().group().pretty()) } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 29e4c6c0f..223c88eee 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -380,23 +380,23 @@ impl<'a> Elaborator<'a> { self.ty_hole() } - fn syn_hole(&mut self) -> (TmS, TmV, BaseTyV) { + fn syn_hole(&mut self) -> (BaseTmS, BaseTmV, BaseTyV) { let tm_m = self.fresh_meta(); let ty_m = self.fresh_meta(); - (TmS::meta(tm_m), TmV::meta(tm_m), BaseTyV::meta(ty_m)) + (BaseTmS::meta(tm_m), BaseTmV::meta(tm_m), BaseTyV::meta(ty_m)) } - fn syn_error(&mut self, msg: impl Into) -> (TmS, TmV, BaseTyV) { + fn syn_error(&mut self, msg: impl Into) -> (BaseTmS, BaseTmV, BaseTyV) { self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); self.syn_hole() } - fn chk_hole(&mut self) -> (TmS, TmV) { + fn chk_hole(&mut self) -> (BaseTmS, BaseTmV) { let tm_m = self.fresh_meta(); - (TmS::meta(tm_m), TmV::meta(tm_m)) + (BaseTmS::meta(tm_m), BaseTmV::meta(tm_m)) } - fn chk_error(&mut self, msg: impl Into) -> (TmS, TmV) { + fn chk_error(&mut self, msg: impl Into) -> (BaseTmS, BaseTmV) { self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); self.chk_hole() } @@ -405,8 +405,8 @@ impl<'a> Elaborator<'a> { Evaluator::new(self.toplevel, self.ctx.env.clone(), self.ctx.scope.len()) } - fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> TmV { - let v = TmV::neu( + fn intro(&mut self, name: VarName, label: LabelSegment, ty: Option) -> BaseTmV { + let v = BaseTmV::neu( TmN::var(self.ctx.scope.len().into(), name, label), ty.clone().unwrap_or(BaseTyV::empty_record()), ); @@ -1021,8 +1021,10 @@ impl<'a> Elaborator<'a> { let (_, ty_v) = elab.ty(ty_n); field_ty_vs.push((name, (label, ty_v.clone()))); elab.ctx.push_scope(name, label, Some(ty_v.clone())); - elab.ctx.env = - elab.ctx.env.snoc(TmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); + elab.ctx.env = elab + .ctx + .env + .snoc(BaseTmV::neu(TmN::proj(self_var.clone(), name, label), ty_v)); } if failed { return elab.ty_hole(); @@ -1089,12 +1091,12 @@ impl<'a> Elaborator<'a> { } } - fn lookup_tm(&mut self, name: Ustr) -> (TmS, TmV, BaseTyV) { + fn lookup_tm(&mut self, name: Ustr) -> (BaseTmS, BaseTmV, BaseTyV) { let label = label_seg(name); let name = name_seg(name); if let Some((i, _, ty)) = self.ctx.lookup(name) { ( - TmS::var(i, name, label), + BaseTmS::var(i, name, label), self.ctx.env.get(*i).unwrap().clone(), ty.clone().unwrap(), ) @@ -1107,7 +1109,11 @@ impl<'a> Elaborator<'a> { TopDecl::Def(d) if d.args.is_empty() => { let def = d.clone(); let eval = self.evaluator(); - (TmS::topapp(name, vec![]), eval.eval_tm(&def.body), eval.eval_ty(&def.ret_ty)) + ( + BaseTmS::topapp(name, vec![]), + eval.eval_tm(&def.body), + eval.eval_ty(&def.ret_ty), + ) } TopDecl::Def(_) => self.syn_error(format!("{name} must be applied to arguments")), TopDecl::Instance(_) => self.syn_error(format!( @@ -1121,7 +1127,7 @@ impl<'a> Elaborator<'a> { } /// Elaborates a term from notation, returning syntax, value, and synthesized type. - fn syn(&mut self, n: &FNtn) -> (TmS, TmV, BaseTyV) { + fn syn(&mut self, n: &FNtn) -> (BaseTmS, BaseTmV, BaseTyV) { let mut elab = self.enter(n.loc()); match n.ast0() { Var(name) => elab.lookup_tm(ustr(name)), @@ -1151,7 +1157,7 @@ impl<'a> Elaborator<'a> { return elab.syn_error(format!("no such field {f}")); } ( - TmS::proj(tm_s, f, label), + BaseTmS::proj(tm_s, f, label), elab.evaluator().proj(&tm_v, f, label), elab.evaluator().field_ty(&ty_v, &tm_v, f), ) @@ -1168,8 +1174,8 @@ impl<'a> Elaborator<'a> { return elab.syn_error("object type does not have a hom type"); }; ( - TmS::id(ob_s), - TmV::id(ob_v.clone()), + BaseTmS::id(ob_s), + BaseTmV::id(ob_v.clone()), BaseTyV::morphism(mor_type, ob_v.clone(), ob_v), ) } @@ -1181,7 +1187,7 @@ impl<'a> Elaborator<'a> { let Some(ob_type) = elab.theory().tabulator(mor_type.clone()) else { return elab.syn_error("theory does not have tabulators"); }; - (TmS::tab(mor_s), TmV::tab(mor_v.clone()), BaseTyV::object(ob_type)) + (BaseTmS::tab(mor_s), BaseTmV::tab(mor_v.clone()), BaseTyV::object(ob_type)) } App1(L(_, Prim(name)), ob_n) => { let name = name_seg(*name); @@ -1192,7 +1198,7 @@ impl<'a> Elaborator<'a> { let dom = elab.theory().ob_op_dom(&ob_op); let (arg_s, arg_v) = elab.chk(&BaseTyV::object(dom), ob_n); let cod = elab.theory().ob_op_cod(&ob_op); - (TmS::ob_app(name, arg_s), TmV::app(name, arg_v), BaseTyV::object(cod)) + (BaseTmS::ob_app(name, arg_s), BaseTmV::app(name, arg_v), BaseTyV::object(cod)) } App2(L(_, Keyword("*")), f_n, g_n) => { let (f_s, f_v, f_ty) = elab.syn(f_n); @@ -1220,8 +1226,8 @@ impl<'a> Elaborator<'a> { )); } ( - TmS::compose(f_s, g_s), - TmV::compose(f_v, g_v), + BaseTmS::compose(f_s, g_s), + BaseTmV::compose(f_v, g_v), BaseTyV::morphism( elab.theory().compose_types2(f_mt.clone(), g_mt.clone()).unwrap(), f_dom.clone(), @@ -1250,12 +1256,12 @@ impl<'a> Elaborator<'a> { env = env.snoc(arg_v); } let eval = elab.evaluator().with_env(env.clone()); - (TmS::topapp(tv, arg_stxs), eval.eval_tm(&d.body), eval.eval_ty(&d.ret_ty)) + (BaseTmS::topapp(tv, arg_stxs), eval.eval_tm(&d.body), eval.eval_ty(&d.ret_ty)) } Tag("tt") => { // `tt` is the unique element of `Unit`, i.e. the empty record `[]`. let (_, ty_v) = elab.empty_record_ty(); - (TmS::cons(Row::empty()), TmV::cons(Row::empty()), ty_v) + (BaseTmS::cons(Row::empty()), BaseTmV::cons(Row::empty()), ty_v) } Tuple(_) => elab.syn_error("must check against a type in order to construct a record"), Prim("hole") => elab.syn_error("explicit hole"), @@ -1264,7 +1270,7 @@ impl<'a> Elaborator<'a> { } /// Elaborates a term from notation, checking against an expected type, and returning syntax and value. - fn chk(&mut self, ty: &BaseTyV, n: &FNtn) -> (TmS, TmV) { + fn chk(&mut self, ty: &BaseTyV, n: &FNtn) -> (BaseTmS, BaseTmV) { let mut elab = self.enter(n.loc()); match (&**ty, n.ast0()) { (BaseTyV_::Record(r), Tuple(field_ns)) => { @@ -1296,14 +1302,14 @@ impl<'a> Elaborator<'a> { return elab.chk_error("unexpected notation for field"); } }; - let v = TmV::cons(field_vals.clone().into()); + let v = BaseTmV::cons(field_vals.clone().into()); let field_ty_v = elab.evaluator().with_env(r.env.snoc(v.clone())).eval_ty(field_ty_s); let (tm_s, tm_v) = elab.chk(&field_ty_v, tm_n); field_stxs.insert(*name, (*label, tm_s)); field_vals.insert(*name, (*label, tm_v)); } - (TmS::cons(field_stxs.into()), TmV::cons(field_vals.into())) + (BaseTmS::cons(field_stxs.into()), BaseTmV::cons(field_vals.into())) } (BaseTyV_::Object(ob_type), Tuple(ob_ns)) => { let Some(ob_type) = ob_type.clone().list_arg() else { @@ -1318,7 +1324,7 @@ impl<'a> Elaborator<'a> { elem_stxs.push(tm_s); elem_vals.push(tm_v); } - (TmS::list(elem_stxs), TmV::list(elem_vals)) + (BaseTmS::list(elem_stxs), BaseTmV::list(elem_vals)) } (_, Tuple(_)) => elab.chk_error("tuple expected to be record or object/morphism type"), (_, Prim("hole")) => elab.chk_error("explicit hole"), @@ -1369,10 +1375,10 @@ fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { (name_seg(key.as_str()), label_seg(key.as_str())) } -fn tms_to_path(tm: &TmS) -> Option> { +fn tms_to_path(tm: &BaseTmS) -> Option> { match &**tm { - TmS_::Var(_, _, _) => Some(vec![]), - TmS_::Proj(inner, name, label) => { + BaseTmS_::Var(_, _, _) => Some(vec![]), + BaseTmS_::Proj(inner, name, label) => { let mut p = tms_to_path(inner)?; p.push((*name, *label)); Some(p) diff --git a/packages/catlog/src/tt/toplevel.rs b/packages/catlog/src/tt/toplevel.rs index bb97bdde8..157e5980f 100644 --- a/packages/catlog/src/tt/toplevel.rs +++ b/packages/catlog/src/tt/toplevel.rs @@ -72,7 +72,7 @@ pub struct Def { pub ret_ty: BaseTyS, /// The body of the definition (to be evaluated in an environment with /// values for the arguments). - pub body: TmS, + pub body: BaseTmS, } impl TopDecl { diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index ab4e26390..ed71a6c1d 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -8,8 +8,8 @@ use derive_more::Deref; use super::{prelude::*, stx::*, theory::*}; use crate::zero::{LabelSegment, QualifiedName}; -/// A way of resolving [BwdIdx] found in [TmS_::Var] to values. -pub type Env = Bwd; +/// A way of resolving [BwdIdx] found in [BaseTmS_::Var] to values. +pub type Env = Bwd; /// The fiber environment: resolves [BwdIdx] found in /// [`super::stx::FiberTmS_::Var`] to fiber-term values. Separate from @@ -86,7 +86,7 @@ pub enum BaseTyV_ { /// Type constructor for object types, also see [BaseTyS_::Object]. Object(ObType), /// Type constructor for morphism types, also see [BaseTyS_::Morphism]. - Morphism(MorType, TmV, TmV), + Morphism(MorType, BaseTmV, BaseTmV), /// Type constructor for specialized record types. /// /// This is the target of both [BaseTyS_::Specialize] and [BaseTyS_::Record]. @@ -96,9 +96,9 @@ pub enum BaseTyV_ { /// evaluate to a value of form `BaseTyV_::Record(_)`). Record(RecordV), /// Type constructor for singleton types, also see [BaseTyS_::Sing]. - Sing(BaseTyV, TmV), + Sing(BaseTyV, BaseTmV), /// Type constructor for identity types, also see [BaseTyS_::Id]. - Id(BaseTyV, TmV, TmV), + Id(BaseTyV, BaseTmV, BaseTmV), /// A metavariable, also see [BaseTyS_::Meta]. Meta(MetaVar), } @@ -115,7 +115,7 @@ impl BaseTyV { } /// Smart constructor for [BaseTyV], [BaseTyV_::Morphism] case. - pub fn morphism(morphism_type: MorType, dom: TmV, cod: TmV) -> Self { + pub fn morphism(morphism_type: MorType, dom: BaseTmV, cod: BaseTmV) -> Self { Self(Rc::new(BaseTyV_::Morphism(morphism_type, dom, cod))) } @@ -125,12 +125,12 @@ impl BaseTyV { } /// Smart constructor for [BaseTyV], [BaseTyV_::Sing] case. - pub fn sing(ty_v: BaseTyV, tm_v: TmV) -> Self { + pub fn sing(ty_v: BaseTyV, tm_v: BaseTmV) -> Self { Self(Rc::new(BaseTyV_::Sing(ty_v, tm_v))) } /// Smart constructor for [BaseTyV], [BaseTyV_::Id] case. - pub fn id(ty_v: BaseTyV, tm_v1: TmV, tm_v2: TmV) -> Self { + pub fn id(ty_v: BaseTyV, tm_v1: BaseTmV, tm_v2: BaseTmV) -> Self { Self(Rc::new(BaseTyV_::Id(ty_v, tm_v1, tm_v2))) } @@ -193,7 +193,7 @@ pub enum TmN_ { Proj(TmN, FieldName, LabelSegment), } -/// Neutrals for [terms](TmV), dereferences to [TmN_]. +/// Neutrals for [terms](BaseTmV), dereferences to [TmN_]. #[derive(Clone, Deref, PartialEq, Eq)] #[deref(forward)] pub struct TmN(Rc); @@ -222,85 +222,85 @@ impl TmN { } } -/// Inner enum for [TmV]. -pub enum TmV_ { +/// Inner enum for [BaseTmV]. +pub enum BaseTmV_ { /// Neutrals. /// /// We store the type because we need it for eta-expansion. Neu(TmN, BaseTyV), /// Application of an object operation in the theory. - App(VarName, TmV), + App(VarName, BaseTmV), /// Lists of objects. - List(Vec), + List(Vec), /// Records. - Cons(Row), + Cons(Row), /// The identity morphism of an object. - Id(TmV), + Id(BaseTmV), /// The tabulation of a morphism. - Tab(TmV), + Tab(BaseTmV), /// Composition of morphisms. - Compose(TmV, TmV), + Compose(BaseTmV, BaseTmV), /// A metavariable. Meta(MetaVar), } -/// Values for terms, dereferences to [TmV_]. +/// Values for terms, dereferences to [BaseTmV_]. #[derive(Clone, Deref)] #[deref(forward)] -pub struct TmV(Rc); +pub struct BaseTmV(Rc); -impl TmV { - /// Smart constructor for [TmV], [TmV_::Neu] case. +impl BaseTmV { + /// Smart constructor for [BaseTmV], [BaseTmV_::Neu] case. pub fn neu(n: TmN, ty: BaseTyV) -> Self { - TmV(Rc::new(TmV_::Neu(n, ty))) + BaseTmV(Rc::new(BaseTmV_::Neu(n, ty))) } - /// Smart constructor for [TmV], [TmV_::App] case. - pub fn app(name: VarName, x: TmV) -> Self { - TmV(Rc::new(TmV_::App(name, x))) + /// Smart constructor for [BaseTmV], [BaseTmV_::App] case. + pub fn app(name: VarName, x: BaseTmV) -> Self { + BaseTmV(Rc::new(BaseTmV_::App(name, x))) } - /// Smart constructor for [TmV], [TmV_::List] case. - pub fn list(elems: Vec) -> Self { - TmV(Rc::new(TmV_::List(elems))) + /// Smart constructor for [BaseTmV], [BaseTmV_::List] case. + pub fn list(elems: Vec) -> Self { + BaseTmV(Rc::new(BaseTmV_::List(elems))) } - /// Smart constructor for [TmV], [TmV_::Cons] case. - pub fn cons(fields: Row) -> Self { - TmV(Rc::new(TmV_::Cons(fields))) + /// Smart constructor for [BaseTmV], [BaseTmV_::Cons] case. + pub fn cons(fields: Row) -> Self { + BaseTmV(Rc::new(BaseTmV_::Cons(fields))) } /// The empty record value `[]` — the unique element of the empty /// record type. Also serves as the (proof-irrelevant) canonical /// inhabitant of `Id` types under eta. pub fn empty_cons() -> Self { - TmV(Rc::new(TmV_::Cons(Row::empty()))) + BaseTmV(Rc::new(BaseTmV_::Cons(Row::empty()))) } - /// Smart constructor for [TmV], [TmV_::Id] case. - pub fn id(x: TmV) -> Self { - TmV(Rc::new(TmV_::Id(x))) + /// Smart constructor for [BaseTmV], [BaseTmV_::Id] case. + pub fn id(x: BaseTmV) -> Self { + BaseTmV(Rc::new(BaseTmV_::Id(x))) } - /// Smart constructor for [TmV], [TmV_::Tab] case. - pub fn tab(mor: TmV) -> Self { - TmV(Rc::new(TmV_::Tab(mor))) + /// Smart constructor for [BaseTmV], [BaseTmV_::Tab] case. + pub fn tab(mor: BaseTmV) -> Self { + BaseTmV(Rc::new(BaseTmV_::Tab(mor))) } - /// Smart constructor for [TmV], [TmV_::Compose] case. - pub fn compose(f: TmV, g: TmV) -> Self { - TmV(Rc::new(TmV_::Compose(f, g))) + /// Smart constructor for [BaseTmV], [BaseTmV_::Compose] case. + pub fn compose(f: BaseTmV, g: BaseTmV) -> Self { + BaseTmV(Rc::new(BaseTmV_::Compose(f, g))) } - /// Smart constructor for [TmV], [TmV_::Meta] case. + /// Smart constructor for [BaseTmV], [BaseTmV_::Meta] case. pub fn meta(mv: MetaVar) -> Self { - TmV(Rc::new(TmV_::Meta(mv))) + BaseTmV(Rc::new(BaseTmV_::Meta(mv))) } /// Unwraps a neutral term, or panics. pub fn unwrap_neu(&self) -> TmN { match &**self { - TmV_::Neu(n, _) => n.clone(), + BaseTmV_::Neu(n, _) => n.clone(), _ => panic!("expected term to be a neutral"), } } @@ -349,7 +349,7 @@ impl FiberTyV { /// Inner enum for [FiberTmV]; value counterpart of [`super::stx::FiberTmS_`]. /// -/// Every fiber term is neutral, so — unlike [`TmV_`] — there is no +/// Every fiber term is neutral, so — unlike [`BaseTmV_`] — there is no /// closure/neutral split and no stored type for eta. Variables carry a /// forward index into the fiber environment. pub enum FiberTmV_ { diff --git a/packages/catlog/src/tt/wd.rs b/packages/catlog/src/tt/wd.rs index 68d3e95ee..52b70c7dd 100644 --- a/packages/catlog/src/tt/wd.rs +++ b/packages/catlog/src/tt/wd.rs @@ -52,7 +52,7 @@ pub fn record_to_uwd(ty: &BaseTyV) -> Option> { let BaseTyV_::Sing(ty, tm) = &**spec_type else { continue; }; - let (BaseTyV_::Object(ob_type), TmV_::Neu(n, _)) = (&**ty, &**tm) else { + let (BaseTyV_::Object(ob_type), BaseTmV_::Neu(n, _)) = (&**ty, &**tm) else { continue; }; let qual_name = n.to_qualified_name(); From e18c270d31cc88a6ee162d68b83b866cc64d5aba Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 29 Jun 2026 16:35:38 -0700 Subject: [PATCH 38/53] fix bug on importing instances of distinct codomains, introduce FiberTyS::TopVar to match with BaseTyS --- .../examples/tt/text/test_instances.dbltt | 15 +++++- .../tt/text/test_instances.dbltt.snapshot | 24 ++++++++++ packages/catlog/src/tt/eval.rs | 3 -- packages/catlog/src/tt/stx.rs | 22 +++++++-- packages/catlog/src/tt/text_elab.rs | 48 +++++++++++++++++-- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 81873593e..32f33bf51 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -48,4 +48,17 @@ instance SelfNamed : WeightedGraph := [ ] #(should_fail) -syn I.E \ No newline at end of file +syn I.E + +model OtherSchema := [ + W : Entity +] + +instance OtherInst : OtherSchema := [ + W := [w0] +] + +#(should_fail) +instance MismatchedImport : WeightedGraph := [ + bad : OtherInst +] \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 07f4542ea..0f7b208bb 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -101,3 +101,27 @@ syn I.E #/ 51| syn I.E #/ 51| ^^^ +model OtherSchema := [ + W : Entity +] +#/ declared: OtherSchema + +instance OtherInst : OtherSchema := [ + W := [w0] +] +#/ declared: OtherInst +#/ instance generators: +#/ w0 : W + +#(should_fail) +instance MismatchedImport : WeightedGraph := [ + bad : OtherInst +] +#/ declared: MismatchedImport +#/ instance has no generators or equations +#/ expected errors: +#/ error[elab]: cannot import OtherInst: it is an instance of a different model than the enclosing instance +#/ --> examples/tt/text/test_instances.dbltt:63:5 +#/ 63| bad : OtherInst +#/ 63| ^^^^^^^^^^^^^^^ + diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 5036846ad..5f1956c7c 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -51,9 +51,6 @@ impl<'a> Evaluator<'a> { match &**ty { BaseTyS_::TopVar(tv) => match self.toplevel.declarations.get(tv).unwrap() { TopDecl::Type(t) => t.val.clone(), - // Instances are fiber types, not base types, so a base - // top-var never refers to one (the elaborator rejects an - // instance name in base-type position before we get here). _ => panic!("top-level {tv} should be a type declaration"), }, BaseTyS_::Object(ot) => BaseTyV::object(ot.clone()), diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 3af8861e7..bef978afd 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -312,11 +312,19 @@ impl fmt::Display for BaseTmS { /// Fiber types type the fiber world — instances of a model and their /// elements — mirroring how [`BaseTyS`] types the base world (models). /// See [`crate::tt::toplevel`] for the comprehension-category picture. -/// The three constructors parallel the base world: [`Over`](Self::Over) -/// is the atomic fiber-element type, [`Record`](Self::Record) assembles -/// them into an instance, and [`Id`](Self::Id) imposes a (propositional) -/// equation, just as [`BaseTyS_::Id`] does in the base. +/// The constructors parallel the base world: [`TopVar`](Self::TopVar) +/// references a top-level instance, [`Over`](Self::Over) is the atomic +/// fiber-element type, [`Record`](Self::Record) assembles them into an +/// instance, and [`Id`](Self::Id) imposes a (propositional) equation — +/// just as [`BaseTyS_::TopVar`] and [`BaseTyS_::Id`] do in the base. pub enum FiberTyS_ { + /// A reference to a top-level instance declaration, as in a + /// sub-instance import `we : Edge`. Mirrors [`BaseTyS_::TopVar`]: it + /// appears only in *syntax* and exists to preserve the instance's + /// name for display — like base top-vars, it is resolved away in the + /// value world (there is no `FiberTyV_::TopVar`), where it becomes the + /// referenced instance's [`Record`](Self::Record). + TopVar(TopVarName), /// The type of a fiber element lying over the object generator of the /// codomain model identified by `path`. No surface syntax — its /// inhabitants ([`FiberTmS`]) are introduced by set-literal clauses @@ -343,6 +351,11 @@ pub enum FiberTyS_ { pub struct FiberTyS(Rc); impl FiberTyS { + /// Smart constructor for [FiberTyS], [FiberTyS_::TopVar] case. + pub fn topvar(name: TopVarName) -> Self { + Self(Rc::new(FiberTyS_::TopVar(name))) + } + /// Smart constructor for [FiberTyS], [FiberTyS_::Over] case. pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { Self(Rc::new(FiberTyS_::Over(path))) @@ -362,6 +375,7 @@ impl FiberTyS { impl ToDoc for FiberTyS { fn to_doc<'a>(&self) -> D<'a> { match &**self { + FiberTyS_::TopVar(name) => t(format!("{}", name)), FiberTyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), FiberTyS_::Record(fields) => tuple(fields.iter().map(|(_, (label, ty))| { binop(t(":"), t(format!("{}", label)).group(), ty.to_doc()) diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 223c88eee..36bbda0f6 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -510,6 +510,25 @@ impl<'a> Elaborator<'a> { (s, v) } + /// Whether two codomain models agree closely enough to import an + /// instance of one into an instance of the other: identical top-level + /// field names (in order) and convertible field types. + /// + /// This is deliberately stricter than [`Evaluator::convertible_ty`], + /// which for records is positional and ignores field names and arity + /// — so it would wrongly accept e.g. `[V : Entity, E : Entity]` as + /// convertible with `[W : Entity, F : Entity]`. + fn codomains_match(&self, a: &BaseTyV, b: &BaseTyV) -> bool { + if let (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) = (&**a, &**b) { + let names_a: Vec<_> = r1.fields.iter().map(|(n, _)| n).collect(); + let names_b: Vec<_> = r2.fields.iter().map(|(n, _)| n).collect(); + if names_a != names_b { + return false; + } + } + self.evaluator().convertible_ty(a, b).is_ok() + } + /// Elaborate a fiber-type annotation. Used for sub-instance imports /// (`we : Edge`, where `Edge` names a top-level instance) and anonymous /// equations (`name : (a == b)`). @@ -517,11 +536,32 @@ impl<'a> Elaborator<'a> { match n.ast0() { Var(name) => { let topvar = name_seg(*name); - match self.toplevel.declarations.get(&topvar) { - Some(TopDecl::Instance(i)) => Some((i.stx.clone(), i.val.clone())), - _ => self - .error(format!("{name} must reference a top-level instance declaration")), + let (imported_codomain, val) = match self.toplevel.declarations.get(&topvar) { + Some(TopDecl::Instance(i)) => (i.codomain.clone(), i.val.clone()), + _ => { + return self.error(format!( + "{name} must reference a top-level instance declaration" + )); + } + }; + // The imported instance must be an instance of the *same* + // model as the enclosing one — otherwise its `Over` paths + // refer to objects foreign to this codomain, producing a + // malformed instance. + if let Some(cod) = self.instance_codomain() { + let enclosing = BaseTyV::record((*cod).clone()); + if !self.codomains_match(&enclosing, &imported_codomain) { + return self.error(format!( + "cannot import {name}: it is an instance of a different model than \ + the enclosing instance" + )); + } } + // The syntax keeps the instance's name (for display); the + // value is the referenced instance's resolved record, just + // as a base top-var evaluates to its model. See + // [`FiberTyS_::TopVar`]. + Some((FiberTyS::topvar(topvar), val)) } App2(L(_, Keyword("==")), a_n, b_n) => { let (a_s, a_v, a_ty) = self.fiber_syn(a_n); From 5381ad5535e8d13be74f01c77167f05839b212d6 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 1 Jul 2026 17:16:33 -0700 Subject: [PATCH 39/53] elaborate instances over modal codomains (multicat, SMC) --- .../examples/tt/text/test_instances.dbltt | 10 +- .../tt/text/test_instances.dbltt.snapshot | 12 +- .../tt/text/test_modal_instances.dbltt | 66 +++++ .../text/test_modal_instances.dbltt.snapshot | 161 ++++++++++++ packages/catlog/src/tt/eval.rs | 55 +++- packages/catlog/src/tt/modelgen.rs | 15 +- packages/catlog/src/tt/stx.rs | 68 +++-- packages/catlog/src/tt/text_elab.rs | 248 +++++++++--------- packages/catlog/src/tt/val.rs | 34 ++- 9 files changed, 497 insertions(+), 172 deletions(-) create mode 100644 packages/catlog/examples/tt/text/test_modal_instances.dbltt create mode 100644 packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt b/packages/catlog/examples/tt/text/test_instances.dbltt index 32f33bf51..99d7967ff 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_instances.dbltt @@ -9,15 +9,15 @@ model WeightedGraph := [ weight : Attr[E, Weight] ] -instance Edge : WeightedGraph := [ +instance WalkingEdge : WeightedGraph := [ E := [e] ] instance I : WeightedGraph := [ V := [v1, v2], Weight := [w], - we : Edge, - wf : Edge, + we : WalkingEdge, + wf : WalkingEdge, src(we.e) := v1, tgt(we.e) := v2, src(wf.e) := v2, @@ -29,8 +29,8 @@ instance I : WeightedGraph := [ instance I2 : WeightedGraph := [ V := [v1, v2], Weight := [w], - we : Edge, - wf : Edge, + we : WalkingEdge, + wf : WalkingEdge, src := [we.e := v1, wf.e := v2], tgt := [we.e := v2, wf.e := v1], weight := [we.e := w, wf.e := w] diff --git a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot index 0f7b208bb..5dd355161 100644 --- a/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_instances.dbltt.snapshot @@ -11,18 +11,18 @@ model WeightedGraph := [ ] #/ declared: WeightedGraph -instance Edge : WeightedGraph := [ +instance WalkingEdge : WeightedGraph := [ E := [e] ] -#/ declared: Edge +#/ declared: WalkingEdge #/ instance generators: #/ e : E instance I : WeightedGraph := [ V := [v1, v2], Weight := [w], - we : Edge, - wf : Edge, + we : WalkingEdge, + wf : WalkingEdge, src(we.e) := v1, tgt(we.e) := v2, src(wf.e) := v2, @@ -48,8 +48,8 @@ instance I : WeightedGraph := [ instance I2 : WeightedGraph := [ V := [v1, v2], Weight := [w], - we : Edge, - wf : Edge, + we : WalkingEdge, + wf : WalkingEdge, src := [we.e := v1, wf.e := v2], tgt := [we.e := v2, wf.e := v1], weight := [we.e := w, wf.e := w] diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt b/packages/catlog/examples/tt/text/test_modal_instances.dbltt new file mode 100644 index 000000000..9c9727eaa --- /dev/null +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt @@ -0,0 +1,66 @@ +set_theory ThMulticategory + +model SigMonoid := [ + M : Object, + op : Multihom[[M, M], M], + unit : Multihom[[], M] +] + +model SigRig := [ + R : Object, + Add : SigMonoid & [ .M := R ], + Mul : SigMonoid & [ .M := R ] +] + +instance Z2 : SigMonoid := [ + M := [x], + op([x,x]) := unit([]), + op([x,unit([])]) := x, + op([unit([]),x]) := x +] + +instance Z2freeZ2 : SigMonoid := [ + a : Z2, + b : Z2 +] + +instance klein : SigMonoid := [ + p : Z2freeZ2, + op([p.a.x,p.b.x]) := op([p.b.x,p.a.x]), + op([op([p.a.x,p.b.x]),op([p.a.x,p.b.x])]) := unit([]) +] + +instance Z2TheRig : SigRig := [ + Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), + Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), + Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), + Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), + Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), + Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) +] + +set_theory ThSymMonoidalCategory + +model HMW := [ + H : Object, + M : Object, + W : Object, + returns_w : (Hom Object)[H, W], + returns_d : (Hom Object)[H, @tensor []], + fight_h : (Hom Object) [@tensor [H,M], H], + fight_m : (Hom Object) [@tensor [H,M], M], + fight_mm : (Hom Object)[@tensor [H, M], @tensor [M, M]] +] + +instance hm : HMW := [ + H := [hercules,odysseus,beowulf], + M := [hydra,polyphemus,grendel,dragon], + W := [megara, penelope], + fight_h(@tensor [odysseus,polyphemus]) := odysseus, + fight_h(@tensor [beowulf,grendel]) := beowulf, + fight_m(@tensor [beowulf,dragon]) := dragon, + fight_mm(@tensor [hercules,hydra]) := @tensor [hydra,hydra], + returns_w(hercules) := megara, + returns_w(odysseus) := penelope, + returns_d(beowulf) := @tensor [] +] \ No newline at end of file diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot new file mode 100644 index 000000000..90a1e5bc9 --- /dev/null +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -0,0 +1,161 @@ +set_theory ThMulticategory +#/ result: set theory to ThMulticategory + +model SigMonoid := [ + M : Object, + op : Multihom[[M, M], M], + unit : Multihom[[], M] +] +#/ declared: SigMonoid + +model SigRig := [ + R : Object, + Add : SigMonoid & [ .M := R ], + Mul : SigMonoid & [ .M := R ] +] +#/ declared: SigRig + +instance Z2 : SigMonoid := [ + M := [x], + op([x,x]) := unit([]), + op([x,unit([])]) := x, + op([unit([]),x]) := x +] +#/ declared: Z2 +#/ instance generation failed: instance generation only supports discrete double theories + +instance Z2freeZ2 : SigMonoid := [ + a : Z2, + b : Z2 +] +#/ declared: Z2freeZ2 +#/ instance generation failed: instance generation only supports discrete double theories + +instance klein : SigMonoid := [ + p : Z2freeZ2, + op([p.a.x,p.b.x]) := op([p.b.x,p.a.x]), + op([op([p.a.x,p.b.x]),op([p.a.x,p.b.x])]) := unit([]) +] +#/ declared: klein +#/ instance generation failed: instance generation only supports discrete double theories + +instance Z2TheRig : SigRig := [ + Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), + Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), + Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), + Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), + Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), + Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) +] +#/ declared: Z2TheRig +#/ instance generation failed: instance generation only supports discrete double theories +#/ unexpected errors: +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:34:5 +#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), +#/ 34| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:34:44 +#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), +#/ 34| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?3 and ?1 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:34:5 +#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), +#/ 34| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:35:5 +#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), +#/ 35| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:35:44 +#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), +#/ 35| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?8 and ?6 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:35:5 +#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), +#/ 35| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:36:5 +#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), +#/ 36| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:36:44 +#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), +#/ 36| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?13 and ?11 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:36:5 +#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), +#/ 36| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:37:5 +#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), +#/ 37| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:37:44 +#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), +#/ 37| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?18 and ?16 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:37:5 +#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), +#/ 37| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:38:5 +#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), +#/ 38| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:38:45 +#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), +#/ 38| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?23 and ?21 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:38:5 +#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), +#/ 38| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:39:5 +#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) +#/ 39| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` +#/ --> examples/tt/text/test_modal_instances.dbltt:39:45 +#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) +#/ 39| ^^^^^^^^^^^^ +#/ error[elab]: fiber element has the wrong type: +#/ over-types lie over different codomain objects: Holes ?28 and ?26 are not equal. +#/ --> examples/tt/text/test_modal_instances.dbltt:39:5 +#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) +#/ 39| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +set_theory ThSymMonoidalCategory +#/ result: set theory to ThSymMonoidalCategory + +model HMW := [ + H : Object, + M : Object, + W : Object, + returns_w : (Hom Object)[H, W], + returns_d : (Hom Object)[H, @tensor []], + fight_h : (Hom Object) [@tensor [H,M], H], + fight_m : (Hom Object) [@tensor [H,M], M], + fight_mm : (Hom Object)[@tensor [H, M], @tensor [M, M]] +] +#/ declared: HMW + +instance hm : HMW := [ + H := [hercules,odysseus,beowulf], + M := [hydra,polyphemus,grendel,dragon], + W := [megara, penelope], + fight_h(@tensor [odysseus,polyphemus]) := odysseus, + fight_h(@tensor [beowulf,grendel]) := beowulf, + fight_m(@tensor [beowulf,dragon]) := dragon, + fight_mm(@tensor [hercules,hydra]) := @tensor [hydra,hydra], + returns_w(hercules) := megara, + returns_w(odysseus) := penelope, + returns_d(beowulf) := @tensor [] +] +#/ declared: hm +#/ instance generation failed: instance generation only supports discrete double theories + diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index 5f1956c7c..ba67aefbc 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -412,7 +412,35 @@ impl<'a> Evaluator<'a> { (BaseTmV_::Tab(mor1), BaseTmV_::Tab(mor2)) => { self.equal_tm_helper(mor1, mor2, strict1, strict2) } - _ => Err(t(format!( + (BaseTmV_::List(es1), BaseTmV_::List(es2)) => { + if es1.len() != es2.len() { + return Err(t("lists have different lengths")); + } + for (e1, e2) in es1.iter().zip(es2.iter()) { + self.equal_tm_helper(e1, e2, strict1, strict2)?; + } + Ok(()) + } + (BaseTmV_::App(n1, a1), BaseTmV_::App(n2, a2)) => { + if n1 != n2 { + return Err(t(format!("object operations {n1} and {n2} are not equal"))); + } + self.equal_tm_helper(a1, a2, strict1, strict2) + } + // Constructor mismatch. Enumerating the left term's variants + // (rather than `_`) keeps this exhaustive, so a new `BaseTmV_` + // variant fails to compile here until its equality is defined. + ( + BaseTmV_::Neu(_, _) + | BaseTmV_::App(_, _) + | BaseTmV_::List(_) + | BaseTmV_::Cons(_) + | BaseTmV_::Id(_) + | BaseTmV_::Tab(_) + | BaseTmV_::Compose(_, _) + | BaseTmV_::Meta(_), + _, + ) => Err(t(format!( "failed to match terms {} and {}", self.quote_tm(tm1), self.quote_tm(tm2) @@ -507,13 +535,9 @@ impl<'a> Evaluator<'a> { /// Check that two fiber types are convertible. pub fn convertible_fiber_ty<'b>(&self, ty1: &FiberTyV, ty2: &FiberTyV) -> Result<(), D<'b>> { match (&**ty1, &**ty2) { - (FiberTyV_::Over(p1), FiberTyV_::Over(p2)) => { - if p1 == p2 { - Ok(()) - } else { - Err(t("over-types refer to different paths in the codomain")) - } - } + (FiberTyV_::Over(o1), FiberTyV_::Over(o2)) => self + .equal_tm(o1, o2) + .map_err(|d| t("over-types lie over different codomain objects: ") + d), (FiberTyV_::Record(r1), FiberTyV_::Record(r2)) => { if r1.iter().count() != r2.iter().count() { return Err(t("instance records have differing shapes")); @@ -552,6 +576,21 @@ impl<'a> Evaluator<'a> { } self.equal_fiber_tm(t1, t2) } + (FiberTmV_::List(es1), FiberTmV_::List(es2)) => { + if es1.len() != es2.len() { + return Err(t("fiber lists have different lengths")); + } + for (e1, e2) in es1.iter().zip(es2.iter()) { + self.equal_fiber_tm(e1, e2)?; + } + Ok(()) + } + (FiberTmV_::ObApp(n1, a1), FiberTmV_::ObApp(n2, a2)) => { + if n1 != n2 { + return Err(t(format!("object operations {n1} and {n2} are not equal"))); + } + self.equal_fiber_tm(a1, a2) + } (FiberTmV_::OverApp(m1, _, _, i1), FiberTmV_::OverApp(m2, _, _, i2)) => { if m1 != m2 { return Err(t(format!("OverApp morphisms {m1} and {m2} are not equal"))); diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 24b5a3832..2e7ac0857 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -455,16 +455,23 @@ fn extract_instance_record( ) -> Result<(), String> { for (name, (label, field_ty)) in fields.iter() { match &**field_ty { - FiberTyV_::Over(path) => { + FiberTyV_::Over(obj) => { if let NameSegment::Uuid(uuid) = name { namespace.set_label(*uuid, *label); } let mut qsegs = prefix.to_vec(); qsegs.push(*name); let qname: QualifiedName = qsegs.into(); - let fiber: QualifiedName = - path.iter().map(|(seg, _)| *seg).collect::>().into(); - instance.add_generator(qname, fiber); + // The fiber is the codomain object the generator lies over. + // For a plain generator that is a projection `self.V`, whose + // qualified name is `V`. Modal objects (lists, tensors) are + // not yet supported by discrete model generation. + let BaseTmV_::Neu(n, _) = &**obj else { + return Err("model generation does not yet support generators over a modal \ + object (list/tensor)" + .into()); + }; + instance.add_generator(qname, n.to_qualified_name()); } FiberTyV_::Record(sub_fields) => { if let NameSegment::Uuid(uuid) = name { diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index bef978afd..761080559 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -175,12 +175,6 @@ fn path_to_string(path: &[(FieldName, LabelSegment)]) -> String { out } -/// Render an object path as a dotted label string with no leading dot -/// (e.g. `V`, or `we.E` for a nested path), for use inside `Over(...)`. -fn object_path_to_string(path: &[(FieldName, LabelSegment)]) -> String { - path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") -} - impl fmt::Display for BaseTyS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_doc().group().pretty()) @@ -325,12 +319,17 @@ pub enum FiberTyS_ { /// value world (there is no `FiberTyV_::TopVar`), where it becomes the /// referenced instance's [`Record`](Self::Record). TopVar(TopVarName), - /// The type of a fiber element lying over the object generator of the - /// codomain model identified by `path`. No surface syntax — its - /// inhabitants ([`FiberTmS`]) are introduced by set-literal clauses - /// `field := [...]`, by projection out of a sub-instance import, and - /// by codomain-morphism application inside an instance body. - Over(Vec<(FieldName, LabelSegment)>), + /// The type of a fiber element lying over a codomain object `obj`. + /// + /// `obj` is a base object *term* (rooted at the codomain model), so it + /// may be a plain generator (`self.V`), or a modal object such as a + /// list `[M, M]` or a tensor `@tensor [H, M]`. Comparing two + /// `Over` types is comparing their base objects, so modal objects need + /// no special handling. No surface syntax — its inhabitants + /// ([`FiberTmS`]) are introduced by set-literal clauses `field := + /// [...]`, projection out of a sub-instance import, fiber list/object + /// -operation literals, and codomain-morphism application. + Over(BaseTmS), /// An instance of a model — an object of the fiber over the codomain /// model — presented as a record of fiber types. A generator is an /// [`Over`](Self::Over) field, a sub-instance import is a nested @@ -357,8 +356,8 @@ impl FiberTyS { } /// Smart constructor for [FiberTyS], [FiberTyS_::Over] case. - pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(FiberTyS_::Over(path))) + pub fn over(obj: BaseTmS) -> Self { + Self(Rc::new(FiberTyS_::Over(obj))) } /// Smart constructor for [FiberTyS], [FiberTyS_::Record] case. @@ -376,7 +375,7 @@ impl ToDoc for FiberTyS { fn to_doc<'a>(&self) -> D<'a> { match &**self { FiberTyS_::TopVar(name) => t(format!("{}", name)), - FiberTyS_::Over(path) => t(format!("Over({})", object_path_to_string(path))), + FiberTyS_::Over(obj) => t("Over(") + obj.to_doc() + t(")"), FiberTyS_::Record(fields) => tuple(fields.iter().map(|(_, (label, ty))| { binop(t(":"), t(format!("{}", label)).group(), ty.to_doc()) })), @@ -407,16 +406,27 @@ pub enum FiberTmS_ { /// Projection of a generator out of a sub-instance import, e.g. /// `we.e`. Proj(FiberTmS, FieldName, LabelSegment), + /// A fiber list literal `[a, b, ...]` (possibly empty). Its fiber type + /// is `Over([A, B, ...])` where each `x_i : Over(A_i)`. Mirrors base + /// [`BaseTmS_::List`]; used to supply the (modal) list argument of a + /// multi-ary morphism, e.g. `op[x, x]`. + List(Vec), + /// Application of a theory object-operation to a fiber element, e.g. + /// `@tensor [a, b]`. Mirrors base [`BaseTmS_::ObApp`]; its fiber type + /// is `Over(@op ...)` over the operation applied to the argument's + /// base object. + ObApp(VarName, FiberTmS), /// Application of a codomain morphism to a fiber element. Arguments, /// in order: the morphism name (e.g. `src`), its display label, the - /// codomain object-path it lands at (stored so the result fiber type - /// is recoverable without consulting the codomain), and the - /// fiber-typed argument (e.g. the elaboration of `we.e`). + /// codomain object it lands at (a base object term, stored so the + /// result fiber type is recoverable without re-deriving it), and the + /// fiber-typed argument (e.g. the elaboration of `we.e`, or a fiber + /// list `[x, x]` for a multi-ary morphism). /// /// Example: in `src(we.e) := v1`, the LHS elaborates to - /// `OverApp(src, src, [(V, V)], Proj(Var(we), e, e))` of fiber type - /// `Over(.V)`. - OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, FiberTmS), + /// `OverApp(src, src, self.V, Proj(Var(we), e, e))` of fiber type + /// `Over(self.V)`. + OverApp(FieldName, LabelSegment, BaseTmS, FiberTmS), /// A metavar (elaboration-error placeholder). Meta(MetaVar), } @@ -437,14 +447,24 @@ impl FiberTmS { Self(Rc::new(FiberTmS_::Proj(tm, field_name, label))) } + /// Smart constructor for [FiberTmS], [FiberTmS_::List] case. + pub fn list(elems: Vec) -> Self { + Self(Rc::new(FiberTmS_::List(elems))) + } + + /// Smart constructor for [FiberTmS], [FiberTmS_::ObApp] case. + pub fn ob_app(name: VarName, arg: FiberTmS) -> Self { + Self(Rc::new(FiberTmS_::ObApp(name, arg))) + } + /// Smart constructor for [FiberTmS], [FiberTmS_::OverApp] case. pub fn over_app( mor: FieldName, mor_label: LabelSegment, - tgt_path: Vec<(FieldName, LabelSegment)>, + cod: BaseTmS, inner: FiberTmS, ) -> Self { - Self(Rc::new(FiberTmS_::OverApp(mor, mor_label, tgt_path, inner))) + Self(Rc::new(FiberTmS_::OverApp(mor, mor_label, cod, inner))) } /// Smart constructor for [FiberTmS], [FiberTmS_::Meta] case. @@ -458,6 +478,8 @@ impl ToDoc for FiberTmS { match &**self { FiberTmS_::Var(_, _, label) => t(format!("{}", label)), FiberTmS_::Proj(tm, _, label) => tm.to_doc() + t(format!(".{}", label)), + FiberTmS_::List(elems) => tuple(elems.iter().map(|e| e.to_doc())), + FiberTmS_::ObApp(name, arg) => unop(t(format!("@{name}")), arg.to_doc()), FiberTmS_::OverApp(_, mor_label, _, inner) => { inner.to_doc() + t(format!(".{mor_label}")) } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 36bbda0f6..2c5e3c201 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -438,7 +438,8 @@ impl<'a> Elaborator<'a> { fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { let tm_m = self.fresh_meta(); - (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(Vec::new())) + let obj_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(BaseTmV::meta(obj_m))) } fn fiber_syn_error(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV, FiberTyV) { @@ -481,7 +482,27 @@ impl<'a> Elaborator<'a> { field_ty, ) } - // Codomain-morphism application: `f(arg)`. + // A theory object-operation on a fiber element, e.g. + // `@tensor [a, b]`. The resulting element lies over the + // operation applied to the argument's base object. + App1(L(_, Prim(op)), arg_n) => { + let op_name = name_seg(*op); + if elab.theory().basic_ob_op([op_name].into()).is_none() { + let th = elab.theory.name.to_string(); + return elab.fiber_syn_error(format!("operation @{op} not in theory {th}")); + } + let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n); + let FiberTyV_::Over(arg_obj) = &*arg_ty else { + return elab.fiber_syn_error(format!("@{op} applied to a non-fiber-element")); + }; + let obj = BaseTmV::app(op_name, arg_obj.clone()); + ( + FiberTmS::ob_app(op_name, arg_s), + FiberTmV::ob_app(op_name, arg_v), + FiberTyV::over(obj), + ) + } + // Codomain-morphism application: `f[arg]` / `f(arg)`. App1(L(_, Var(f)), arg_n) => { // A display label for the argument, used only in errors. let label = match arg_n.ast0() { @@ -492,9 +513,29 @@ impl<'a> Elaborator<'a> { let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n); elab.apply_codomain_morphism(f, arg_s, arg_v, arg_ty, &label) } + // A fiber list literal `[a, b, ...]` (the argument of a + // multi-ary morphism); its object is the list of the elements' + // objects. + Tuple(elems) => { + let mut ss = Vec::with_capacity(elems.len()); + let mut vs = Vec::with_capacity(elems.len()); + let mut objs = Vec::with_capacity(elems.len()); + for e in elems.iter() { + let (s, v, ty) = elab.fiber_syn(e); + let FiberTyV_::Over(o) = &*ty else { + return elab.fiber_syn_error( + "fiber list elements must be elements over an object", + ); + }; + objs.push(o.clone()); + ss.push(s); + vs.push(v); + } + (FiberTmS::list(ss), FiberTmV::list(vs), FiberTyV::over(BaseTmV::list(objs))) + } _ => elab.fiber_syn_error( - "expected a fiber element: a generator, a projection `we.e`, or a \ - codomain-morphism application `f(..)`", + "expected a fiber element: a generator, a projection `we.e`, a fiber list \ + `[..]`, an object operation `@op [..]`, or a morphism application `f[..]`", ), } } @@ -572,14 +613,14 @@ impl<'a> Elaborator<'a> { e.pretty() )); } - let FiberTyV_::Over(path) = &*a_ty else { + let FiberTyV_::Over(obj) = &*a_ty else { return self.error( "instance equations must be between elements over an object \ (fiber elements); morphism equations constrain the model, not \ an instance", ); }; - let over_s = FiberTyS::over(path.clone()); + let over_s = FiberTyS::over(self.evaluator().quote_tm(obj)); Some((FiberTyS::id(over_s, a_s, b_s), FiberTyV::id(a_ty.clone(), a_v, b_v))) } _ => self.error("expected an instance name or an equation `a == b`"), @@ -593,9 +634,27 @@ impl<'a> Elaborator<'a> { (BaseTyS::record(Row::empty()), BaseTyV::empty_record()) } - /// Apply a codomain morphism `f` to an already-elaborated argument - /// of fiber type. Shared by the bare `f(x)` and `f(receiver.fld)` - /// arms of [`Self::syn`]. + /// The value of the codomain `self` binding — the eta-expanded model + /// record. Codomain object values (`self.V`, morphism dom/cod) are + /// obtained by projecting / evaluating field types against it, so that + /// every codomain object is rooted at the same `self` neutral and thus + /// compares equal under [`Evaluator::equal_tm`]. + fn codomain_self_value(&self) -> Option { + let (i, _, _) = self.ctx.lookup(name_seg(Self::CODOMAIN_BINDER))?; + self.ctx.env.get(*i).cloned() + } + + /// The codomain object `self.` (a base object value), for a + /// generator declared over the object-typed codomain field `field`. + fn codomain_object(&self, field: FieldName, label: LabelSegment) -> Option { + Some(self.evaluator().proj(&self.codomain_self_value()?, field, label)) + } + + /// Apply a codomain morphism `f` to an already-elaborated fiber + /// argument. The argument's `Over` object must equal the morphism's + /// domain object (compared as base objects, so modal domains — lists, + /// tensors — need no special handling); the result lies over the + /// morphism's codomain object. fn apply_codomain_morphism( &mut self, f: &str, @@ -609,34 +668,41 @@ impl<'a> Elaborator<'a> { "applied codomain morphism is only allowed inside an instance body", ); }; - let FiberTyV_::Over(src_path) = &*arg_ty else { + let FiberTyV_::Over(arg_obj) = &*arg_ty else { return self.fiber_syn_error(format!( "argument {arg_label_str} is not an element over an object", )); }; + let arg_obj = arg_obj.clone(); let f_label = label_seg(f); let f_name = name_seg(f); - let Some(mor_ty_s) = codomain.fields.get(f_name) else { + if !codomain.fields.has(f_name) { return self.fiber_syn_error(format!("no such codomain morphism {f_name}")); + } + let Some(self_val) = self.codomain_self_value() else { + return self.fiber_syn_error( + "applied codomain morphism is only allowed inside an instance body", + ); }; - let BaseTyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { + let record_ty = BaseTyV::record((*codomain).clone()); + let mor_ty = self.evaluator().field_ty(&record_ty, &self_val, f_name); + let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &*mor_ty else { return self.fiber_syn_error(format!("codomain field {f_name} is not a morphism")); }; - let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) else { - return self.fiber_syn_error(format!( - "codomain morphism {f_name} has non-path dom/cod; \ - applied-morphism syntax requires both to be paths", - )); - }; - if dom_path != *src_path { + if let Err(e) = self.evaluator().equal_tm(&arg_obj, dom_obj) { + let ev = self.evaluator(); return self.fiber_syn_error(format!( - "codomain morphism {f_name} has source path differing from the argument", + "argument to {f_name} lies over {}, but {f_name} expects {}:\n{}", + ev.quote_tm(&arg_obj), + ev.quote_tm(dom_obj), + e.pretty() )); } + let cod_s = self.evaluator().quote_tm(cod_obj); ( - FiberTmS::over_app(f_name, f_label, cod_path.clone(), arg_s), - FiberTmV::over_app(f_name, f_label, cod_path.clone(), arg_v), - FiberTyV::over(cod_path), + FiberTmS::over_app(f_name, f_label, cod_s, arg_s), + FiberTmV::over_app(f_name, f_label, cod_obj.clone(), arg_v), + FiberTyV::over(cod_obj.clone()), ) } @@ -758,29 +824,16 @@ impl<'a> Elaborator<'a> { continue; }; let f_seg = name_seg(*field_name); - let f_label = label_seg(*field_name); - let Some(mor_ty_s) = codomain.fields.get(f_seg) else { + if !codomain.fields.has(f_seg) { elab.error::<()>(format!("no such codomain field {field_name}")); failed = true; continue; - }; - let BaseTyS_::Morphism(_, dom_s, cod_s) = &**mor_ty_s else { - elab.error::<()>(format!( - "mapping-literal assignment requires field {field_name} to be \ - morphism-typed", - )); - failed = true; - continue; - }; - let (Some(dom_path), Some(cod_path)) = (tms_to_path(dom_s), tms_to_path(cod_s)) - else { - elab.error::<()>(format!( - "codomain morphism {field_name} has non-path dom/cod; \ - mapping-literal assignment requires both to be paths", - )); - failed = true; - continue; - }; + } + // Each `key := target` entry is the equation + // `field(key) == target`: apply the codomain morphism + // to the key (which also checks the key's object against + // the morphism's domain and yields the codomain object), + // then equate the result to the target. let mut entry_failed = false; for entry_n in entries.iter() { elab.loc = Some(entry_n.loc()); @@ -788,34 +841,18 @@ impl<'a> Elaborator<'a> { unreachable!("guard ensured all entries are `:=` clauses"); }; let (key_s, key_v, key_ty) = elab.fiber_syn(key_n); - let FiberTyV_::Over(key_path) = &*key_ty else { - elab.error::<()>(format!( - "mapping-literal key is not an element over {}", - object_path_str(&dom_path), - )); + let label = format!("{field_name} key"); + let (lhs_s, lhs_v, lhs_ty) = + elab.apply_codomain_morphism(field_name, key_s, key_v, key_ty, &label); + let FiberTyV_::Over(cod_obj) = &*lhs_ty else { entry_failed = true; break; }; - if key_path != &dom_path { - elab.error::<()>(format!( - "mapping-literal key is an element over {}, but {field_name} expects an element over {}", - object_path_str(key_path), - object_path_str(&dom_path), - )); - entry_failed = true; - break; - } - let lhs_ty_v = FiberTyV::over(cod_path.clone()); - let lhs_s = FiberTmS::over_app(f_seg, f_label, cod_path.clone(), key_s); - let lhs_v = FiberTmV::over_app(f_seg, f_label, cod_path.clone(), key_v); - let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty_v, target_n); + let over_s = FiberTyS::over(elab.evaluator().quote_tm(cod_obj)); + let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, target_n); let (eqn, eql) = next_eq_field(&mut eq_count); - fields_s.insert( - eqn, - eql, - FiberTyS::id(FiberTyS::over(cod_path.clone()), lhs_s, rhs_s), - ); - fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty_v, lhs_v, rhs_v)); + fields_s.insert(eqn, eql, FiberTyS::id(over_s, lhs_s, rhs_s)); + fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); } if entry_failed { failed = true; @@ -848,7 +885,16 @@ impl<'a> Elaborator<'a> { failed = true; continue; } - let path = vec![(f_seg, f_label)]; + // Generators lie over the codomain object `self.`. + let Some(gen_obj_v) = elab.codomain_object(f_seg, f_label) else { + elab.error::<()>( + "set-literal field assignment is only allowed inside an \ + instance body", + ); + failed = true; + continue; + }; + let gen_obj_s = elab.evaluator().quote_tm(&gen_obj_v); for name_n in name_ns.iter() { let Var(gen_name) = name_n.ast0() else { elab.loc = Some(name_n.loc()); @@ -858,37 +904,31 @@ impl<'a> Elaborator<'a> { }; let gen_seg = name_seg(*gen_name); let gen_label = label_seg(*gen_name); - elab.intro_fiber(gen_seg, gen_label, FiberTyV::over(path.clone())); - fields_s.insert(gen_seg, gen_label, FiberTyS::over(path.clone())); - fields_v.insert(gen_seg, gen_label, FiberTyV::over(path.clone())); + elab.intro_fiber(gen_seg, gen_label, FiberTyV::over(gen_obj_v.clone())); + fields_s.insert(gen_seg, gen_label, FiberTyS::over(gen_obj_s.clone())); + fields_v.insert(gen_seg, gen_label, FiberTyV::over(gen_obj_v.clone())); } } // `mor(arg) := target` — a single equation witness. App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { let (lhs_s, lhs_v, lhs_ty) = elab.fiber_syn(lhs_n); - let FiberTyV_::Over(over_path) = &*lhs_ty else { - elab.loc = Some(lhs_n.loc()); - elab.error::<()>( - "mapping-entry clause `mor(arg) := target` requires the LHS \ - to be an element over an object (a fiber element); morphism \ - equations constrain the model, not an instance", - ); - failed = true; - continue; + let over_s = match &*lhs_ty { + FiberTyV_::Over(obj) => FiberTyS::over(elab.evaluator().quote_tm(obj)), + _ => { + elab.loc = Some(lhs_n.loc()); + elab.error::<()>( + "mapping-entry clause `mor(arg) := target` requires the LHS \ + to be an element over an object (a fiber element); morphism \ + equations constrain the model, not an instance", + ); + failed = true; + continue; + } }; - let over_path = over_path.clone(); let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, rhs_n); let (eqn, eql) = next_eq_field(&mut eq_count); - fields_s.insert( - eqn, - eql, - FiberTyS::id(FiberTyS::over(over_path.clone()), lhs_s, rhs_s), - ); - fields_v.insert( - eqn, - eql, - FiberTyV::id(FiberTyV::over(over_path), lhs_v, rhs_v), - ); + fields_s.insert(eqn, eql, FiberTyS::id(over_s, lhs_s, rhs_s)); + fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); } _ => { elab.error::<()>( @@ -1393,20 +1433,6 @@ impl<'a> Elaborator<'a> { } } -/// Read off a path of `(name, label)` segments from a term that is a chain -/// of projections rooted in a variable. -/// -/// The leading variable is treated as the implicit root (e.g., the `self` -/// of an enclosing record) and contributes no segment. So `Var(self)` -/// returns `[]` and `Proj(Var(self), E, E_label)` returns `[(E, E_label)]`, -/// matching the path representation produced by the surface `.E` syntax. -/// Returns `None` if the term has any other shape. -/// Render an object path (e.g. `[(V, V)]`) as a dotted label string -/// (e.g. `V`, or `we.E` for a nested path) for use in error messages. -fn object_path_str(path: &[(FieldName, LabelSegment)]) -> String { - path.iter().map(|(_, seg)| seg.to_string()).collect::>().join(".") -} - /// The synthetic field name/label `_eqN` for the next auto-named equation /// field of an instance record, advancing the counter. fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { @@ -1415,18 +1441,6 @@ fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { (name_seg(key.as_str()), label_seg(key.as_str())) } -fn tms_to_path(tm: &BaseTmS) -> Option> { - match &**tm { - BaseTmS_::Var(_, _, _) => Some(vec![]), - BaseTmS_::Proj(inner, name, label) => { - let mut p = tms_to_path(inner)?; - p.push((*name, *label)); - Some(p) - } - _ => None, - } -} - // NOTE: Most tests for the text elaborator are in the `examples` dir. #[cfg(test)] mod tests { diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index ed71a6c1d..f6821a80e 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -314,9 +314,9 @@ impl BaseTmV { /// [`Id`](Self::Id) equation fields are read off by name downstream /// (conversion and model generation) rather than re-evaluated. pub enum FiberTyV_ { - /// The type of a fiber element over the codomain object at `path`. - /// See [`super::stx::FiberTyS_::Over`]. - Over(Vec<(FieldName, LabelSegment)>), + /// The type of a fiber element over the codomain object `obj` (a base + /// object value, possibly modal). See [`super::stx::FiberTyS_::Over`]. + Over(BaseTmV), /// An instance presented as a record of fiber types. See /// [`super::stx::FiberTyS_::Record`]. Record(Row), @@ -332,8 +332,8 @@ pub struct FiberTyV(Rc); impl FiberTyV { /// Smart constructor for [FiberTyV], [FiberTyV_::Over] case. - pub fn over(path: Vec<(FieldName, LabelSegment)>) -> Self { - Self(Rc::new(FiberTyV_::Over(path))) + pub fn over(obj: BaseTmV) -> Self { + Self(Rc::new(FiberTyV_::Over(obj))) } /// Smart constructor for [FiberTyV], [FiberTyV_::Record] case. @@ -357,9 +357,15 @@ pub enum FiberTmV_ { Var(FwdIdx, VarName, LabelSegment), /// Projection of a generator out of a sub-instance import (`we.e`). Proj(FiberTmV, FieldName, LabelSegment), - /// Application of a codomain morphism to a fiber element. See + /// A fiber list literal. See [`super::stx::FiberTmS_::List`]. + List(Vec), + /// A theory object-operation applied to a fiber element. See + /// [`super::stx::FiberTmS_::ObApp`]. + ObApp(VarName, FiberTmV), + /// Application of a codomain morphism to a fiber element; the third + /// field is the codomain object it lands at. See /// [`super::stx::FiberTmS_::OverApp`]. - OverApp(FieldName, LabelSegment, Vec<(FieldName, LabelSegment)>, FiberTmV), + OverApp(FieldName, LabelSegment, BaseTmV, FiberTmV), /// A metavariable. Meta(MetaVar), } @@ -380,14 +386,24 @@ impl FiberTmV { Self(Rc::new(FiberTmV_::Proj(tm, field_name, label))) } + /// Smart constructor for [FiberTmV], [FiberTmV_::List] case. + pub fn list(elems: Vec) -> Self { + Self(Rc::new(FiberTmV_::List(elems))) + } + + /// Smart constructor for [FiberTmV], [FiberTmV_::ObApp] case. + pub fn ob_app(name: VarName, arg: FiberTmV) -> Self { + Self(Rc::new(FiberTmV_::ObApp(name, arg))) + } + /// Smart constructor for [FiberTmV], [FiberTmV_::OverApp] case. pub fn over_app( mor: FieldName, mor_label: LabelSegment, - tgt_path: Vec<(FieldName, LabelSegment)>, + cod: BaseTmV, inner: FiberTmV, ) -> Self { - Self(Rc::new(FiberTmV_::OverApp(mor, mor_label, tgt_path, inner))) + Self(Rc::new(FiberTmV_::OverApp(mor, mor_label, cod, inner))) } /// Smart constructor for [FiberTmV], [FiberTmV_::Meta] case. From ada1ab060f88a40fdce0a9fd65cbdeb6ff690639 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Wed, 1 Jul 2026 18:46:09 -0700 Subject: [PATCH 40/53] support nested codomain-morphism paths in instances (Add.op) --- .../text/test_modal_instances.dbltt.snapshot | 79 ------------------- packages/catlog/src/tt/eval.rs | 9 ++- packages/catlog/src/tt/modelgen.rs | 6 +- packages/catlog/src/tt/stx.rs | 32 ++++---- packages/catlog/src/tt/text_elab.rs | 65 +++++++++++---- packages/catlog/src/tt/val.rs | 17 ++-- 6 files changed, 82 insertions(+), 126 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot index 90a1e5bc9..4aebafe4d 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -49,85 +49,6 @@ instance Z2TheRig : SigRig := [ ] #/ declared: Z2TheRig #/ instance generation failed: instance generation only supports discrete double theories -#/ unexpected errors: -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:34:5 -#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), -#/ 34| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:34:44 -#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), -#/ 34| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?3 and ?1 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:34:5 -#/ 34| Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), -#/ 34| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:35:5 -#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), -#/ 35| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:35:44 -#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), -#/ 35| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?8 and ?6 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:35:5 -#/ 35| Add.op([Mul.unit([]),Add.unit([])]) := Mul.unit([]), -#/ 35| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:36:5 -#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), -#/ 36| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:36:44 -#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), -#/ 36| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?13 and ?11 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:36:5 -#/ 36| Add.op([Add.unit([]),Mul.unit([])]) := Mul.unit([]), -#/ 36| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:37:5 -#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), -#/ 37| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:37:44 -#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), -#/ 37| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?18 and ?16 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:37:5 -#/ 37| Mul.op([Add.unit([]),Add.unit([])]) := Add.unit([]), -#/ 37| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:38:5 -#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), -#/ 38| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:38:45 -#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), -#/ 38| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?23 and ?21 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:38:5 -#/ 38| Mul.op([Mul.unit([]), Add.unit([])]) := Add.unit([]), -#/ 38| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:39:5 -#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) -#/ 39| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -#/ error[elab]: expected a fiber element: a generator, a projection `we.e`, a fiber list `[..]`, an object operation `@op [..]`, or a morphism application `f[..]` -#/ --> examples/tt/text/test_modal_instances.dbltt:39:45 -#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) -#/ 39| ^^^^^^^^^^^^ -#/ error[elab]: fiber element has the wrong type: -#/ over-types lie over different codomain objects: Holes ?28 and ?26 are not equal. -#/ --> examples/tt/text/test_modal_instances.dbltt:39:5 -#/ 39| Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) -#/ 39| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ set_theory ThSymMonoidalCategory #/ result: set theory to ThSymMonoidalCategory diff --git a/packages/catlog/src/tt/eval.rs b/packages/catlog/src/tt/eval.rs index ba67aefbc..cc7aa481e 100644 --- a/packages/catlog/src/tt/eval.rs +++ b/packages/catlog/src/tt/eval.rs @@ -591,9 +591,12 @@ impl<'a> Evaluator<'a> { } self.equal_fiber_tm(a1, a2) } - (FiberTmV_::OverApp(m1, _, _, i1), FiberTmV_::OverApp(m2, _, _, i2)) => { - if m1 != m2 { - return Err(t(format!("OverApp morphisms {m1} and {m2} are not equal"))); + (FiberTmV_::OverApp(p1, _, i1), FiberTmV_::OverApp(p2, _, i2)) => { + // Compare the morphism paths by name. + let names1 = p1.iter().map(|(n, _)| n).collect::>(); + let names2 = p2.iter().map(|(n, _)| n).collect::>(); + if names1 != names2 { + return Err(t("applied codomain morphisms are not equal")); } self.equal_fiber_tm(i1, i2) } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 2e7ac0857..76afe6c06 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -510,8 +510,10 @@ fn fiber_tm_to_discrete_instance_term( let mut cur = tm; let base: QualifiedName = loop { match &**cur { - FiberTmV_::OverApp(mor_name, _, _, inner) => { - mors_outer_first.push(QualifiedName::single(*mor_name)); + FiberTmV_::OverApp(mor_path, _, inner) => { + let name: QualifiedName = + mor_path.iter().map(|(seg, _)| *seg).collect::>().into(); + mors_outer_first.push(name); cur = inner; } _ => { diff --git a/packages/catlog/src/tt/stx.rs b/packages/catlog/src/tt/stx.rs index 761080559..5c144876a 100644 --- a/packages/catlog/src/tt/stx.rs +++ b/packages/catlog/src/tt/stx.rs @@ -417,16 +417,17 @@ pub enum FiberTmS_ { /// base object. ObApp(VarName, FiberTmS), /// Application of a codomain morphism to a fiber element. Arguments, - /// in order: the morphism name (e.g. `src`), its display label, the - /// codomain object it lands at (a base object term, stored so the - /// result fiber type is recoverable without re-deriving it), and the - /// fiber-typed argument (e.g. the elaboration of `we.e`, or a fiber - /// list `[x, x]` for a multi-ary morphism). + /// in order: the *path* to the morphism in the codomain (a single + /// segment like `src`, or a nested one like `Add.op` for a morphism of + /// a sub-model), the codomain object it lands at (a base object term, + /// stored so the result fiber type is recoverable without re-deriving + /// it), and the fiber-typed argument (e.g. the elaboration of `we.e`, + /// or a fiber list `[x, x]` for a multi-ary morphism). /// /// Example: in `src(we.e) := v1`, the LHS elaborates to - /// `OverApp(src, src, self.V, Proj(Var(we), e, e))` of fiber type + /// `OverApp([src], self.V, Proj(Var(we), e, e))` of fiber type /// `Over(self.V)`. - OverApp(FieldName, LabelSegment, BaseTmS, FiberTmS), + OverApp(Vec<(FieldName, LabelSegment)>, BaseTmS, FiberTmS), /// A metavar (elaboration-error placeholder). Meta(MetaVar), } @@ -458,13 +459,8 @@ impl FiberTmS { } /// Smart constructor for [FiberTmS], [FiberTmS_::OverApp] case. - pub fn over_app( - mor: FieldName, - mor_label: LabelSegment, - cod: BaseTmS, - inner: FiberTmS, - ) -> Self { - Self(Rc::new(FiberTmS_::OverApp(mor, mor_label, cod, inner))) + pub fn over_app(mor: Vec<(FieldName, LabelSegment)>, cod: BaseTmS, inner: FiberTmS) -> Self { + Self(Rc::new(FiberTmS_::OverApp(mor, cod, inner))) } /// Smart constructor for [FiberTmS], [FiberTmS_::Meta] case. @@ -480,8 +476,12 @@ impl ToDoc for FiberTmS { FiberTmS_::Proj(tm, _, label) => tm.to_doc() + t(format!(".{}", label)), FiberTmS_::List(elems) => tuple(elems.iter().map(|e| e.to_doc())), FiberTmS_::ObApp(name, arg) => unop(t(format!("@{name}")), arg.to_doc()), - FiberTmS_::OverApp(_, mor_label, _, inner) => { - inner.to_doc() + t(format!(".{mor_label}")) + FiberTmS_::OverApp(path, _, inner) => { + let mut d = inner.to_doc(); + for (_, label) in path { + d = d + t(format!(".{label}")); + } + d } FiberTmS_::Meta(mv) => t(format!("?{}", mv.id)), } diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index 2c5e3c201..d7a44978d 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -502,8 +502,16 @@ impl<'a> Elaborator<'a> { FiberTyV::over(obj), ) } - // Codomain-morphism application: `f[arg]` / `f(arg)`. - App1(L(_, Var(f)), arg_n) => { + // Codomain-morphism application `f(arg)`. The morphism `f` may + // be a nested path into the codomain (e.g. `Add.op`), so its + // head is a projection chain, not just a bare variable. + App1(head_n, arg_n) => { + let Some(path) = morphism_path(head_n) else { + return elab.fiber_syn_error( + "expected a codomain morphism (a name or path like `Add.op`) applied \ + to a fiber element", + ); + }; // A display label for the argument, used only in errors. let label = match arg_n.ast0() { Var(x) => x.to_string(), @@ -511,7 +519,7 @@ impl<'a> Elaborator<'a> { _ => "argument".to_string(), }; let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n); - elab.apply_codomain_morphism(f, arg_s, arg_v, arg_ty, &label) + elab.apply_codomain_morphism(&path, arg_s, arg_v, arg_ty, &label) } // A fiber list literal `[a, b, ...]` (the argument of a // multi-ary morphism); its object is the list of the elements' @@ -657,7 +665,7 @@ impl<'a> Elaborator<'a> { /// morphism's codomain object. fn apply_codomain_morphism( &mut self, - f: &str, + path: &[(FieldName, LabelSegment)], arg_s: FiberTmS, arg_v: FiberTmV, arg_ty: FiberTyV, @@ -674,25 +682,30 @@ impl<'a> Elaborator<'a> { )); }; let arg_obj = arg_obj.clone(); - let f_label = label_seg(f); - let f_name = name_seg(f); - if !codomain.fields.has(f_name) { - return self.fiber_syn_error(format!("no such codomain morphism {f_name}")); - } let Some(self_val) = self.codomain_self_value() else { return self.fiber_syn_error( "applied codomain morphism is only allowed inside an instance body", ); }; + // Resolve the morphism's type by walking its (possibly nested) + // path into the codomain model, e.g. `Add.op`. let record_ty = BaseTyV::record((*codomain).clone()); - let mor_ty = self.evaluator().field_ty(&record_ty, &self_val, f_name); + let mor_ty = match self.evaluator().path_ty(&record_ty, &self_val, path) { + Ok(ty) => ty, + Err(e) => { + return self + .fiber_syn_error(format!("no such codomain morphism {}: {e}", path_str(path))); + } + }; let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &*mor_ty else { - return self.fiber_syn_error(format!("codomain field {f_name} is not a morphism")); + return self + .fiber_syn_error(format!("codomain field {} is not a morphism", path_str(path))); }; if let Err(e) = self.evaluator().equal_tm(&arg_obj, dom_obj) { let ev = self.evaluator(); return self.fiber_syn_error(format!( - "argument to {f_name} lies over {}, but {f_name} expects {}:\n{}", + "argument to {} lies over {}, but it expects {}:\n{}", + path_str(path), ev.quote_tm(&arg_obj), ev.quote_tm(dom_obj), e.pretty() @@ -700,8 +713,8 @@ impl<'a> Elaborator<'a> { } let cod_s = self.evaluator().quote_tm(cod_obj); ( - FiberTmS::over_app(f_name, f_label, cod_s, arg_s), - FiberTmV::over_app(f_name, f_label, cod_obj.clone(), arg_v), + FiberTmS::over_app(path.to_vec(), cod_s, arg_s), + FiberTmV::over_app(path.to_vec(), cod_obj.clone(), arg_v), FiberTyV::over(cod_obj.clone()), ) } @@ -842,8 +855,9 @@ impl<'a> Elaborator<'a> { }; let (key_s, key_v, key_ty) = elab.fiber_syn(key_n); let label = format!("{field_name} key"); + let mor_path = vec![(name_seg(*field_name), label_seg(*field_name))]; let (lhs_s, lhs_v, lhs_ty) = - elab.apply_codomain_morphism(field_name, key_s, key_v, key_ty, &label); + elab.apply_codomain_morphism(&mor_path, key_s, key_v, key_ty, &label); let FiberTyV_::Over(cod_obj) = &*lhs_ty else { entry_failed = true; break; @@ -1433,6 +1447,27 @@ impl<'a> Elaborator<'a> { } } +/// Extract the path to a codomain morphism from the head of an +/// application: a bare variable `f` gives `[f]`, and a projection chain +/// `Add.op` gives `[Add, op]`. Returns `None` for any other shape. +fn morphism_path(n: &FNtn) -> Option> { + match n.ast0() { + Var(f) => Some(vec![(name_seg(*f), label_seg(*f))]), + App1(recv, L(_, Field(g))) => { + let mut p = morphism_path(recv)?; + p.push((name_seg(*g), label_seg(*g))); + Some(p) + } + _ => None, + } +} + +/// Render a morphism/object path as dotted labels (e.g. `Add.op`), for +/// error messages. +fn path_str(path: &[(FieldName, LabelSegment)]) -> String { + path.iter().map(|(_, label)| label.to_string()).collect::>().join(".") +} + /// The synthetic field name/label `_eqN` for the next auto-named equation /// field of an instance record, advancing the counter. fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { diff --git a/packages/catlog/src/tt/val.rs b/packages/catlog/src/tt/val.rs index f6821a80e..b5c683afc 100644 --- a/packages/catlog/src/tt/val.rs +++ b/packages/catlog/src/tt/val.rs @@ -362,10 +362,10 @@ pub enum FiberTmV_ { /// A theory object-operation applied to a fiber element. See /// [`super::stx::FiberTmS_::ObApp`]. ObApp(VarName, FiberTmV), - /// Application of a codomain morphism to a fiber element; the third - /// field is the codomain object it lands at. See - /// [`super::stx::FiberTmS_::OverApp`]. - OverApp(FieldName, LabelSegment, BaseTmV, FiberTmV), + /// Application of a codomain morphism (identified by its path) to a + /// fiber element; the second field is the codomain object it lands at. + /// See [`super::stx::FiberTmS_::OverApp`]. + OverApp(Vec<(FieldName, LabelSegment)>, BaseTmV, FiberTmV), /// A metavariable. Meta(MetaVar), } @@ -397,13 +397,8 @@ impl FiberTmV { } /// Smart constructor for [FiberTmV], [FiberTmV_::OverApp] case. - pub fn over_app( - mor: FieldName, - mor_label: LabelSegment, - cod: BaseTmV, - inner: FiberTmV, - ) -> Self { - Self(Rc::new(FiberTmV_::OverApp(mor, mor_label, cod, inner))) + pub fn over_app(mor: Vec<(FieldName, LabelSegment)>, cod: BaseTmV, inner: FiberTmV) -> Self { + Self(Rc::new(FiberTmV_::OverApp(mor, cod, inner))) } /// Smart constructor for [FiberTmV], [FiberTmV_::Meta] case. From 0cc997c4a3d5f1a832ec2d33e03bb7a2dcff385e Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 6 Jul 2026 11:34:46 -0700 Subject: [PATCH 41/53] DblModalModel datastructure --- packages/catlog/src/dbl/modal/mod.rs | 2 + .../catlog/src/dbl/modal/model_instance.rs | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 packages/catlog/src/dbl/modal/model_instance.rs diff --git a/packages/catlog/src/dbl/modal/mod.rs b/packages/catlog/src/dbl/modal/mod.rs index fe527ba76..1fed83b2c 100644 --- a/packages/catlog/src/dbl/modal/mod.rs +++ b/packages/catlog/src/dbl/modal/mod.rs @@ -1,7 +1,9 @@ //! Doctrine of modal double theories. pub mod model; +pub mod model_instance; pub mod theory; pub use model::*; +pub use model_instance::*; pub use theory::*; diff --git a/packages/catlog/src/dbl/modal/model_instance.rs b/packages/catlog/src/dbl/modal/model_instance.rs new file mode 100644 index 000000000..f634c7735 --- /dev/null +++ b/packages/catlog/src/dbl/modal/model_instance.rs @@ -0,0 +1,57 @@ +//! Instances of models of a modal double theory. + +use crate::dbl::model_instance::{DblModelInstance, HasInstanceTerm, InstanceTerm}; +use crate::dbl::theory::DblTheoryKind; +use crate::zero::QualifiedName; + +use super::model::{ModalDblModel, ModalMor}; +use super::theory::List; + +/// A term in an instance of a modal double model: a single model morphism +/// applied to a base built from instance generators. +/// +/// As in the discrete case, composition of model morphisms is reflected +/// inside [`mor`](Self::mor) itself — via [`ModalMor::Composite`] for +/// sequential composition and [`ModalMor::List`] for list-tupling — rather +/// than by nesting term constructors. A tree-shaped multicategory composite +/// such as `f([g(x), y])` therefore normalizes to *one* [`ModalMor`] applied +/// *once* to a base of bare generators; applications never nest. When `mor` +/// is the identity (`ModalMor::Composite(Path::Id(ob))`), the term denotes +/// `base` directly, and that `Id` object must agree with the fiber of `base` +/// in the surrounding instance. +/// +/// The only recursion that survives lives in [`base`](Self::base), and only +/// through [`ModalInstanceBase::List`], mirroring [`ModalOb::List`](super::model::ModalOb::List): +/// this is what lets a generator over a nested list object be written inline +/// as e.g. `[x, y]` rather than forced to be a single named generator. A base +/// can never hold a morphism, so the "no application inside an application" +/// invariant is enforced structurally. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ModalInstanceTerm { + /// Model morphism applied to `base`. + pub mor: ModalMor, + /// The base of instance generators at the root of the term. + pub base: ModalInstanceBase, +} + +/// The base of a [`ModalInstanceTerm`]: instance generators, tupled into +/// lists to match list-shaped fibers. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ModalInstanceBase { + /// A single instance generator. + Generator(QualifiedName), + /// A list of bases in a [list modality](List), living over a + /// [list object](super::model::ModalOb::List). + List(List, Vec), +} + +impl InstanceTerm for ModalInstanceTerm { + type Mor = ModalMor; +} + +impl HasInstanceTerm for ModalDblModel { + type Term = ModalInstanceTerm; +} + +/// An instance of a model of a modal double theory. +pub type ModalDblModelInstance = DblModelInstance>; From a61939a2cba33a8ad4d0da81b822b442eef11807 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Mon, 6 Jul 2026 15:04:47 -0700 Subject: [PATCH 42/53] multicategory instances good to go --- .../text/test_modal_instances.dbltt.snapshot | 41 ++- .../catlog/src/dbl/modal/model_instance.rs | 22 +- packages/catlog/src/tt/batch.rs | 162 ++++++++-- packages/catlog/src/tt/modelgen.rs | 288 ++++++++++++++++-- 4 files changed, 462 insertions(+), 51 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot index 4aebafe4d..08b56abff 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -22,14 +22,28 @@ instance Z2 : SigMonoid := [ op([unit([]),x]) := x ] #/ declared: Z2 -#/ instance generation failed: instance generation only supports discrete double theories +#/ instance generators: +#/ x : M +#/ instance equations: +#/ op([x, x]) == unit([]) +#/ op([x, unit([])]) == x +#/ op([unit([]), x]) == x instance Z2freeZ2 : SigMonoid := [ a : Z2, b : Z2 ] #/ declared: Z2freeZ2 -#/ instance generation failed: instance generation only supports discrete double theories +#/ instance generators: +#/ a.x : M +#/ b.x : M +#/ instance equations: +#/ op([a.x, a.x]) == unit([]) +#/ op([a.x, unit([])]) == a.x +#/ op([unit([]), a.x]) == a.x +#/ op([b.x, b.x]) == unit([]) +#/ op([b.x, unit([])]) == b.x +#/ op([unit([]), b.x]) == b.x instance klein : SigMonoid := [ p : Z2freeZ2, @@ -37,7 +51,18 @@ instance klein : SigMonoid := [ op([op([p.a.x,p.b.x]),op([p.a.x,p.b.x])]) := unit([]) ] #/ declared: klein -#/ instance generation failed: instance generation only supports discrete double theories +#/ instance generators: +#/ p.a.x : M +#/ p.b.x : M +#/ instance equations: +#/ op([p.a.x, p.a.x]) == unit([]) +#/ op([p.a.x, unit([])]) == p.a.x +#/ op([unit([]), p.a.x]) == p.a.x +#/ op([p.b.x, p.b.x]) == unit([]) +#/ op([p.b.x, unit([])]) == p.b.x +#/ op([unit([]), p.b.x]) == p.b.x +#/ op([p.a.x, p.b.x]) == op([p.b.x, p.a.x]) +#/ op([op([p.a.x, p.b.x]), op([p.a.x, p.b.x])]) == unit([]) instance Z2TheRig : SigRig := [ Add.op([Mul.unit([]),Mul.unit([])]) := Add.unit([]), @@ -48,7 +73,13 @@ instance Z2TheRig : SigRig := [ Mul.op([Add.unit([]), Mul.unit([])]) := Add.unit([]) ] #/ declared: Z2TheRig -#/ instance generation failed: instance generation only supports discrete double theories +#/ instance equations: +#/ Add.op([Mul.unit([]), Mul.unit([])]) == Add.unit([]) +#/ Add.op([Mul.unit([]), Add.unit([])]) == Mul.unit([]) +#/ Add.op([Add.unit([]), Mul.unit([])]) == Mul.unit([]) +#/ Mul.op([Add.unit([]), Add.unit([])]) == Add.unit([]) +#/ Mul.op([Mul.unit([]), Add.unit([])]) == Add.unit([]) +#/ Mul.op([Add.unit([]), Mul.unit([])]) == Add.unit([]) set_theory ThSymMonoidalCategory #/ result: set theory to ThSymMonoidalCategory @@ -78,5 +109,5 @@ instance hm : HMW := [ returns_d(beowulf) := @tensor [] ] #/ declared: hm -#/ instance generation failed: instance generation only supports discrete double theories +#/ instance generation failed: instance terms do not yet support object-operation applications (e.g. `@tensor`) diff --git a/packages/catlog/src/dbl/modal/model_instance.rs b/packages/catlog/src/dbl/modal/model_instance.rs index f634c7735..281a1687b 100644 --- a/packages/catlog/src/dbl/modal/model_instance.rs +++ b/packages/catlog/src/dbl/modal/model_instance.rs @@ -2,9 +2,10 @@ use crate::dbl::model_instance::{DblModelInstance, HasInstanceTerm, InstanceTerm}; use crate::dbl::theory::DblTheoryKind; +use crate::one::path::Path; use crate::zero::QualifiedName; -use super::model::{ModalDblModel, ModalMor}; +use super::model::{ModalDblModel, ModalMor, ModalOb}; use super::theory::List; /// A term in an instance of a modal double model: a single model morphism @@ -21,7 +22,7 @@ use super::theory::List; /// in the surrounding instance. /// /// The only recursion that survives lives in [`base`](Self::base), and only -/// through [`ModalInstanceBase::List`], mirroring [`ModalOb::List`](super::model::ModalOb::List): +/// through [`ModalInstanceBase::List`], mirroring [`ModalOb::List`]: /// this is what lets a generator over a nested list object be written inline /// as e.g. `[x, y]` rather than forced to be a single named generator. A base /// can never hold a morphism, so the "no application inside an application" @@ -55,3 +56,20 @@ impl HasInstanceTerm for ModalDblModel { /// An instance of a model of a modal double theory. pub type ModalDblModelInstance = DblModelInstance>; + +/// If `mor` is an identity morphism (`Composite(Path::Id(ob))`), returns the +/// object it is the identity on. +/// +/// Instance-term normalization uses this to keep terms in their flat normal +/// form: an identity `mor` means the term denotes its [`base`](ModalInstanceTerm::base) +/// directly, so a nested application whose argument is a pure base of +/// generators need not introduce a `Composite`/`List` wrapper. +pub fn modal_mor_as_identity(mor: &ModalMor) -> Option<&ModalOb> { + match mor { + ModalMor::Composite(path) => match path.as_ref() { + Path::Id(ob) => Some(ob), + _ => None, + }, + _ => None, + } +} diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index e11811bc4..62a911a28 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -11,8 +11,17 @@ use scopeguard::guard; use tattle::display::SourceInfo; use tattle::{Reporter, declare_error}; -use super::{modelgen::instance_from_def, text_elab::*, theory::std_theories, toplevel::*}; -use crate::dbl::discrete::{DiscreteDblModelInstance, DiscreteInstanceTerm}; +use super::{ + modelgen::{ModelInstance, instance_from_def}, + text_elab::*, + theory::std_theories, + toplevel::*, +}; +use crate::dbl::discrete::DiscreteInstanceTerm; +use crate::dbl::modal::{ + ModalInstanceBase, ModalInstanceTerm, ModalMor, ModalOb, modal_mor_as_identity, +}; +use crate::dbl::model_instance::{DblModelInstance, HasInstanceTerm}; use crate::one::path::Path; use crate::zero::NameSegment; @@ -66,32 +75,28 @@ impl BatchOutput { } } - fn instance_summary(&self, instance: &DiscreteDblModelInstance) { + fn instance_summary(&self, instance: &ModelInstance) { if let BatchOutput::Snapshot(out) = self { let mut out = out.borrow_mut(); - let gens: Vec<_> = instance.generators().collect(); - let eqns: Vec<_> = instance.equations().collect(); - if gens.is_empty() && eqns.is_empty() { - writeln!(out, "#/ instance has no generators or equations").unwrap(); - return; - } - if !gens.is_empty() { - writeln!(out, "#/ instance generators:").unwrap(); - for (name, fiber) in &gens { - writeln!(out, "#/ {name} : {fiber}").unwrap(); - } - } - if !eqns.is_empty() { - writeln!(out, "#/ instance equations:").unwrap(); - for (lhs, rhs) in &eqns { - writeln!( - out, - "#/ {} == {}", - format_instance_term(lhs), - format_instance_term(rhs) - ) - .unwrap(); - } + match instance { + ModelInstance::Discrete(instance) => write_instance_summary( + &mut out, + instance, + |fiber| format!("{fiber}"), + format_instance_term, + ), + ModelInstance::ModalUnital(instance) => write_instance_summary( + &mut out, + instance, + format_modal_ob, + format_modal_instance_term, + ), + ModelInstance::ModalNonUnital(instance) => write_instance_summary( + &mut out, + instance, + format_modal_ob, + format_modal_instance_term, + ), } } } @@ -302,3 +307,108 @@ fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { } s } + +/// Writes the generators and equations of an instance, using the given +/// per-doctrine formatters for fibers and equation terms. +fn write_instance_summary( + out: &mut String, + instance: &DblModelInstance, + fmt_ob: impl Fn(&M::Ob) -> String, + fmt_term: impl Fn(&M::Term) -> String, +) { + let gens: Vec<_> = instance.generators().collect(); + let eqns: Vec<_> = instance.equations().collect(); + if gens.is_empty() && eqns.is_empty() { + writeln!(out, "#/ instance has no generators or equations").unwrap(); + return; + } + if !gens.is_empty() { + writeln!(out, "#/ instance generators:").unwrap(); + for (name, fiber) in &gens { + writeln!(out, "#/ {name} : {}", fmt_ob(fiber)).unwrap(); + } + } + if !eqns.is_empty() { + writeln!(out, "#/ instance equations:").unwrap(); + for (lhs, rhs) in &eqns { + writeln!(out, "#/ {} == {}", fmt_term(lhs), fmt_term(rhs)).unwrap(); + } + } +} + +/// Renders a modal object for snapshot output: generators by name, object +/// operations as `op(inner)`, and lists as `[a, b, …]`. +fn format_modal_ob(ob: &ModalOb) -> String { + match ob { + ModalOb::Generator(name) => format!("{name}"), + ModalOb::App(inner, op) => format!("{op}({})", format_modal_ob(inner)), + ModalOb::List(_, obs) => { + let inner: Vec<_> = obs.iter().map(format_modal_ob).collect(); + format!("[{}]", inner.join(", ")) + } + } +} + +/// An applicative rendering of a modal instance term, reconstructed from its +/// flat `(mor, base)` normal form so it prints back in surface syntax. +enum Rendered { + Gen(String), + App(String, Box), + List(Vec), +} + +impl Rendered { + fn render(&self) -> String { + match self { + Rendered::Gen(name) => name.clone(), + Rendered::App(name, inner) => format!("{name}({})", inner.render()), + Rendered::List(items) => { + let inner: Vec<_> = items.iter().map(Rendered::render).collect(); + format!("[{}]", inner.join(", ")) + } + } + } +} + +/// Renders a modal instance term as e.g. `op([x, unit([])])`, re-interleaving +/// the morphism with its base (the inverse of the flattening done during +/// extraction). +fn format_modal_instance_term(tm: &ModalInstanceTerm) -> String { + apply_mor(&tm.mor, base_rendered(&tm.base)).render() +} + +fn base_rendered(base: &ModalInstanceBase) -> Rendered { + match base { + ModalInstanceBase::Generator(name) => Rendered::Gen(format!("{name}")), + ModalInstanceBase::List(_, bases) => { + Rendered::List(bases.iter().map(base_rendered).collect()) + } + } +} + +/// Applies a model morphism to an already-rendered argument, undoing the +/// `Composite`/`List` tupling introduced by normalization: a `Composite` path +/// folds its morphisms outermost-last, and a list morphism zips into a list +/// argument. +fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { + if modal_mor_as_identity(mor).is_some() { + return arg; + } + match mor { + ModalMor::Generator(name) => Rendered::App(format!("{name}"), Box::new(arg)), + ModalMor::App(_, op) | ModalMor::HomApp(_, op) => { + Rendered::App(format!("{op}"), Box::new(arg)) + } + ModalMor::Composite(path) => match path.as_ref() { + Path::Id(_) => arg, + Path::Seq(edges) => edges.iter().fold(arg, |acc, mor| apply_mor(mor, acc)), + }, + ModalMor::List(_, mors) => match arg { + Rendered::List(items) if items.len() == mors.len() => { + Rendered::List(mors.iter().zip(items).map(|(m, a)| apply_mor(m, a)).collect()) + } + // Should not arise: a list morphism always applies to a list base. + other => other, + }, + } +} diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 76afe6c06..f542c4987 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -10,14 +10,14 @@ use super::{eval::*, prelude::*, text_elab, theory::*, toplevel::*, val::*}; use crate::dbl::{ discrete::{self, DiscreteDblModelInstance, DiscreteInstanceTerm}, discrete_tabulator, modal, - model::{DblModel, DblModelPrinter, MutDblModel}, + model::{DblModel, DblModelPrinter, FpDblModel, MutDblModel}, theory::{DblTheory, DblTheoryKind, NonUnital, Unital}, }; use crate::one::{ Category, path::{Path, PathEq}, }; -use crate::zero::{Namespace, QualifiedName}; +use crate::zero::{Namespace, QualifiedName, SkelColumn}; /// A model generated by DoubleTT. /// @@ -407,35 +407,246 @@ impl<'a> ModelGenerator<'a> { BaseTyV_::Meta(_) => None, } } + + /// Extracts a [`ModelInstance`] from the fiber record of an instance body, + /// dispatching on the doctrine of the (already generated) codomain model. + fn instance( + &self, + fields: &Row, + namespace: &mut Namespace, + ) -> Result { + match &self.model { + Model::Discrete(model) => { + let mut instance = DiscreteDblModelInstance::new(Rc::new((**model).clone())); + extract_instance_record(&mut instance, namespace, &[], fields)?; + Ok(ModelInstance::Discrete(instance)) + } + Model::DiscreteTab(_) => { + Err("instance generation does not support discrete tabulator theories".into()) + } + Model::ModalUnital(model) => { + Ok(ModelInstance::ModalUnital(self.modal_instance(model, namespace, fields)?)) + } + Model::ModalNonUnital(model) => { + Ok(ModelInstance::ModalNonUnital(self.modal_instance(model, namespace, fields)?)) + } + } + } + + /// Builds a modal instance over the given codomain model by walking the + /// fiber record. + fn modal_instance( + &self, + model: &modal::ModalDblModel, + namespace: &mut Namespace, + fields: &Row, + ) -> Result, String> { + let mut instance = modal::ModalDblModelInstance::new(Rc::new(model.clone())); + self.extract_modal_record(&mut instance, namespace, &[], fields)?; + Ok(instance) + } + + /// Registers the contents of an instance (a fiber record) into a modal + /// model instance, recursing into sub-instance imports under their prefix. + /// + /// Two passes, matching [`extract_instance_record`]: generators and + /// sub-instances first, then the equation ([`Id`](FiberTyV_::Id)) fields, + /// so every generator a term may mention is already registered. + fn extract_modal_record( + &self, + instance: &mut modal::ModalDblModelInstance, + namespace: &mut Namespace, + prefix: &[NameSegment], + fields: &Row, + ) -> Result<(), String> { + for (name, (label, field_ty)) in fields.iter() { + match &**field_ty { + FiberTyV_::Over(obj) => { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let mut qsegs = prefix.to_vec(); + qsegs.push(*name); + let qname: QualifiedName = qsegs.into(); + let fiber = self.modal_fiber_ob(obj)?; + instance.add_generator(qname, fiber); + } + FiberTyV_::Record(sub_fields) => { + if let NameSegment::Uuid(uuid) = name { + namespace.set_label(*uuid, *label); + } + let mut sub_prefix = prefix.to_vec(); + sub_prefix.push(*name); + self.extract_modal_record(instance, namespace, &sub_prefix, sub_fields)?; + } + FiberTyV_::Id(_, _, _) => {} + } + } + for (_, (_, field_ty)) in fields.iter() { + if let FiberTyV_::Id(eq_ty, lhs, rhs) = &**field_ty { + let ob_type = self.modal_equation_ob_type(eq_ty)?; + let lhs_t = self.modal_instance_term(&*instance, lhs, &ob_type, prefix)?; + let rhs_t = self.modal_instance_term(&*instance, rhs, &ob_type, prefix)?; + instance.add_equation(lhs_t, rhs_t); + } + } + Ok(()) + } + + /// Converts the codomain object a generator lies over into a [`ModalOb`]. + fn modal_fiber_ob(&self, obj: &BaseTmV) -> Result { + let (ob, _) = self.make_ob_synth_type(obj).ok_or_else(|| { + "instance generator lies over an object this doctrine cannot yet extract".to_string() + })?; + ob.try_into() + .map_err(|_| "expected a modal object as a generator's fiber".to_string()) + } + + /// The object type an equation lives over (its `Over` fiber type). + fn modal_equation_ob_type(&self, eq_ty: &FiberTyV) -> Result { + let FiberTyV_::Over(obj) = &**eq_ty else { + return Err("instance equation is not over an object".into()); + }; + let (_, ob_type) = self + .make_ob_synth_type(obj) + .ok_or_else(|| "cannot determine the type of an instance equation".to_string())?; + Ok(ob_type) + } + + /// Converts a fiber term into a [`ModalInstanceTerm`]. + fn modal_instance_term( + &self, + instance: &modal::ModalDblModelInstance, + tm: &FiberTmV, + expected: &ObType, + prefix: &[NameSegment], + ) -> Result { + let (mor, base) = self.modal_mor_base(instance, tm, expected, prefix)?; + Ok(modal::ModalInstanceTerm { mor, base }) + } + + /// The heart of modal term extraction: converts a fiber term into a + /// morphism applied to a base of generators, threading the expected object + /// type down so list modalities can be recovered. + /// + /// The returned morphism is an identity exactly when the term is a pure + /// base (generators/lists with no applied morphisms), keeping terms in the + /// flat normal form of [`ModalInstanceTerm`]: composition and list-tupling + /// of morphisms encountered inside list elements are pushed into the + /// morphism (via `Composite`/`List`) rather than nesting applications. + fn modal_mor_base( + &self, + instance: &modal::ModalDblModelInstance, + tm: &FiberTmV, + expected: &ObType, + prefix: &[NameSegment], + ) -> Result<(modal::ModalMor, modal::ModalInstanceBase), String> { + use modal::{ModalInstanceBase, ModalMor, ModalOb, Modality, MorListData}; + match &**tm { + FiberTmV_::Var(_, _, _) | FiberTmV_::Proj(_, _, _) => { + let mut segs = prefix.to_vec(); + segs.extend(fiber_full_name(tm)?); + let qname: QualifiedName = segs.into(); + let fiber = instance + .fiber_of(&qname) + .ok_or_else(|| format!("instance term mentions unknown generator {qname}"))?; + let id = instance.model().id(fiber.clone()); + Ok((id, ModalInstanceBase::Generator(qname))) + } + FiberTmV_::List(elems) => { + let (modality, el_type) = expected + .clone() + .mode_app() + .ok_or_else(|| "expected a modal list type for a list term".to_string())?; + let Modality::List(list_ty) = modality else { + return Err("expected a list modality for a list term".into()); + }; + let mut mors = Vec::with_capacity(elems.len()); + let mut bases = Vec::with_capacity(elems.len()); + for elem in elems { + let (m, b) = self.modal_mor_base(instance, elem, &el_type, prefix)?; + mors.push(m); + bases.push(b); + } + let base = ModalInstanceBase::List(list_ty, bases); + // If every element is a pure base, the whole list is too, and + // the morphism is the identity on the list object. + let identity_obs: Option> = + mors.iter().map(modal::modal_mor_as_identity).collect(); + if let Some(objs) = identity_obs { + let list_ob = ModalOb::List(list_ty, objs.into_iter().cloned().collect()); + Ok((instance.model().id(list_ob), base)) + } else { + let data = match list_ty { + modal::List::Plain => MorListData::Plain(), + modal::List::Symmetric => { + MorListData::Symmetric(SkelColumn::new((0..mors.len()).collect())) + } + other => { + return Err(format!( + "instance terms do not yet support the {other:?} list modality" + )); + } + }; + Ok((ModalMor::List(data, mors), base)) + } + } + FiberTmV_::OverApp(path, _cod, inner) => { + let qname: QualifiedName = + path.iter().map(|(seg, _)| *seg).collect::>().into(); + let mor = ModalMor::Generator(qname.clone()); + let mor_type = MorType::Modal(instance.model().mor_generator_type(&qname)); + let dom_ty = self.theory.src_type(&mor_type); + let (inner_mor, base) = self.modal_mor_base(instance, inner, &dom_ty, prefix)?; + let full = if modal::modal_mor_as_identity(&inner_mor).is_some() { + mor + } else { + instance.model().compose2(inner_mor, mor) + }; + Ok((full, base)) + } + FiberTmV_::ObApp(op, _) => Err(format!( + "instance terms do not yet support object-operation applications (e.g. `@{op}`)" + )), + FiberTmV_::Meta(_) => Err("instance term contains an unresolved metavariable".into()), + } + } +} + +/// An instance of a model generated by DoubleTT. +/// +/// Like [`Model`], this boxes the per-doctrine concrete instance types +/// behind one enum. +pub enum ModelInstance { + /// An instance of a discrete double model. + Discrete(DiscreteDblModelInstance), + /// An instance of a unital modal double model. + ModalUnital(modal::ModalDblModelInstance), + /// An instance of a non-unital modal double model. + ModalNonUnital(modal::ModalDblModelInstance), } -/// Generates a [`DiscreteDblModelInstance`] from an elaborated [`Instance`] -/// declaration. +/// Generates a [`ModelInstance`] from an elaborated [`Instance`] declaration. /// /// Walks the instance's fiber [`Record`](FiberTyV_::Record), registering -/// each generator with its fiber, each equation as a pair of -/// [`DiscreteInstanceTerm`]s, and each sub-instance's contents under -/// the appropriate prefix. +/// each generator with its fiber, each equation as a pair of instance +/// terms, and each sub-instance's contents under the appropriate prefix. /// -/// Restricted to discrete double theories for the moment. +/// The codomain model is built with a `ModelGenerator`, which is then +/// reused to type the instance's generators and equation terms (this is how +/// modal list modalities are recovered). pub fn instance_from_def( toplevel: &Toplevel, th: &TheoryDef, inst: &Instance, -) -> Result<(DiscreteDblModelInstance, Namespace), String> { - let TheoryDef::Discrete(_) = th else { - return Err("instance generation only supports discrete double theories".into()); - }; - let (cod_model, _) = Model::from_ty(toplevel, th, &inst.codomain); - let cod_model = cod_model - .as_discrete() - .ok_or_else(|| "expected a discrete codomain model".to_string())?; - let mut instance = DiscreteDblModelInstance::new(Rc::new(cod_model)); +) -> Result<(ModelInstance, Namespace), String> { + let mut generator = ModelGenerator::new(toplevel, th); + generator.generate(&inst.codomain); let FiberTyV_::Record(fields) = &*inst.val else { return Err("expected an instance (a fiber record)".into()); }; let mut namespace = Namespace::new_for_uuid(); - extract_instance_record(&mut instance, &mut namespace, &[], fields)?; + let instance = generator.instance(fields, &mut namespace)?; Ok((instance, namespace)) } @@ -611,6 +822,9 @@ instance I : WeightedGraph := [ _ => panic!("expected I to be an instance declaration"), }; let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); + let ModelInstance::Discrete(instance) = instance else { + panic!("expected a discrete instance"); + }; let e_qname: QualifiedName = vec![name_seg("e")].into(); let e_fiber: QualifiedName = vec![name_seg("E")].into(); @@ -672,9 +886,47 @@ instance UseLoop : WeightedGraph := [ }; let (instance, _ns) = instance_from_def(&toplevel, &use_def.theory.definition, &use_def).unwrap(); + let ModelInstance::Discrete(instance) = instance else { + panic!("expected a discrete instance"); + }; let le_qname: QualifiedName = vec![name_seg("l"), name_seg("e")].into(); let e_fiber: QualifiedName = vec![name_seg("E")].into(); assert_eq!(instance.fiber_of(&le_qname), Some(&e_fiber)); assert_eq!(instance.equations().count(), 2); } + + /// A modal (multicategory) instance: generators lie over plain objects and + /// equations are list-domain morphism applications. + #[test] + fn instance_over_multicategory_monoid() { + let src = r#" +set_theory ThMulticategory + +model SigMonoid := [ + M : Object, + op : Multihom[[M, M], M], + unit : Multihom[[], M] +] + +instance Z2 : SigMonoid := [ + M := [x], + op([x,x]) := unit([]), + op([x,unit([])]) := x +] +"#; + let toplevel = elaborate_to_toplevel(src); + let def = match toplevel.declarations.get(&name_seg("Z2")) { + Some(TopDecl::Instance(i)) => i.clone(), + _ => panic!("expected Z2 to be an instance declaration"), + }; + let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); + let ModelInstance::ModalUnital(instance) = instance else { + panic!("expected a unital modal instance"); + }; + + let x_qname: QualifiedName = vec![name_seg("x")].into(); + let m_fiber = modal::ModalOb::Generator(vec![name_seg("M")].into()); + assert_eq!(instance.fiber_of(&x_qname), Some(&m_fiber)); + assert_eq!(instance.equations().count(), 2); + } } From 3a22e08bec2a1cdfd43fa2765096d62e6c0bd1a6 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 7 Jul 2026 15:04:18 -0700 Subject: [PATCH 43/53] support for ob ops --- .../text/test_modal_instances.dbltt.snapshot | 19 +++++- .../catlog/src/dbl/modal/model_instance.rs | 19 ++++-- packages/catlog/src/tt/batch.rs | 5 ++ packages/catlog/src/tt/modelgen.rs | 60 ++++++++++++++++++- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot index 08b56abff..7d528ccf2 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -109,5 +109,22 @@ instance hm : HMW := [ returns_d(beowulf) := @tensor [] ] #/ declared: hm -#/ instance generation failed: instance terms do not yet support object-operation applications (e.g. `@tensor`) +#/ instance generators: +#/ hercules : H +#/ odysseus : H +#/ beowulf : H +#/ hydra : M +#/ polyphemus : M +#/ grendel : M +#/ dragon : M +#/ megara : W +#/ penelope : W +#/ instance equations: +#/ fight_h(@tensor [odysseus, polyphemus]) == odysseus +#/ fight_h(@tensor [beowulf, grendel]) == beowulf +#/ fight_m(@tensor [beowulf, dragon]) == dragon +#/ fight_mm(@tensor [hercules, hydra]) == @tensor [hydra, hydra] +#/ returns_w(hercules) == megara +#/ returns_w(odysseus) == penelope +#/ returns_d(beowulf) == @tensor [] diff --git a/packages/catlog/src/dbl/modal/model_instance.rs b/packages/catlog/src/dbl/modal/model_instance.rs index 281a1687b..9f34b0ea3 100644 --- a/packages/catlog/src/dbl/modal/model_instance.rs +++ b/packages/catlog/src/dbl/modal/model_instance.rs @@ -21,12 +21,14 @@ use super::theory::List; /// `base` directly, and that `Id` object must agree with the fiber of `base` /// in the surrounding instance. /// -/// The only recursion that survives lives in [`base`](Self::base), and only -/// through [`ModalInstanceBase::List`], mirroring [`ModalOb::List`]: -/// this is what lets a generator over a nested list object be written inline -/// as e.g. `[x, y]` rather than forced to be a single named generator. A base -/// can never hold a morphism, so the "no application inside an application" -/// invariant is enforced structurally. +/// The only recursion that survives lives in [`base`](Self::base), through +/// [`ModalInstanceBase::List`] (mirroring [`ModalOb::List`]) and +/// [`ModalInstanceBase::ObApp`] (mirroring [`ModalOb::App`]): this is what +/// lets a generator over a nested list object be written inline as e.g. +/// `[x, y]`, or an element of a product object as `@tensor [x, y]`. A base +/// holds only generators, lists, and object-operation applications — never a +/// morphism — so the "no application inside an application" invariant is +/// enforced structurally. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ModalInstanceTerm { /// Model morphism applied to `base`. @@ -44,6 +46,11 @@ pub enum ModalInstanceBase { /// A list of bases in a [list modality](List), living over a /// [list object](super::model::ModalOb::List). List(List, Vec), + /// An object operation applied to a base, e.g. `@tensor [x, y]`, living + /// over an [object-operation application](super::model::ModalOb::App). + /// Object operations are functorial actions on objects, not morphisms, so + /// this stays in the base rather than in the term's morphism. + ObApp(QualifiedName, Box), } impl InstanceTerm for ModalInstanceTerm { diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 62a911a28..cf0cc9024 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -354,6 +354,7 @@ fn format_modal_ob(ob: &ModalOb) -> String { enum Rendered { Gen(String), App(String, Box), + ObApp(String, Box), List(Vec), } @@ -362,6 +363,7 @@ impl Rendered { match self { Rendered::Gen(name) => name.clone(), Rendered::App(name, inner) => format!("{name}({})", inner.render()), + Rendered::ObApp(op, inner) => format!("@{op} {}", inner.render()), Rendered::List(items) => { let inner: Vec<_> = items.iter().map(Rendered::render).collect(); format!("[{}]", inner.join(", ")) @@ -383,6 +385,9 @@ fn base_rendered(base: &ModalInstanceBase) -> Rendered { ModalInstanceBase::List(_, bases) => { Rendered::List(bases.iter().map(base_rendered).collect()) } + ModalInstanceBase::ObApp(op, inner) => { + Rendered::ObApp(format!("{op}"), Box::new(base_rendered(inner))) + } } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index f542c4987..e261b59b6 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -605,9 +605,28 @@ impl<'a> ModelGenerator<'a> { }; Ok((full, base)) } - FiberTmV_::ObApp(op, _) => Err(format!( - "instance terms do not yet support object-operation applications (e.g. `@{op}`)" - )), + FiberTmV_::ObApp(op, inner) => { + let op_name: QualifiedName = [*op].into(); + // Recurse into the argument at the object operation's domain + // type, so a list argument recovers its modality. + let ob_op = modal::ModalObOp::generator(op_name.clone()); + let dom_ty: ObType = instance.model().theory().ob_op_dom(&ob_op).into(); + let (inner_mor, inner_base) = + self.modal_mor_base(instance, inner, &dom_ty, prefix)?; + // TODO: pure case only. The argument must be a base (identity + // morphism). Applying an object operation to a term that + // contains morphisms is its functorial action (a HomApp), + // which is not yet supported. + let Some(inner_ob) = modal::modal_mor_as_identity(&inner_mor) else { + return Err(format!( + "instance terms do not yet support the functorial action of an object \ + operation `@{op}` on a term containing morphism applications" + )); + }; + let app_ob = ModalOb::App(Box::new(inner_ob.clone()), op_name.clone()); + let base = ModalInstanceBase::ObApp(op_name, Box::new(inner_base)); + Ok((instance.model().id(app_ob), base)) + } FiberTmV_::Meta(_) => Err("instance term contains an unresolved metavariable".into()), } } @@ -929,4 +948,39 @@ instance Z2 : SigMonoid := [ assert_eq!(instance.fiber_of(&x_qname), Some(&m_fiber)); assert_eq!(instance.equations().count(), 2); } + + /// A symmetric-monoidal instance: equation terms feed generators through + /// the `@tensor` object operation into unary homs between product objects. + #[test] + fn instance_over_symmetric_monoidal() { + let src = r#" +set_theory ThSymMonoidalCategory + +model AB := [ + A : Object, + B : Object, + f : (Hom Object)[@tensor [A, B], A] +] + +instance i : AB := [ + A := [a], + B := [b], + f(@tensor [a, b]) := a +] +"#; + let toplevel = elaborate_to_toplevel(src); + let def = match toplevel.declarations.get(&name_seg("i")) { + Some(TopDecl::Instance(i)) => i.clone(), + _ => panic!("expected i to be an instance declaration"), + }; + let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); + let ModelInstance::ModalUnital(instance) = instance else { + panic!("expected a unital modal instance"); + }; + + let a_qname: QualifiedName = vec![name_seg("a")].into(); + let a_fiber = modal::ModalOb::Generator(vec![name_seg("A")].into()); + assert_eq!(instance.fiber_of(&a_qname), Some(&a_fiber)); + assert_eq!(instance.equations().count(), 1); + } } From c75fbab8e91f1e4bec50831c1286e8a510df6964 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Tue, 7 Jul 2026 16:42:15 -0700 Subject: [PATCH 44/53] Handling HomApps --- .../tt/text/test_modal_instances.dbltt | 13 +++- .../text/test_modal_instances.dbltt.snapshot | 18 ++++++ packages/catlog/src/tt/batch.rs | 27 ++++++-- packages/catlog/src/tt/modelgen.rs | 63 +++++++++++++++---- 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt b/packages/catlog/examples/tt/text/test_modal_instances.dbltt index 9c9727eaa..01ae97271 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt @@ -63,4 +63,15 @@ instance hm : HMW := [ returns_w(hercules) := megara, returns_w(odysseus) := penelope, returns_d(beowulf) := @tensor [] -] \ No newline at end of file +] +model Chain := [ + X : Object, + Y : Object, + s : (Hom Object)[X, Y], + t : (Hom Object)[@tensor [Y, Y], X] +] + +instance chain : Chain := [ + X := [x0], + t(@tensor [s(x0), s(x0)]) := x0 +] diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot index 7d528ccf2..85926668e 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -128,3 +128,21 @@ instance hm : HMW := [ #/ returns_w(odysseus) == penelope #/ returns_d(beowulf) == @tensor [] +model Chain := [ + X : Object, + Y : Object, + s : (Hom Object)[X, Y], + t : (Hom Object)[@tensor [Y, Y], X] +] +#/ declared: Chain + +instance chain : Chain := [ + X := [x0], + t(@tensor [s(x0), s(x0)]) := x0 +] +#/ declared: chain +#/ instance generators: +#/ x0 : X +#/ instance equations: +#/ t(@tensor [s(x0), s(x0)]) == x0 + diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index cf0cc9024..a4f4d4486 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -401,13 +401,19 @@ fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { } match mor { ModalMor::Generator(name) => Rendered::App(format!("{name}"), Box::new(arg)), - ModalMor::App(_, op) | ModalMor::HomApp(_, op) => { - Rendered::App(format!("{op}"), Box::new(arg)) - } - ModalMor::Composite(path) => match path.as_ref() { - Path::Id(_) => arg, - Path::Seq(edges) => edges.iter().fold(arg, |acc, mor| apply_mor(mor, acc)), + ModalMor::App(_, op) => Rendered::App(format!("{op}"), Box::new(arg)), + // The functorial action of an object operation: it applies to an + // `@op [..]` base, and the lifted morphisms act on the operation's + // content, so we push them inside the existing wrapper rather than + // adding another. + ModalMor::HomApp(path, _op) => match arg { + Rendered::ObApp(op_name, inner) => { + Rendered::ObApp(op_name, Box::new(apply_path(path, *inner))) + } + // Should not arise: a hom operation applies to an object-op base. + other => other, }, + ModalMor::Composite(path) => apply_path(path, arg), ModalMor::List(_, mors) => match arg { Rendered::List(items) if items.len() == mors.len() => { Rendered::List(mors.iter().zip(items).map(|(m, a)| apply_mor(m, a)).collect()) @@ -417,3 +423,12 @@ fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { }, } } + +/// Applies a path of morphisms to a rendered argument, folding outermost-last +/// (so `[m1, m2]` renders as `m2(m1(arg))`). +fn apply_path(path: &Path, arg: Rendered) -> Rendered { + match path { + Path::Id(_) => arg, + Path::Seq(edges) => edges.iter().fold(arg, |acc, mor| apply_mor(mor, acc)), + } +} diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index e261b59b6..78a2aae06 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -613,19 +613,20 @@ impl<'a> ModelGenerator<'a> { let dom_ty: ObType = instance.model().theory().ob_op_dom(&ob_op).into(); let (inner_mor, inner_base) = self.modal_mor_base(instance, inner, &dom_ty, prefix)?; - // TODO: pure case only. The argument must be a base (identity - // morphism). Applying an object operation to a term that - // contains morphisms is its functorial action (a HomApp), - // which is not yet supported. - let Some(inner_ob) = modal::modal_mor_as_identity(&inner_mor) else { - return Err(format!( - "instance terms do not yet support the functorial action of an object \ - operation `@{op}` on a term containing morphism applications" - )); + // If the argument is a pure base (identity morphism), the + // object operation lands on data and the morphism stays the + // identity on the resulting `App` object. Otherwise the + // operation acts functorially on the argument's morphism, + // yielding a `HomApp`. + let mor = match modal::modal_mor_as_identity(&inner_mor).cloned() { + Some(inner_ob) => { + let app_ob = ModalOb::App(Box::new(inner_ob), op_name.clone()); + instance.model().id(app_ob) + } + None => ModalMor::HomApp(Box::new(inner_mor.into()), op_name.clone()), }; - let app_ob = ModalOb::App(Box::new(inner_ob.clone()), op_name.clone()); let base = ModalInstanceBase::ObApp(op_name, Box::new(inner_base)); - Ok((instance.model().id(app_ob), base)) + Ok((mor, base)) } FiberTmV_::Meta(_) => Err("instance term contains an unresolved metavariable".into()), } @@ -983,4 +984,44 @@ instance i : AB := [ assert_eq!(instance.fiber_of(&a_qname), Some(&a_fiber)); assert_eq!(instance.equations().count(), 1); } + + /// A symmetric-monoidal instance whose equation feeds a *morphism* + /// application through `@tensor`, exercising the functorial action of the + /// object operation (a `HomApp`) rather than a pure object-op base. + #[test] + fn instance_over_symmetric_monoidal_functorial() { + let src = r#" +set_theory ThSymMonoidalCategory + +model Chain := [ + X : Object, + Y : Object, + s : (Hom Object)[X, Y], + t : (Hom Object)[@tensor [Y, Y], X] +] + +instance chain : Chain := [ + X := [x0], + t(@tensor [s(x0), s(x0)]) := x0 +] +"#; + let toplevel = elaborate_to_toplevel(src); + let def = match toplevel.declarations.get(&name_seg("chain")) { + Some(TopDecl::Instance(i)) => i.clone(), + _ => panic!("expected chain to be an instance declaration"), + }; + let (instance, _ns) = instance_from_def(&toplevel, &def.theory.definition, &def).unwrap(); + let ModelInstance::ModalUnital(instance) = instance else { + panic!("expected a unital modal instance"); + }; + assert_eq!(instance.equations().count(), 1); + + // The left-hand side's morphism is a HomApp: the object operation + // `tensor` acting on the list morphism `[s, s]`. + let (lhs, _) = instance.equations().next().unwrap(); + assert!( + matches!(&lhs.mor, modal::ModalMor::Composite(_)), + "expected the applied morphism to compose `t` after the tensor's HomApp" + ); + } } From 8ff89a95a07fca126165ca1beb1b1952eb0de2b9 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 11:38:10 -0700 Subject: [PATCH 45/53] show off some normalization --- .../tt/text/test_modal_instances.dbltt | 6 + .../text/test_modal_instances.dbltt.snapshot | 9 ++ packages/catlog/src/tt/modelgen.rs | 138 ++++++++++++++++++ packages/catlog/src/tt/text_elab.rs | 61 +++++++- 4 files changed, 213 insertions(+), 1 deletion(-) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt b/packages/catlog/examples/tt/text/test_modal_instances.dbltt index 01ae97271..e32bef9ec 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt @@ -75,3 +75,9 @@ instance chain : Chain := [ X := [x0], t(@tensor [s(x0), s(x0)]) := x0 ] + +norm [chain] x0 + +norm [chain] s(x0) + +norm [chain] t(@tensor [s(x0), s(x0)]) diff --git a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot index 85926668e..0d29c57e9 100644 --- a/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot +++ b/packages/catlog/examples/tt/text/test_modal_instances.dbltt.snapshot @@ -146,3 +146,12 @@ instance chain : Chain := [ #/ instance equations: #/ t(@tensor [s(x0), s(x0)]) == x0 +norm [chain] x0 +#/ result: x0 + +norm [chain] s(x0) +#/ result: s @ x0 + +norm [chain] t(@tensor [s(x0), s(x0)]) +#/ result: (@tensor [s, s] ; t) @ @tensor [x0, x0] + diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 78a2aae06..613f5016c 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -670,6 +670,144 @@ pub fn instance_from_def( Ok((instance, namespace)) } +/// The flat normal form of a single instance term, as produced by +/// [`normalize_instance_term`]: a composite model morphism applied to a base +/// of generators. Doctrine-specific because each has its own term type. +pub enum NormalizedInstanceTerm { + /// A term of an instance of a discrete double model. + Discrete(DiscreteInstanceTerm), + /// A term of an instance of a modal double model (unital or non-unital). + Modal(modal::ModalInstanceTerm), +} + +impl NormalizedInstanceTerm { + /// Renders the term in its flat normal form `mor @ base`, exposing the + /// single (composite) model morphism acting on a base of generators. A + /// term whose morphism is the identity (a pure base) is rendered as just + /// its base. This is deliberately distinct from the applicative + /// reconstruction used in instance summaries: the point of `norm` is to + /// *show* the composite, e.g. `t(@tensor [s(x0), s(x0)])` normalizing to + /// `(@tensor [s, s] ; t) @ @tensor [x0, x0]`. + pub fn render(&self) -> String { + match self { + NormalizedInstanceTerm::Discrete(t) => match &t.path { + Path::Id(_) => format!("{}", t.base), + Path::Seq(edges) => { + let parts: Vec<_> = edges.iter().map(|mor| format!("{mor}")).collect(); + if parts.len() == 1 { + format!("{} @ {}", parts[0], t.base) + } else { + format!("({}) @ {}", parts.join(" ; "), t.base) + } + } + }, + NormalizedInstanceTerm::Modal(t) => { + let base = render_modal_base(&t.base); + match modal::modal_mor_as_identity(&t.mor) { + Some(_) => base, + None => format!("{} @ {}", render_modal_mor(&t.mor), base), + } + } + } + } +} + +/// Renders the base of a flat modal term: generators by name, lists as +/// `[a, b, …]`, and object operations as `@op `. +fn render_modal_base(base: &modal::ModalInstanceBase) -> String { + match base { + modal::ModalInstanceBase::Generator(name) => format!("{name}"), + modal::ModalInstanceBase::List(_, bases) => { + let inner: Vec<_> = bases.iter().map(render_modal_base).collect(); + format!("[{}]", inner.join(", ")) + } + modal::ModalInstanceBase::ObApp(op, inner) => { + format!("@{op} {}", render_modal_base(inner)) + } + } +} + +/// Renders a model morphism in a flat term. A composite of length ≥ 2 is +/// shown parenthesized as `(m1 ; m2 ; …)`, applied left-to-right; the +/// functorial `HomApp` of an object operation shows as `@op `. +fn render_modal_mor(mor: &modal::ModalMor) -> String { + match mor { + modal::ModalMor::Generator(name) => format!("{name}"), + modal::ModalMor::Composite(path) => { + let parts = render_modal_mor_path(path); + match parts.len() { + 0 => "id".to_string(), + 1 => parts.into_iter().next().unwrap(), + _ => format!("({})", parts.join(" ; ")), + } + } + modal::ModalMor::App(path, op) => { + format!("{op}({})", render_modal_mor_path(path).join(" ; ")) + } + modal::ModalMor::HomApp(path, op) => { + format!("@{op} {}", render_modal_mor_path(path).join(" ; ")) + } + modal::ModalMor::List(_, mors) => { + let inner: Vec<_> = mors.iter().map(render_modal_mor).collect(); + format!("[{}]", inner.join(", ")) + } + } +} + +/// The morphisms along a path, each flat-rendered; empty for an identity path. +fn render_modal_mor_path(path: &Path) -> Vec { + match path { + Path::Id(_) => Vec::new(), + Path::Seq(edges) => edges.iter().map(render_modal_mor).collect(), + } +} + +/// Normalizes a single already-elaborated fiber term `tm` (lying over the +/// codomain object `over`) in the context of the instance `inst`, returning +/// its flat normal form. +/// +/// This is what backs `norm [inst] `: the term is elaborated against +/// `inst`'s fiber scope elsewhere (in `text_elab`), then handed here to run +/// the same extraction that turns an instance body's equations into +/// [`modal::ModalInstanceTerm`]s — which is where nested morphism applications +/// get composed into a single (`Composite`/`List`) morphism. +pub fn normalize_instance_term( + toplevel: &Toplevel, + th: &TheoryDef, + inst: &Instance, + tm: &FiberTmV, + over: &BaseTmV, +) -> Result { + let mut generator = ModelGenerator::new(toplevel, th); + generator.generate(&inst.codomain); + let FiberTyV_::Record(fields) = &*inst.val else { + return Err("expected an instance (a fiber record)".into()); + }; + // Build the instance first, so every generator the term may mention is + // registered before we extract (extraction resolves generators by name). + let mut namespace = Namespace::new_for_uuid(); + match generator.instance(fields, &mut namespace)? { + ModelInstance::Discrete(instance) => { + let term = fiber_tm_to_discrete_instance_term(&instance, tm, &[])?; + Ok(NormalizedInstanceTerm::Discrete(term)) + } + ModelInstance::ModalUnital(instance) => { + let (_, ob_type) = generator + .make_ob_synth_type(over) + .ok_or_else(|| "cannot determine the object the term lies over".to_string())?; + let term = generator.modal_instance_term(&instance, tm, &ob_type, &[])?; + Ok(NormalizedInstanceTerm::Modal(term)) + } + ModelInstance::ModalNonUnital(instance) => { + let (_, ob_type) = generator + .make_ob_synth_type(over) + .ok_or_else(|| "cannot determine the object the term lies over".to_string())?; + let term = generator.modal_instance_term(&instance, tm, &ob_type, &[])?; + Ok(NormalizedInstanceTerm::Modal(term)) + } + } +} + /// Register the contents of an instance (a fiber record) into the model /// instance under construction, recursing into sub-instance imports under /// their prefix. diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index d7a44978d..b4bac8af7 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -229,8 +229,38 @@ impl TopElaborator { ))) } "norm" => { - let theory = self.get_theory(tn.loc)?; let (ctx_ns, n) = self.expr_with_context(tn.body); + // `norm [inst] `: normalize a single instance + // term in the scope of the existing instance `inst`, showing + // its flat (composite) normal form. A fiber term is neutral + // at the type-theory level; the composition happens when it is + // extracted to a model-instance term (see + // [`normalize_instance_term`]). + if let [single] = ctx_ns + && let Var(inst_name) = single.ast0() + && let Some(TopDecl::Instance(inst)) = + toplevel.declarations.get(&name_seg(*inst_name)) + { + let mut elab = self.elaborator(&inst.theory, toplevel); + elab.enter_instance(inst)?; + let (_, tm_v, ty_v) = elab.fiber_syn(n); + let FiberTyV_::Over(over) = &*ty_v else { + return elab + .error("norm expects an instance element (a term over an object)"); + }; + let over = over.clone(); + return match normalize_instance_term( + toplevel, + &inst.theory.definition, + inst, + &tm_v, + &over, + ) { + Ok(nt) => Some(TopElabResult::Output(nt.render())), + Err(msg) => self.error(tn.loc, msg), + }; + } + let theory = self.get_theory(tn.loc)?; let mut elab = self.elaborator(&theory, toplevel); for ctx_n in ctx_ns { let (name, label, _, ty_v) = elab.binding(ctx_n)?; @@ -745,6 +775,35 @@ impl<'a> Elaborator<'a> { result } + /// Re-establish the scope of an already-elaborated instance so a fresh + /// term can be elaborated against it (see the `norm [inst] ` + /// command): bind the codomain model as `self` — so codomain-morphism + /// syntax like `t(..)` resolves — and introduce each generator and + /// sub-instance import into the fiber scope by its original name. + fn enter_instance(&mut self, inst: &Instance) -> Option<()> { + self.intro( + name_seg(Self::CODOMAIN_BINDER), + label_seg(Self::CODOMAIN_BINDER), + Some(inst.codomain.clone()), + ); + let FiberTyV_::Record(fields) = &*inst.val else { + return self.error("instance value is not a fiber record"); + }; + for (name, (label, field_ty)) in fields.iter() { + match &**field_ty { + // Generators (`Over`) and sub-instance imports (`Record`) + // become fiber-scope bindings; projections into an import + // resolve against the record type. Equations (`Id`) are not + // in scope as terms. + FiberTyV_::Over(_) | FiberTyV_::Record(_) => { + self.intro_fiber(*name, *label, field_ty.clone()); + } + FiberTyV_::Id(_, _, _) => {} + } + } + Some(()) + } + /// Elaborate the clauses of an instance body (the f-notation `n`) into a /// fiber [`Record`](FiberTyS_::Record). The codomain is already set on /// the context by [`Self::instance_body`]. From 070be327678fb307c58edfcbf919e9ea2afe9a2e Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 17:21:33 -0700 Subject: [PATCH 46/53] add klausmeir examples from matteo --- .../examples/tt/text/test_klausmeier.dbltt | 87 +++++++++ .../tt/text/test_klausmeier.dbltt.snapshot | 167 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 packages/catlog/examples/tt/text/test_klausmeier.dbltt create mode 100644 packages/catlog/examples/tt/text/test_klausmeier.dbltt.snapshot diff --git a/packages/catlog/examples/tt/text/test_klausmeier.dbltt b/packages/catlog/examples/tt/text/test_klausmeier.dbltt new file mode 100644 index 000000000..c4cdd88bf --- /dev/null +++ b/packages/catlog/examples/tt/text/test_klausmeier.dbltt @@ -0,0 +1,87 @@ +set_theory ThMulticategory + +#/ The signature of a fragment of the discrete exterior calculus (DEC), as a +#/ multicategory presentation. Translated from Matt's notebook-elaborated +#/ Klausmeier examples (examples/tt/notebook/klausmeier). +model DEC := [ + Form0 : Object, + Form1 : Object, + DualForm0 : Object, + lapl_d0 : Multihom[[DualForm0], DualForm0], + partial_0 : Multihom[[Form0], Form0], + partial_1 : Multihom[[Form1], Form1], + partial_d0 : Multihom[[DualForm0], DualForm0], + square_d0 : Multihom[[DualForm0], DualForm0], + add_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + sub_d01 : Multihom[[DualForm0, Form0], DualForm0], + sub_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + mult_00 : Multihom[[Form0, Form0], Form0], + mult_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + mult_0d0 : Multihom[[Form0, DualForm0], DualForm0], + lie_1d0 : Multihom[[Form1, DualForm0], DualForm0], + wedge_00 : Multihom[[Form0, Form0], Form0], + wedge_10 : Multihom[[Form1, Form0], Form1] +] + +#/ Faithful rendering: named intermediate generators (one per notebook cell), +#/ each defined by a fiber equation, mirroring the diagram structure. + +#/ (n is a DualForm0 here -- Matt's draft mistakenly placed it in Form0, but +#/ square_d0/mult_d0d0 demand a DualForm0, and the Klausmeier gluing below +#/ equates it with Phytodynamics' DualForm0 n.) +instance Hydrodynamics : DEC := [ + Form0 := [a, k], + Form1 := [dX], + DualForm0 := [w, n, x0, x1, x2, x3, x4, x5], + x0 := sub_d01([w, a]), + x1 := square_d0([n]), + x2 := mult_d0d0([w, x1]), + x3 := sub_d0d0([x0, x2]), + x4 := lie_1d0([dX, w]), + x5 := mult_0d0([k, x4]), + partial_d0([w]) := add_d0d0([x3, x5]) +] + +instance Phytodynamics : DEC := [ + Form0 := [m], + DualForm0 := [n, w, y0, y1, y2, y3, y4], + y0 := square_d0([n]), + y1 := mult_d0d0([w, y0]), + y2 := mult_0d0([m, n]), + y3 := sub_d0d0([y1, y2]), + y4 := lapl_d0([n]), + #/ NOTE: Matt's draft binds partial_d0([w]) here; for the plant-biomass + #/ equation this is likely partial_d0([n]) -- confirm against the notebook. + partial_d0([w]) := add_d0d0([y3, y4]) +] + +#/ The full Klausmeier model glues the two sub-instances along their shared +#/ water (w) and plant-density (n) fields. +instance Klausmeier : DEC := [ + hydro : Hydrodynamics, + phyto : Phytodynamics, + hydro.n := phyto.n, + hydro.w := phyto.w +] + +#/ Inlined rendering: the same equations with intermediates substituted, +#/ exercising deeply-nested multihom applications. + +instance HydrodynamicsInline : DEC := [ + Form0 := [a, k], + Form1 := [dX], + DualForm0 := [w, n], + partial_d0([w]) := add_d0d0([ + sub_d0d0([sub_d01([w, a]), mult_d0d0([w, square_d0([n])])]), + mult_0d0([k, lie_1d0([dX, w])]) + ]) +] + +instance PhytodynamicsInline : DEC := [ + Form0 := [m], + DualForm0 := [n, w], + partial_d0([w]) := add_d0d0([ + sub_d0d0([mult_d0d0([w, square_d0([n])]), mult_0d0([m, n])]), + lapl_d0([n]) + ]) +] diff --git a/packages/catlog/examples/tt/text/test_klausmeier.dbltt.snapshot b/packages/catlog/examples/tt/text/test_klausmeier.dbltt.snapshot new file mode 100644 index 000000000..7a572c931 --- /dev/null +++ b/packages/catlog/examples/tt/text/test_klausmeier.dbltt.snapshot @@ -0,0 +1,167 @@ +set_theory ThMulticategory +#/ result: set theory to ThMulticategory + +model DEC := [ + Form0 : Object, + Form1 : Object, + DualForm0 : Object, + lapl_d0 : Multihom[[DualForm0], DualForm0], + partial_0 : Multihom[[Form0], Form0], + partial_1 : Multihom[[Form1], Form1], + partial_d0 : Multihom[[DualForm0], DualForm0], + square_d0 : Multihom[[DualForm0], DualForm0], + add_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + sub_d01 : Multihom[[DualForm0, Form0], DualForm0], + sub_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + mult_00 : Multihom[[Form0, Form0], Form0], + mult_d0d0 : Multihom[[DualForm0, DualForm0], DualForm0], + mult_0d0 : Multihom[[Form0, DualForm0], DualForm0], + lie_1d0 : Multihom[[Form1, DualForm0], DualForm0], + wedge_00 : Multihom[[Form0, Form0], Form0], + wedge_10 : Multihom[[Form1, Form0], Form1] +] +#/ declared: DEC + +instance Hydrodynamics : DEC := [ + Form0 := [a, k], + Form1 := [dX], + DualForm0 := [w, n, x0, x1, x2, x3, x4, x5], + x0 := sub_d01([w, a]), + x1 := square_d0([n]), + x2 := mult_d0d0([w, x1]), + x3 := sub_d0d0([x0, x2]), + x4 := lie_1d0([dX, w]), + x5 := mult_0d0([k, x4]), + partial_d0([w]) := add_d0d0([x3, x5]) +] +#/ declared: Hydrodynamics +#/ instance generators: +#/ a : Form0 +#/ k : Form0 +#/ dX : Form1 +#/ w : DualForm0 +#/ n : DualForm0 +#/ x0 : DualForm0 +#/ x1 : DualForm0 +#/ x2 : DualForm0 +#/ x3 : DualForm0 +#/ x4 : DualForm0 +#/ x5 : DualForm0 +#/ instance equations: +#/ x0 == sub_d01([w, a]) +#/ x1 == square_d0([n]) +#/ x2 == mult_d0d0([w, x1]) +#/ x3 == sub_d0d0([x0, x2]) +#/ x4 == lie_1d0([dX, w]) +#/ x5 == mult_0d0([k, x4]) +#/ partial_d0([w]) == add_d0d0([x3, x5]) + +instance Phytodynamics : DEC := [ + Form0 := [m], + DualForm0 := [n, w, y0, y1, y2, y3, y4], + y0 := square_d0([n]), + y1 := mult_d0d0([w, y0]), + y2 := mult_0d0([m, n]), + y3 := sub_d0d0([y1, y2]), + y4 := lapl_d0([n]), + #/ NOTE: Matt's draft binds partial_d0([w]) here; for the plant-biomass + #/ equation this is likely partial_d0([n]) -- confirm against the notebook. + partial_d0([w]) := add_d0d0([y3, y4]) +] +#/ declared: Phytodynamics +#/ instance generators: +#/ m : Form0 +#/ n : DualForm0 +#/ w : DualForm0 +#/ y0 : DualForm0 +#/ y1 : DualForm0 +#/ y2 : DualForm0 +#/ y3 : DualForm0 +#/ y4 : DualForm0 +#/ instance equations: +#/ y0 == square_d0([n]) +#/ y1 == mult_d0d0([w, y0]) +#/ y2 == mult_0d0([m, n]) +#/ y3 == sub_d0d0([y1, y2]) +#/ y4 == lapl_d0([n]) +#/ partial_d0([w]) == add_d0d0([y3, y4]) + +instance Klausmeier : DEC := [ + hydro : Hydrodynamics, + phyto : Phytodynamics, + hydro.n := phyto.n, + hydro.w := phyto.w +] +#/ declared: Klausmeier +#/ instance generators: +#/ hydro.a : Form0 +#/ hydro.k : Form0 +#/ hydro.dX : Form1 +#/ hydro.w : DualForm0 +#/ hydro.n : DualForm0 +#/ hydro.x0 : DualForm0 +#/ hydro.x1 : DualForm0 +#/ hydro.x2 : DualForm0 +#/ hydro.x3 : DualForm0 +#/ hydro.x4 : DualForm0 +#/ hydro.x5 : DualForm0 +#/ phyto.m : Form0 +#/ phyto.n : DualForm0 +#/ phyto.w : DualForm0 +#/ phyto.y0 : DualForm0 +#/ phyto.y1 : DualForm0 +#/ phyto.y2 : DualForm0 +#/ phyto.y3 : DualForm0 +#/ phyto.y4 : DualForm0 +#/ instance equations: +#/ hydro.x0 == sub_d01([hydro.w, hydro.a]) +#/ hydro.x1 == square_d0([hydro.n]) +#/ hydro.x2 == mult_d0d0([hydro.w, hydro.x1]) +#/ hydro.x3 == sub_d0d0([hydro.x0, hydro.x2]) +#/ hydro.x4 == lie_1d0([hydro.dX, hydro.w]) +#/ hydro.x5 == mult_0d0([hydro.k, hydro.x4]) +#/ partial_d0([hydro.w]) == add_d0d0([hydro.x3, hydro.x5]) +#/ phyto.y0 == square_d0([phyto.n]) +#/ phyto.y1 == mult_d0d0([phyto.w, phyto.y0]) +#/ phyto.y2 == mult_0d0([phyto.m, phyto.n]) +#/ phyto.y3 == sub_d0d0([phyto.y1, phyto.y2]) +#/ phyto.y4 == lapl_d0([phyto.n]) +#/ partial_d0([phyto.w]) == add_d0d0([phyto.y3, phyto.y4]) +#/ hydro.n == phyto.n +#/ hydro.w == phyto.w + +instance HydrodynamicsInline : DEC := [ + Form0 := [a, k], + Form1 := [dX], + DualForm0 := [w, n], + partial_d0([w]) := add_d0d0([ + sub_d0d0([sub_d01([w, a]), mult_d0d0([w, square_d0([n])])]), + mult_0d0([k, lie_1d0([dX, w])]) + ]) +] +#/ declared: HydrodynamicsInline +#/ instance generators: +#/ a : Form0 +#/ k : Form0 +#/ dX : Form1 +#/ w : DualForm0 +#/ n : DualForm0 +#/ instance equations: +#/ partial_d0([w]) == add_d0d0([sub_d0d0([sub_d01([w, a]), mult_d0d0([w, square_d0([n])])]), mult_0d0([k, lie_1d0([dX, w])])]) + +instance PhytodynamicsInline : DEC := [ + Form0 := [m], + DualForm0 := [n, w], + partial_d0([w]) := add_d0d0([ + sub_d0d0([mult_d0d0([w, square_d0([n])]), mult_0d0([m, n])]), + lapl_d0([n]) + ]) +] +#/ declared: PhytodynamicsInline +#/ instance generators: +#/ m : Form0 +#/ n : DualForm0 +#/ w : DualForm0 +#/ instance equations: +#/ partial_d0([w]) == add_d0d0([sub_d0d0([mult_d0d0([w, square_d0([n])]), mult_0d0([m, n])]), lapl_d0([n])]) + From 224553acfc927c5f39b60489f931b31310a8f534 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 17:23:10 -0700 Subject: [PATCH 47/53] formatting on nb --- .../tt/notebook/sch_weighted_graph.json | 109 ++++++++++++- .../examples/tt/notebook/sir_petri.json | 154 +++++++++++++++++- 2 files changed, 261 insertions(+), 2 deletions(-) diff --git a/packages/catlog/examples/tt/notebook/sch_weighted_graph.json b/packages/catlog/examples/tt/notebook/sch_weighted_graph.json index cd94e101d..426485da9 100644 --- a/packages/catlog/examples/tt/notebook/sch_weighted_graph.json +++ b/packages/catlog/examples/tt/notebook/sch_weighted_graph.json @@ -1 +1,108 @@ -{"name":"Weighted graph","notebook":{"cellContents":{"01997d96-c72a-72f8-be4c-f19e6f4873d2":{"tag":"formal","id":"01997d96-c72a-72f8-be4c-f19e6f4873d2","content":{"tag":"object","id":"01997d96-c72a-72f8-be4c-ec4490c264b7","name":"E","obType":{"tag":"Basic","content":"Entity"}}},"01997d96-d225-76ea-a1d2-bbcd290a586e":{"tag":"formal","id":"01997d96-d225-76ea-a1d2-bbcd290a586e","content":{"tag":"object","id":"01997d96-d224-71f8-80f2-b98ef8b2899a","name":"V","obType":{"tag":"Basic","content":"Entity"}}},"01997d96-e5e2-73df-8a3e-096efcc1e54e":{"tag":"formal","id":"01997d96-e5e2-73df-8a3e-096efcc1e54e","content":{"tag":"morphism","id":"01997d96-e5e1-743d-8bf7-8a8ad24f080e","name":"weight","morType":{"tag":"Basic","content":"Attr"},"dom":{"tag":"Basic","content":"01997d96-c72a-72f8-be4c-ec4490c264b7"},"cod":{"tag":"Basic","content":"01997d96-f476-745e-adbd-780d79ef5c31"}}},"01997d96-f476-745e-adbd-7ee1400d1674":{"tag":"formal","id":"01997d96-f476-745e-adbd-7ee1400d1674","content":{"tag":"object","id":"01997d96-f476-745e-adbd-780d79ef5c31","name":"Weight","obType":{"tag":"Basic","content":"AttrType"}}},"01997d97-3020-756c-9b2f-88513943dc39":{"tag":"formal","id":"01997d97-3020-756c-9b2f-88513943dc39","content":{"tag":"morphism","id":"01997d97-3020-756c-9b2f-85cbd3a4410c","name":"src","morType":{"tag":"Hom","content":{"tag":"Basic","content":"Entity"}},"dom":{"tag":"Basic","content":"01997d96-c72a-72f8-be4c-ec4490c264b7"},"cod":{"tag":"Basic","content":"01997d96-d224-71f8-80f2-b98ef8b2899a"}}},"01997d97-4d0d-73e1-82b0-27efdcea4d4b":{"tag":"formal","id":"01997d97-4d0d-73e1-82b0-27efdcea4d4b","content":{"tag":"morphism","id":"01997d97-4d0d-73e1-82b0-2384c982ec3b","name":"tgt","morType":{"tag":"Hom","content":{"tag":"Basic","content":"Entity"}},"dom":{"tag":"Basic","content":"01997d96-c72a-72f8-be4c-ec4490c264b7"},"cod":{"tag":"Basic","content":"01997d96-d224-71f8-80f2-b98ef8b2899a"}}}},"cellOrder":["01997d96-c72a-72f8-be4c-f19e6f4873d2","01997d96-d225-76ea-a1d2-bbcd290a586e","01997d96-f476-745e-adbd-7ee1400d1674","01997d96-e5e2-73df-8a3e-096efcc1e54e","01997d97-3020-756c-9b2f-88513943dc39","01997d97-4d0d-73e1-82b0-27efdcea4d4b"]},"theory":"simple-schema","type":"model","version":"1"} \ No newline at end of file +{ + "name": "Weighted graph", + "notebook": { + "cellContents": { + "01997d96-c72a-72f8-be4c-f19e6f4873d2": { + "tag": "formal", + "id": "01997d96-c72a-72f8-be4c-f19e6f4873d2", + "content": { + "tag": "object", + "id": "01997d96-c72a-72f8-be4c-ec4490c264b7", + "name": "E", + "obType": { "tag": "Basic", "content": "Entity" } + } + }, + "01997d96-d225-76ea-a1d2-bbcd290a586e": { + "tag": "formal", + "id": "01997d96-d225-76ea-a1d2-bbcd290a586e", + "content": { + "tag": "object", + "id": "01997d96-d224-71f8-80f2-b98ef8b2899a", + "name": "V", + "obType": { "tag": "Basic", "content": "Entity" } + } + }, + "01997d96-e5e2-73df-8a3e-096efcc1e54e": { + "tag": "formal", + "id": "01997d96-e5e2-73df-8a3e-096efcc1e54e", + "content": { + "tag": "morphism", + "id": "01997d96-e5e1-743d-8bf7-8a8ad24f080e", + "name": "weight", + "morType": { "tag": "Basic", "content": "Attr" }, + "dom": { + "tag": "Basic", + "content": "01997d96-c72a-72f8-be4c-ec4490c264b7" + }, + "cod": { + "tag": "Basic", + "content": "01997d96-f476-745e-adbd-780d79ef5c31" + } + } + }, + "01997d96-f476-745e-adbd-7ee1400d1674": { + "tag": "formal", + "id": "01997d96-f476-745e-adbd-7ee1400d1674", + "content": { + "tag": "object", + "id": "01997d96-f476-745e-adbd-780d79ef5c31", + "name": "Weight", + "obType": { "tag": "Basic", "content": "AttrType" } + } + }, + "01997d97-3020-756c-9b2f-88513943dc39": { + "tag": "formal", + "id": "01997d97-3020-756c-9b2f-88513943dc39", + "content": { + "tag": "morphism", + "id": "01997d97-3020-756c-9b2f-85cbd3a4410c", + "name": "src", + "morType": { + "tag": "Hom", + "content": { "tag": "Basic", "content": "Entity" } + }, + "dom": { + "tag": "Basic", + "content": "01997d96-c72a-72f8-be4c-ec4490c264b7" + }, + "cod": { + "tag": "Basic", + "content": "01997d96-d224-71f8-80f2-b98ef8b2899a" + } + } + }, + "01997d97-4d0d-73e1-82b0-27efdcea4d4b": { + "tag": "formal", + "id": "01997d97-4d0d-73e1-82b0-27efdcea4d4b", + "content": { + "tag": "morphism", + "id": "01997d97-4d0d-73e1-82b0-2384c982ec3b", + "name": "tgt", + "morType": { + "tag": "Hom", + "content": { "tag": "Basic", "content": "Entity" } + }, + "dom": { + "tag": "Basic", + "content": "01997d96-c72a-72f8-be4c-ec4490c264b7" + }, + "cod": { + "tag": "Basic", + "content": "01997d96-d224-71f8-80f2-b98ef8b2899a" + } + } + } + }, + "cellOrder": [ + "01997d96-c72a-72f8-be4c-f19e6f4873d2", + "01997d96-d225-76ea-a1d2-bbcd290a586e", + "01997d96-f476-745e-adbd-7ee1400d1674", + "01997d96-e5e2-73df-8a3e-096efcc1e54e", + "01997d97-3020-756c-9b2f-88513943dc39", + "01997d97-4d0d-73e1-82b0-27efdcea4d4b" + ] + }, + "theory": "simple-schema", + "type": "model", + "version": "1" +} diff --git a/packages/catlog/examples/tt/notebook/sir_petri.json b/packages/catlog/examples/tt/notebook/sir_petri.json index aea520347..448c83a2b 100644 --- a/packages/catlog/examples/tt/notebook/sir_petri.json +++ b/packages/catlog/examples/tt/notebook/sir_petri.json @@ -1 +1,153 @@ -{"name":"SIR","notebook":{"cellContents":{"019c34ca-5c0e-77af-88e3-c7ae9e1936cc":{"content":{"id":"019c34ca-5c0e-77af-88e3-c2d4487cadaf","name":"S","obType":{"content":"Object","tag":"Basic"},"tag":"object"},"id":"019c34ca-5c0e-77af-88e3-c7ae9e1936cc","tag":"formal"},"019c34ca-8090-7566-9db3-49f54b5d7eae":{"content":{"id":"019c34ca-8090-7566-9db3-45d5fd4f8ffd","name":"I","obType":{"content":"Object","tag":"Basic"},"tag":"object"},"id":"019c34ca-8090-7566-9db3-49f54b5d7eae","tag":"formal"},"019c34ca-83b0-70f9-a861-663b422d3109":{"content":{"id":"019c34ca-83b0-70f9-a861-61ad91bdd5b6","name":"R","obType":{"content":"Object","tag":"Basic"},"tag":"object"},"id":"019c34ca-83b0-70f9-a861-663b422d3109","tag":"formal"},"019c34ca-92a8-702b-b09b-f711a18563b8":{"content":{"cod":{"content":{"ob":{"content":{"modality":"SymmetricList","objects":[{"content":"019c34ca-8090-7566-9db3-45d5fd4f8ffd","tag":"Basic"},{"content":"019c34ca-8090-7566-9db3-45d5fd4f8ffd","tag":"Basic"}]},"tag":"List"},"op":{"content":"tensor","tag":"Basic"}},"tag":"App"},"dom":{"content":{"ob":{"content":{"modality":"SymmetricList","objects":[{"content":"019c34ca-5c0e-77af-88e3-c2d4487cadaf","tag":"Basic"},{"content":"019c34ca-8090-7566-9db3-45d5fd4f8ffd","tag":"Basic"}]},"tag":"List"},"op":{"content":"tensor","tag":"Basic"}},"tag":"App"},"id":"019c34ca-92a8-702b-b09b-f303b34fddde","morType":{"content":{"content":"Object","tag":"Basic"},"tag":"Hom"},"name":"infect","tag":"morphism"},"id":"019c34ca-92a8-702b-b09b-f711a18563b8","tag":"formal"},"019c34ca-a808-7119-8682-626850b293e2":{"content":{"cod":{"content":{"ob":{"content":{"modality":"SymmetricList","objects":[{"content":"019c34ca-83b0-70f9-a861-61ad91bdd5b6","tag":"Basic"}]},"tag":"List"},"op":{"content":"tensor","tag":"Basic"}},"tag":"App"},"dom":{"content":{"ob":{"content":{"modality":"SymmetricList","objects":[{"content":"019c34ca-8090-7566-9db3-45d5fd4f8ffd","tag":"Basic"}]},"tag":"List"},"op":{"content":"tensor","tag":"Basic"}},"tag":"App"},"id":"019c34ca-a808-7119-8682-5d964079256e","morType":{"content":{"content":"Object","tag":"Basic"},"tag":"Hom"},"name":"recover","tag":"morphism"},"id":"019c34ca-a808-7119-8682-626850b293e2","tag":"formal"}},"cellOrder":["019c34ca-5c0e-77af-88e3-c7ae9e1936cc","019c34ca-8090-7566-9db3-49f54b5d7eae","019c34ca-83b0-70f9-a861-663b422d3109","019c34ca-92a8-702b-b09b-f711a18563b8","019c34ca-a808-7119-8682-626850b293e2"]},"theory":"petri-net","type":"model","version":"1"} \ No newline at end of file +{ + "name": "SIR", + "notebook": { + "cellContents": { + "019c34ca-5c0e-77af-88e3-c7ae9e1936cc": { + "content": { + "id": "019c34ca-5c0e-77af-88e3-c2d4487cadaf", + "name": "S", + "obType": { "content": "Object", "tag": "Basic" }, + "tag": "object" + }, + "id": "019c34ca-5c0e-77af-88e3-c7ae9e1936cc", + "tag": "formal" + }, + "019c34ca-8090-7566-9db3-49f54b5d7eae": { + "content": { + "id": "019c34ca-8090-7566-9db3-45d5fd4f8ffd", + "name": "I", + "obType": { "content": "Object", "tag": "Basic" }, + "tag": "object" + }, + "id": "019c34ca-8090-7566-9db3-49f54b5d7eae", + "tag": "formal" + }, + "019c34ca-83b0-70f9-a861-663b422d3109": { + "content": { + "id": "019c34ca-83b0-70f9-a861-61ad91bdd5b6", + "name": "R", + "obType": { "content": "Object", "tag": "Basic" }, + "tag": "object" + }, + "id": "019c34ca-83b0-70f9-a861-663b422d3109", + "tag": "formal" + }, + "019c34ca-92a8-702b-b09b-f711a18563b8": { + "content": { + "cod": { + "content": { + "ob": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "019c34ca-8090-7566-9db3-45d5fd4f8ffd", + "tag": "Basic" + }, + { + "content": "019c34ca-8090-7566-9db3-45d5fd4f8ffd", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "op": { "content": "tensor", "tag": "Basic" } + }, + "tag": "App" + }, + "dom": { + "content": { + "ob": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "019c34ca-5c0e-77af-88e3-c2d4487cadaf", + "tag": "Basic" + }, + { + "content": "019c34ca-8090-7566-9db3-45d5fd4f8ffd", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "op": { "content": "tensor", "tag": "Basic" } + }, + "tag": "App" + }, + "id": "019c34ca-92a8-702b-b09b-f303b34fddde", + "morType": { + "content": { "content": "Object", "tag": "Basic" }, + "tag": "Hom" + }, + "name": "infect", + "tag": "morphism" + }, + "id": "019c34ca-92a8-702b-b09b-f711a18563b8", + "tag": "formal" + }, + "019c34ca-a808-7119-8682-626850b293e2": { + "content": { + "cod": { + "content": { + "ob": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "019c34ca-83b0-70f9-a861-61ad91bdd5b6", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "op": { "content": "tensor", "tag": "Basic" } + }, + "tag": "App" + }, + "dom": { + "content": { + "ob": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "019c34ca-8090-7566-9db3-45d5fd4f8ffd", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "op": { "content": "tensor", "tag": "Basic" } + }, + "tag": "App" + }, + "id": "019c34ca-a808-7119-8682-5d964079256e", + "morType": { + "content": { "content": "Object", "tag": "Basic" }, + "tag": "Hom" + }, + "name": "recover", + "tag": "morphism" + }, + "id": "019c34ca-a808-7119-8682-626850b293e2", + "tag": "formal" + } + }, + "cellOrder": [ + "019c34ca-5c0e-77af-88e3-c7ae9e1936cc", + "019c34ca-8090-7566-9db3-49f54b5d7eae", + "019c34ca-83b0-70f9-a861-663b422d3109", + "019c34ca-92a8-702b-b09b-f711a18563b8", + "019c34ca-a808-7119-8682-626850b293e2" + ] + }, + "theory": "petri-net", + "type": "model", + "version": "1" +} From 064d95e4abf5ae1579ec5623d528efaf4a5ff9bf Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 18:25:10 -0700 Subject: [PATCH 48/53] document types for instances as opposed to diagrams --- packages/catlog/src/tt/notebook_elab.rs | 16 ++++++++++++++++ packages/document-types/src/v0/api.rs | 4 ++++ packages/document-types/src/v0/mod.rs | 1 + packages/document-types/src/v1/mod.rs | 5 ++++- packages/document-types/src/v2/document.rs | 15 +++++++++++++++ packages/document-types/src/v2/mod.rs | 5 ++++- 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index a2ffb3405..4e0103266 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -592,6 +592,22 @@ mod test { ); } + /// The Klausmeier instance-notebook fixtures deserialize against the + /// document schema. Elaboration of instance notebooks is not implemented + /// yet; this test pins schema/fixture agreement in the meantime. + #[test] + fn klausmeier_fixtures_deserialize() { + use catcolab_document_types::current::InstanceDocumentContent; + let src = fs::read_to_string("examples/tt/notebook/klausmeier/dec_model.json").unwrap(); + let _: ModelDocumentContent = serde_json::from_str(&src).unwrap(); + for name in ["hydrodynamics", "phytodynamics", "klausmeier"] { + let src = + fs::read_to_string(format!("examples/tt/notebook/klausmeier/{name}.json")).unwrap(); + let doc: InstanceDocumentContent = serde_json::from_str(&src).unwrap(); + assert!(doc.notebook.formal_content().count() > 0); + } + } + /// Test a notebook with an equation. #[test] fn commutative_square() { diff --git a/packages/document-types/src/v0/api.rs b/packages/document-types/src/v0/api.rs index 5ab7838eb..c75b00961 100644 --- a/packages/document-types/src/v0/api.rs +++ b/packages/document-types/src/v0/api.rs @@ -57,6 +57,9 @@ pub enum LinkType { #[serde(rename = "diagram-in")] DiagramIn, + #[serde(rename = "instance-of")] + InstanceOf, + #[serde(rename = "instantiation")] Instantiation, } @@ -75,6 +78,7 @@ pub(crate) mod arbitrary { proptest::sample::select(&[ LinkType::AnalysisOf, LinkType::DiagramIn, + LinkType::InstanceOf, LinkType::Instantiation, ]) .boxed() diff --git a/packages/document-types/src/v0/mod.rs b/packages/document-types/src/v0/mod.rs index fb177c539..c36df2736 100644 --- a/packages/document-types/src/v0/mod.rs +++ b/packages/document-types/src/v0/mod.rs @@ -3,6 +3,7 @@ pub mod api; pub mod cell; pub mod diagram_judgment; pub mod document; +pub mod instance_judgment; pub mod model; pub mod model_judgment; pub mod notebook; diff --git a/packages/document-types/src/v1/mod.rs b/packages/document-types/src/v1/mod.rs index 06d8a8699..37c6810ee 100644 --- a/packages/document-types/src/v1/mod.rs +++ b/packages/document-types/src/v1/mod.rs @@ -1,6 +1,8 @@ use crate::v0; -pub use v0::{analysis, api, cell, diagram_judgment, model, model_judgment, path, theory}; +pub use v0::{ + analysis, api, cell, diagram_judgment, instance_judgment, model, model_judgment, path, theory, +}; pub mod document; pub mod notebook; @@ -10,6 +12,7 @@ pub use api::*; pub use cell::*; pub use diagram_judgment::*; pub use document::*; +pub use instance_judgment::*; pub use model::*; pub use model_judgment::*; pub use notebook::*; diff --git a/packages/document-types/src/v2/document.rs b/packages/document-types/src/v2/document.rs index 6f142093e..7c4b4ab1c 100644 --- a/packages/document-types/src/v2/document.rs +++ b/packages/document-types/src/v2/document.rs @@ -36,6 +36,21 @@ pub struct DiagramDocumentContent { pub version: String, } +/// This is the content of an instance document, presenting an instance of the +/// model that the document's `instanceOf` link points to. +/// +/// Not yet a variant of [`Document`]: wiring into the document enum (and the +/// frontend) is deferred until instance notebooks elaborate. +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct InstanceDocumentContent { + pub name: String, + #[serde(rename = "instanceOf")] + pub instance_of: Link, + pub notebook: Notebook, + pub version: String, +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct AnalysisDocumentContent { diff --git a/packages/document-types/src/v2/mod.rs b/packages/document-types/src/v2/mod.rs index ce20e6a8a..b8a8b4dd1 100644 --- a/packages/document-types/src/v2/mod.rs +++ b/packages/document-types/src/v2/mod.rs @@ -1,6 +1,8 @@ use crate::v1; -pub use v1::{analysis, api, diagram_judgment, model, model_judgment, path, theory}; +pub use v1::{ + analysis, api, diagram_judgment, instance_judgment, model, model_judgment, path, theory, +}; pub mod cell; pub mod document; @@ -11,6 +13,7 @@ pub use api::*; pub use cell::*; pub use diagram_judgment::*; pub use document::*; +pub use instance_judgment::*; pub use model::*; pub use model_judgment::*; pub use notebook::*; From 1964f9c653ebb8bb9ea54983888e690212e69f25 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 19:21:25 -0700 Subject: [PATCH 49/53] AI will never be able to forget to git add new files like a genuine human --- .../tt/notebook/klausmeier/dec_model.json | 510 ++++++++++++++++++ .../tt/notebook/klausmeier/hydrodynamics.json | 457 ++++++++++++++++ .../tt/notebook/klausmeier/klausmeier.json | 85 +++ .../tt/notebook/klausmeier/phytodynamics.json | 372 +++++++++++++ .../src/v0/instance_judgment.rs | 131 +++++ 5 files changed, 1555 insertions(+) create mode 100644 packages/catlog/examples/tt/notebook/klausmeier/dec_model.json create mode 100644 packages/catlog/examples/tt/notebook/klausmeier/hydrodynamics.json create mode 100644 packages/catlog/examples/tt/notebook/klausmeier/klausmeier.json create mode 100644 packages/catlog/examples/tt/notebook/klausmeier/phytodynamics.json create mode 100644 packages/document-types/src/v0/instance_judgment.rs diff --git a/packages/catlog/examples/tt/notebook/klausmeier/dec_model.json b/packages/catlog/examples/tt/notebook/klausmeier/dec_model.json new file mode 100644 index 000000000..5af4fe3da --- /dev/null +++ b/packages/catlog/examples/tt/notebook/klausmeier/dec_model.json @@ -0,0 +1,510 @@ +{ + "name": "DEC", + "notebook": { + "cellContents": { + "1a374345-df5f-5182-9c83-66b3c4ad1b8e": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "e8308841-9bc2-5bd9-8558-1d3fbd9a1071", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "add_d0d0", + "tag": "morphism" + }, + "id": "1a374345-df5f-5182-9c83-66b3c4ad1b8e", + "tag": "formal" + }, + "342172c2-69d5-53f2-b243-3d66e80b4fe6": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "8347d8cf-c755-523a-b260-db827bc05af7", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "mult_d0d0", + "tag": "morphism" + }, + "id": "342172c2-69d5-53f2-b243-3d66e80b4fe6", + "tag": "formal" + }, + "3b197e62-d1e4-5d2d-82e7-95c1a6a7704c": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "1184aae2-c2e9-5a8a-8c1b-c9adc42c3dd4", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "sub_d01", + "tag": "morphism" + }, + "id": "3b197e62-d1e4-5d2d-82e7-95c1a6a7704c", + "tag": "formal" + }, + "54a45aa8-9399-5927-8faa-4e549f01bbe8": { + "content": { + "cod": { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "883176d2-85a3-5047-a6cb-e1eaa6baafb8", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "partial_1", + "tag": "morphism" + }, + "id": "54a45aa8-9399-5927-8faa-4e549f01bbe8", + "tag": "formal" + }, + "5533d80f-74d3-5872-a210-19f5b87965c0": { + "content": { + "cod": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "70fe0637-480c-5fd3-9b15-c8b0343ab5f9", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "partial_0", + "tag": "morphism" + }, + "id": "5533d80f-74d3-5872-a210-19f5b87965c0", + "tag": "formal" + }, + "607509ed-f2ef-599c-8f44-67e465a1b7c8": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "3bbb63e3-45cd-50a7-835e-200dbaf249d8", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "square_d0", + "tag": "morphism" + }, + "id": "607509ed-f2ef-599c-8f44-67e465a1b7c8", + "tag": "formal" + }, + "6ba914cf-1e47-5d29-b8fa-79cea7d7e1b8": { + "content": { + "cod": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "707a689d-3dd0-5a6d-807d-0872d6f437ba", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "mult_00", + "tag": "morphism" + }, + "id": "6ba914cf-1e47-5d29-b8fa-79cea7d7e1b8", + "tag": "formal" + }, + "72fad82e-f2b6-53bc-a3e2-4560b18fdc22": { + "content": { + "id": "5b62880f-6702-5485-9605-78a964901750", + "name": "DualForm0", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + "id": "72fad82e-f2b6-53bc-a3e2-4560b18fdc22", + "tag": "formal" + }, + "73cf5adb-0b77-5019-a2e4-8238d1fd064b": { + "content": { + "id": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "name": "Form1", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + "id": "73cf5adb-0b77-5019-a2e4-8238d1fd064b", + "tag": "formal" + }, + "877c3499-d8e5-5551-a191-174d2f6ed0dd": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "55b1c6ab-28b3-5554-a387-237cea9a8db8", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "sub_d0d0", + "tag": "morphism" + }, + "id": "877c3499-d8e5-5551-a191-174d2f6ed0dd", + "tag": "formal" + }, + "8c0383fd-0b88-52e0-873a-f9f7814916a0": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "442d7941-0b1a-52d2-83ea-7566b3302d18", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "lapl_d0", + "tag": "morphism" + }, + "id": "8c0383fd-0b88-52e0-873a-f9f7814916a0", + "tag": "formal" + }, + "98c0911b-b628-5cb5-93d9-f516627e6245": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "3a4f308f-ba91-55af-b87d-629f48b206ec", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "partial_d0", + "tag": "morphism" + }, + "id": "98c0911b-b628-5cb5-93d9-f516627e6245", + "tag": "formal" + }, + "accfd5e9-9df2-549a-b6ff-d97445461dcb": { + "content": { + "cod": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "03fdb89b-edd8-5013-9d67-b6ea37e2291c", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "wedge_00", + "tag": "morphism" + }, + "id": "accfd5e9-9df2-549a-b6ff-d97445461dcb", + "tag": "formal" + }, + "dbe66955-0b78-55e9-8654-dc3bc048bedd": { + "content": { + "cod": { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + }, + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "7fd9ae1e-0dc8-5dcf-ba8c-09b3b9d799e6", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "wedge_10", + "tag": "morphism" + }, + "id": "dbe66955-0b78-55e9-8654-dc3bc048bedd", + "tag": "formal" + }, + "e4b5d611-b095-5e3a-b3e1-76a88b4becbe": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + }, + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "15a851ab-9d76-568d-941b-f2141845e792", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "lie_1d0", + "tag": "morphism" + }, + "id": "e4b5d611-b095-5e3a-b3e1-76a88b4becbe", + "tag": "formal" + }, + "efca3f7e-c21e-5252-ab9d-5762ce0cef20": { + "content": { + "cod": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "dom": { + "content": { + "modality": "SymmetricList", + "objects": [ + { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + } + ] + }, + "tag": "List" + }, + "id": "3a1b19df-9345-585a-b673-93383680e0ff", + "morType": { + "content": "Multihom", + "tag": "Basic" + }, + "name": "mult_0d0", + "tag": "morphism" + }, + "id": "efca3f7e-c21e-5252-ab9d-5762ce0cef20", + "tag": "formal" + }, + "fddce294-699f-50d7-b13f-595870968434": { + "content": { + "id": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "name": "Form0", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + "id": "fddce294-699f-50d7-b13f-595870968434", + "tag": "formal" + } + }, + "cellOrder": [ + "fddce294-699f-50d7-b13f-595870968434", + "73cf5adb-0b77-5019-a2e4-8238d1fd064b", + "72fad82e-f2b6-53bc-a3e2-4560b18fdc22", + "8c0383fd-0b88-52e0-873a-f9f7814916a0", + "5533d80f-74d3-5872-a210-19f5b87965c0", + "54a45aa8-9399-5927-8faa-4e549f01bbe8", + "98c0911b-b628-5cb5-93d9-f516627e6245", + "607509ed-f2ef-599c-8f44-67e465a1b7c8", + "1a374345-df5f-5182-9c83-66b3c4ad1b8e", + "3b197e62-d1e4-5d2d-82e7-95c1a6a7704c", + "877c3499-d8e5-5551-a191-174d2f6ed0dd", + "6ba914cf-1e47-5d29-b8fa-79cea7d7e1b8", + "342172c2-69d5-53f2-b243-3d66e80b4fe6", + "efca3f7e-c21e-5252-ab9d-5762ce0cef20", + "e4b5d611-b095-5e3a-b3e1-76a88b4becbe", + "accfd5e9-9df2-549a-b6ff-d97445461dcb", + "dbe66955-0b78-55e9-8654-dc3bc048bedd" + ] + }, + "theory": "multicategory", + "type": "model", + "version": "2" +} diff --git a/packages/catlog/examples/tt/notebook/klausmeier/hydrodynamics.json b/packages/catlog/examples/tt/notebook/klausmeier/hydrodynamics.json new file mode 100644 index 000000000..77aa672b6 --- /dev/null +++ b/packages/catlog/examples/tt/notebook/klausmeier/hydrodynamics.json @@ -0,0 +1,457 @@ +{ + "instanceOf": { + "_id": "dec_model", + "_server": "catcolab.org", + "_version": null, + "type": "instance-of" + }, + "name": "Hydrodynamics", + "notebook": { + "cellContents": { + "106a6abd-e027-55d4-96ca-df15f575f482": { + "content": { + "id": "d91b49f2-5b1c-5419-b04f-62b74aabc526", + "lhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "ea1e412c-4f98-5826-ae11-73d616b455ae", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3a4f308f-ba91-55af-b87d-629f48b206ec", + "tag": "Basic" + } + }, + "tag": "App" + }, + "name": "dt_w", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "b1c8325a-de26-5e0d-bf0e-0d9e7b1d6fbf", + "tag": "Generator" + }, + { + "content": "3ab01ae1-7f00-5109-b6bc-1d4e0d05a8a6", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "e8308841-9bc2-5bd9-8558-1d3fbd9a1071", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "106a6abd-e027-55d4-96ca-df15f575f482", + "tag": "formal" + }, + "1fe18bb5-699d-598f-bebf-f83df924759d": { + "content": { + "id": "e514acf8-4d3e-57e7-ad75-2d0244044889", + "name": "n", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "1fe18bb5-699d-598f-bebf-f83df924759d", + "tag": "formal" + }, + "20683357-ac91-5ef8-8015-5e3252f6ad65": { + "content": { + "id": "3e813cda-2117-5bdf-b792-3b2c8ed6cf2b", + "lhs": { + "content": "cd84f055-bf0d-5df7-bee6-d1ba424a25d1", + "tag": "Generator" + }, + "name": "x0", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "ea1e412c-4f98-5826-ae11-73d616b455ae", + "tag": "Generator" + }, + { + "content": "e4484d37-8168-5710-92f2-7763561cf694", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "1184aae2-c2e9-5a8a-8c1b-c9adc42c3dd4", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "20683357-ac91-5ef8-8015-5e3252f6ad65", + "tag": "formal" + }, + "32ddb0e8-1f7c-5e8f-bc61-7bce6ba40ed3": { + "content": { + "id": "cd84f055-bf0d-5df7-bee6-d1ba424a25d1", + "name": "x0", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "32ddb0e8-1f7c-5e8f-bc61-7bce6ba40ed3", + "tag": "formal" + }, + "3ce4f25a-ca15-5293-b421-8d993395ad74": { + "content": { + "id": "3bfa0def-532d-528e-b101-6785c6ceeefd", + "name": "x4", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "3ce4f25a-ca15-5293-b421-8d993395ad74", + "tag": "formal" + }, + "4eec6679-5ef8-5280-a3ec-4ba69b9edbb9": { + "content": { + "id": "36fa1566-b334-5517-929a-71ca3fb3f4f7", + "name": "dX", + "over": { + "content": "b20812f2-5c13-5b5b-b22e-4d6116b70cfb", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "4eec6679-5ef8-5280-a3ec-4ba69b9edbb9", + "tag": "formal" + }, + "626865e9-c3ca-57a3-9d2b-ebdb272f1e3b": { + "content": { + "id": "b5d25f13-4513-50d5-be7f-3d14b35a6536", + "name": "x1", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "626865e9-c3ca-57a3-9d2b-ebdb272f1e3b", + "tag": "formal" + }, + "64317784-ab2c-5a2c-b8a7-3a9f0398563d": { + "content": { + "id": "3ab01ae1-7f00-5109-b6bc-1d4e0d05a8a6", + "name": "x5", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "64317784-ab2c-5a2c-b8a7-3a9f0398563d", + "tag": "formal" + }, + "75e02274-df1b-52f6-a5b4-6ce1f440cf16": { + "content": { + "id": "a40b2b97-958b-5590-a1c4-ca4a4e2a1a48", + "lhs": { + "content": "3bfa0def-532d-528e-b101-6785c6ceeefd", + "tag": "Generator" + }, + "name": "x4", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "36fa1566-b334-5517-929a-71ca3fb3f4f7", + "tag": "Generator" + }, + { + "content": "ea1e412c-4f98-5826-ae11-73d616b455ae", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "15a851ab-9d76-568d-941b-f2141845e792", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "75e02274-df1b-52f6-a5b4-6ce1f440cf16", + "tag": "formal" + }, + "76ed6707-49e0-51c3-a7fc-596e15290107": { + "content": { + "id": "ea1e412c-4f98-5826-ae11-73d616b455ae", + "name": "w", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "76ed6707-49e0-51c3-a7fc-596e15290107", + "tag": "formal" + }, + "778c841a-5e5a-525a-84d8-2a4ce600c05b": { + "content": { + "id": "e7ea8f5a-1cee-5ed3-acd7-6cc2eacf4938", + "lhs": { + "content": "b1c8325a-de26-5e0d-bf0e-0d9e7b1d6fbf", + "tag": "Generator" + }, + "name": "x3", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "cd84f055-bf0d-5df7-bee6-d1ba424a25d1", + "tag": "Generator" + }, + { + "content": "d2f4cedc-9a9c-5dcb-9191-bbe22b19c047", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "55b1c6ab-28b3-5554-a387-237cea9a8db8", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "778c841a-5e5a-525a-84d8-2a4ce600c05b", + "tag": "formal" + }, + "794a91df-b9d8-5de6-9701-404bee687d42": { + "content": { + "id": "e4484d37-8168-5710-92f2-7763561cf694", + "name": "a", + "over": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "794a91df-b9d8-5de6-9701-404bee687d42", + "tag": "formal" + }, + "921c7498-6735-5657-927d-d1e073833250": { + "content": { + "id": "d2f4cedc-9a9c-5dcb-9191-bbe22b19c047", + "name": "x2", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "921c7498-6735-5657-927d-d1e073833250", + "tag": "formal" + }, + "9bc3cde3-c0ce-553c-aca5-78db66ddc1ab": { + "content": { + "id": "32b7e9b6-3be2-5efc-837b-9ddcc294758f", + "name": "k", + "over": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "9bc3cde3-c0ce-553c-aca5-78db66ddc1ab", + "tag": "formal" + }, + "aed296df-42b8-523c-93a1-8ead9b476bdb": { + "content": { + "id": "b1c8325a-de26-5e0d-bf0e-0d9e7b1d6fbf", + "name": "x3", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "aed296df-42b8-523c-93a1-8ead9b476bdb", + "tag": "formal" + }, + "c218d84b-1230-5240-8dc3-034107307805": { + "content": { + "id": "99131db7-a212-5332-ba6f-60c01b9b15e6", + "lhs": { + "content": "3ab01ae1-7f00-5109-b6bc-1d4e0d05a8a6", + "tag": "Generator" + }, + "name": "x5", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "32b7e9b6-3be2-5efc-837b-9ddcc294758f", + "tag": "Generator" + }, + { + "content": "3bfa0def-532d-528e-b101-6785c6ceeefd", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3a1b19df-9345-585a-b673-93383680e0ff", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "c218d84b-1230-5240-8dc3-034107307805", + "tag": "formal" + }, + "daa0f4cb-b2a9-5bb7-93ba-2c509cc66b5d": { + "content": { + "id": "249b9f4b-757e-5172-b314-9882256ca9f9", + "lhs": { + "content": "b5d25f13-4513-50d5-be7f-3d14b35a6536", + "tag": "Generator" + }, + "name": "x1", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "e514acf8-4d3e-57e7-ad75-2d0244044889", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3bbb63e3-45cd-50a7-835e-200dbaf249d8", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "daa0f4cb-b2a9-5bb7-93ba-2c509cc66b5d", + "tag": "formal" + }, + "f4c7a514-5705-5e31-9ec0-fd4c529e9184": { + "content": { + "id": "08301ffc-4f50-51de-a30a-bc5392735faa", + "lhs": { + "content": "d2f4cedc-9a9c-5dcb-9191-bbe22b19c047", + "tag": "Generator" + }, + "name": "x2", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "ea1e412c-4f98-5826-ae11-73d616b455ae", + "tag": "Generator" + }, + { + "content": "b5d25f13-4513-50d5-be7f-3d14b35a6536", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "8347d8cf-c755-523a-b260-db827bc05af7", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "f4c7a514-5705-5e31-9ec0-fd4c529e9184", + "tag": "formal" + } + }, + "cellOrder": [ + "794a91df-b9d8-5de6-9701-404bee687d42", + "9bc3cde3-c0ce-553c-aca5-78db66ddc1ab", + "4eec6679-5ef8-5280-a3ec-4ba69b9edbb9", + "76ed6707-49e0-51c3-a7fc-596e15290107", + "1fe18bb5-699d-598f-bebf-f83df924759d", + "32ddb0e8-1f7c-5e8f-bc61-7bce6ba40ed3", + "626865e9-c3ca-57a3-9d2b-ebdb272f1e3b", + "921c7498-6735-5657-927d-d1e073833250", + "aed296df-42b8-523c-93a1-8ead9b476bdb", + "3ce4f25a-ca15-5293-b421-8d993395ad74", + "64317784-ab2c-5a2c-b8a7-3a9f0398563d", + "20683357-ac91-5ef8-8015-5e3252f6ad65", + "daa0f4cb-b2a9-5bb7-93ba-2c509cc66b5d", + "f4c7a514-5705-5e31-9ec0-fd4c529e9184", + "778c841a-5e5a-525a-84d8-2a4ce600c05b", + "75e02274-df1b-52f6-a5b4-6ce1f440cf16", + "c218d84b-1230-5240-8dc3-034107307805", + "106a6abd-e027-55d4-96ca-df15f575f482" + ] + }, + "type": "instance", + "version": "2" +} diff --git a/packages/catlog/examples/tt/notebook/klausmeier/klausmeier.json b/packages/catlog/examples/tt/notebook/klausmeier/klausmeier.json new file mode 100644 index 000000000..a308b496b --- /dev/null +++ b/packages/catlog/examples/tt/notebook/klausmeier/klausmeier.json @@ -0,0 +1,85 @@ +{ + "instanceOf": { + "_id": "dec_model", + "_server": "catcolab.org", + "_version": null, + "type": "instance-of" + }, + "name": "Klausmeier", + "notebook": { + "cellContents": { + "04346fd8-836f-5f2d-ac8d-ed4c2db7de9d": { + "content": { + "id": "2efa1069-f315-5d31-bbed-4ac4bbafb42a", + "lhs": { + "content": "0ef9dd0c-e099-5897-b2bf-213af44f8f4a.e514acf8-4d3e-57e7-ad75-2d0244044889", + "tag": "Generator" + }, + "name": "glue_n", + "rhs": { + "content": "217c4878-87ad-560b-a992-c66db09212a2.471460c5-a2ee-5e44-8c57-8034ad370593", + "tag": "Generator" + }, + "tag": "equation" + }, + "id": "04346fd8-836f-5f2d-ac8d-ed4c2db7de9d", + "tag": "formal" + }, + "6f756822-6698-5ef3-b054-7e2142d4cd60": { + "content": { + "id": "0ef9dd0c-e099-5897-b2bf-213af44f8f4a", + "instance": { + "_id": "hydrodynamics", + "_server": "catcolab.org", + "_version": null, + "type": "instantiation" + }, + "name": "hydro", + "tag": "import" + }, + "id": "6f756822-6698-5ef3-b054-7e2142d4cd60", + "tag": "formal" + }, + "f3a29761-7bf0-50e6-b164-6a2d19ab981c": { + "content": { + "id": "217c4878-87ad-560b-a992-c66db09212a2", + "instance": { + "_id": "phytodynamics", + "_server": "catcolab.org", + "_version": null, + "type": "instantiation" + }, + "name": "phyto", + "tag": "import" + }, + "id": "f3a29761-7bf0-50e6-b164-6a2d19ab981c", + "tag": "formal" + }, + "fc199e1f-e2b2-59db-bdb8-0a35235315c7": { + "content": { + "id": "f4b19c42-0541-5050-9425-f7c4242ea2b9", + "lhs": { + "content": "0ef9dd0c-e099-5897-b2bf-213af44f8f4a.ea1e412c-4f98-5826-ae11-73d616b455ae", + "tag": "Generator" + }, + "name": "glue_w", + "rhs": { + "content": "217c4878-87ad-560b-a992-c66db09212a2.4bf26d23-ad3c-5cec-9c9a-d49c35bd0f64", + "tag": "Generator" + }, + "tag": "equation" + }, + "id": "fc199e1f-e2b2-59db-bdb8-0a35235315c7", + "tag": "formal" + } + }, + "cellOrder": [ + "6f756822-6698-5ef3-b054-7e2142d4cd60", + "f3a29761-7bf0-50e6-b164-6a2d19ab981c", + "04346fd8-836f-5f2d-ac8d-ed4c2db7de9d", + "fc199e1f-e2b2-59db-bdb8-0a35235315c7" + ] + }, + "type": "instance", + "version": "2" +} diff --git a/packages/catlog/examples/tt/notebook/klausmeier/phytodynamics.json b/packages/catlog/examples/tt/notebook/klausmeier/phytodynamics.json new file mode 100644 index 000000000..85fe0b532 --- /dev/null +++ b/packages/catlog/examples/tt/notebook/klausmeier/phytodynamics.json @@ -0,0 +1,372 @@ +{ + "instanceOf": { + "_id": "dec_model", + "_server": "catcolab.org", + "_version": null, + "type": "instance-of" + }, + "name": "Phytodynamics", + "notebook": { + "cellContents": { + "041949ed-aee8-57f4-ae2f-2a4051c4ec87": { + "content": { + "id": "4bf26d23-ad3c-5cec-9c9a-d49c35bd0f64", + "name": "w", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "041949ed-aee8-57f4-ae2f-2a4051c4ec87", + "tag": "formal" + }, + "53c2d96e-a94f-5f48-ae82-a1c47121b386": { + "content": { + "id": "41ef7859-fcee-55ac-ab8c-e2f4c4af0f2c", + "lhs": { + "content": "c1ef64cf-8968-5e41-8d64-40949d25daf2", + "tag": "Generator" + }, + "name": "y2", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "c46c610a-611e-5ebb-8276-304cd3ef5e99", + "tag": "Generator" + }, + { + "content": "471460c5-a2ee-5e44-8c57-8034ad370593", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3a1b19df-9345-585a-b673-93383680e0ff", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "53c2d96e-a94f-5f48-ae82-a1c47121b386", + "tag": "formal" + }, + "7707cc47-3162-5b28-aa52-ba7cfff37008": { + "content": { + "id": "b93fbd14-b77c-507f-bac1-1908fc1cd224", + "lhs": { + "content": "ad814f83-f647-5a9e-983a-f6942e4790f3", + "tag": "Generator" + }, + "name": "y3", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "5d85d291-b5f7-510f-967d-c259b7cdcb63", + "tag": "Generator" + }, + { + "content": "c1ef64cf-8968-5e41-8d64-40949d25daf2", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "55b1c6ab-28b3-5554-a387-237cea9a8db8", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "7707cc47-3162-5b28-aa52-ba7cfff37008", + "tag": "formal" + }, + "8bec0a75-bca8-59a0-aa9c-122daddc5c94": { + "content": { + "id": "c1ef64cf-8968-5e41-8d64-40949d25daf2", + "name": "y2", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "8bec0a75-bca8-59a0-aa9c-122daddc5c94", + "tag": "formal" + }, + "a212d5e0-4c8e-5925-aa68-ed568075a1d9": { + "content": { + "id": "3c4fd918-4b55-5723-b3ee-81109a668daf", + "name": "y0", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "a212d5e0-4c8e-5925-aa68-ed568075a1d9", + "tag": "formal" + }, + "a5abff61-28bd-5239-a17d-038c85907d5a": { + "content": { + "id": "ad814f83-f647-5a9e-983a-f6942e4790f3", + "name": "y3", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "a5abff61-28bd-5239-a17d-038c85907d5a", + "tag": "formal" + }, + "b6faab41-0f79-514a-892d-2683651640c4": { + "content": { + "id": "db4d3b25-0fd0-5180-b315-31a4903712a0", + "lhs": { + "content": "5d85d291-b5f7-510f-967d-c259b7cdcb63", + "tag": "Generator" + }, + "name": "y1", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "4bf26d23-ad3c-5cec-9c9a-d49c35bd0f64", + "tag": "Generator" + }, + { + "content": "3c4fd918-4b55-5723-b3ee-81109a668daf", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "8347d8cf-c755-523a-b260-db827bc05af7", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "b6faab41-0f79-514a-892d-2683651640c4", + "tag": "formal" + }, + "bf6410f3-4f35-5749-a47a-91a6d3926da4": { + "content": { + "id": "b694063c-4d37-5992-8ca7-8c1648d40b16", + "name": "y4", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "bf6410f3-4f35-5749-a47a-91a6d3926da4", + "tag": "formal" + }, + "c7f92d49-62bb-5bc8-b8ba-c97eae09ca39": { + "content": { + "id": "5d85d291-b5f7-510f-967d-c259b7cdcb63", + "name": "y1", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "c7f92d49-62bb-5bc8-b8ba-c97eae09ca39", + "tag": "formal" + }, + "d3a94d9f-5605-5f66-ba85-db20ae529407": { + "content": { + "id": "2f4eb787-2df9-5469-9518-52308b1d1370", + "lhs": { + "content": "3c4fd918-4b55-5723-b3ee-81109a668daf", + "tag": "Generator" + }, + "name": "y0", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "471460c5-a2ee-5e44-8c57-8034ad370593", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3bbb63e3-45cd-50a7-835e-200dbaf249d8", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "d3a94d9f-5605-5f66-ba85-db20ae529407", + "tag": "formal" + }, + "e3d8d25e-650d-5a79-b732-0d7a017580d1": { + "content": { + "id": "c6606495-3b90-56a4-b871-8ed684b123df", + "lhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "4bf26d23-ad3c-5cec-9c9a-d49c35bd0f64", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "3a4f308f-ba91-55af-b87d-629f48b206ec", + "tag": "Basic" + } + }, + "tag": "App" + }, + "name": "dt_w", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "ad814f83-f647-5a9e-983a-f6942e4790f3", + "tag": "Generator" + }, + { + "content": "b694063c-4d37-5992-8ca7-8c1648d40b16", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "e8308841-9bc2-5bd9-8558-1d3fbd9a1071", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "e3d8d25e-650d-5a79-b732-0d7a017580d1", + "tag": "formal" + }, + "e63d3629-4296-5dfc-a9a0-9a8cd6ef4f17": { + "content": { + "id": "471460c5-a2ee-5e44-8c57-8034ad370593", + "name": "n", + "over": { + "content": "5b62880f-6702-5485-9605-78a964901750", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "e63d3629-4296-5dfc-a9a0-9a8cd6ef4f17", + "tag": "formal" + }, + "e6882460-5932-5cbc-b34b-e6c82f5f4082": { + "content": { + "id": "b0a4c980-c481-5869-9dbd-1bf5d13a5bba", + "lhs": { + "content": "b694063c-4d37-5992-8ca7-8c1648d40b16", + "tag": "Generator" + }, + "name": "y4", + "rhs": { + "content": { + "arg": { + "content": { + "modality": "SymmetricList", + "terms": [ + { + "content": "471460c5-a2ee-5e44-8c57-8034ad370593", + "tag": "Generator" + } + ] + }, + "tag": "List" + }, + "mor": { + "content": "442d7941-0b1a-52d2-83ea-7566b3302d18", + "tag": "Basic" + } + }, + "tag": "App" + }, + "tag": "equation" + }, + "id": "e6882460-5932-5cbc-b34b-e6c82f5f4082", + "tag": "formal" + }, + "fff789e3-eca2-5013-9fe2-dea4b38480c2": { + "content": { + "id": "c46c610a-611e-5ebb-8276-304cd3ef5e99", + "name": "m", + "over": { + "content": "0b11fe33-a54c-5ce3-90b4-c6fb951beb92", + "tag": "Basic" + }, + "tag": "generator" + }, + "id": "fff789e3-eca2-5013-9fe2-dea4b38480c2", + "tag": "formal" + } + }, + "cellOrder": [ + "fff789e3-eca2-5013-9fe2-dea4b38480c2", + "e63d3629-4296-5dfc-a9a0-9a8cd6ef4f17", + "041949ed-aee8-57f4-ae2f-2a4051c4ec87", + "a212d5e0-4c8e-5925-aa68-ed568075a1d9", + "c7f92d49-62bb-5bc8-b8ba-c97eae09ca39", + "8bec0a75-bca8-59a0-aa9c-122daddc5c94", + "a5abff61-28bd-5239-a17d-038c85907d5a", + "bf6410f3-4f35-5749-a47a-91a6d3926da4", + "d3a94d9f-5605-5f66-ba85-db20ae529407", + "b6faab41-0f79-514a-892d-2683651640c4", + "53c2d96e-a94f-5f48-ae82-a1c47121b386", + "7707cc47-3162-5b28-aa52-ba7cfff37008", + "e6882460-5932-5cbc-b34b-e6c82f5f4082", + "e3d8d25e-650d-5a79-b732-0d7a017580d1" + ] + }, + "type": "instance", + "version": "2" +} diff --git a/packages/document-types/src/v0/instance_judgment.rs b/packages/document-types/src/v0/instance_judgment.rs new file mode 100644 index 000000000..83923889d --- /dev/null +++ b/packages/document-types/src/v0/instance_judgment.rs @@ -0,0 +1,131 @@ +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use uuid::Uuid; + +use super::api::Link; +use super::model::{Mor, Ob}; +use super::theory::{Modality, ObOp}; + +/// Declares a generator of an instance of a model. +/// +/// The generator lies in the fiber over an object of the codomain model. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi, missing_as_null)] +pub struct InstanceGenDecl { + /// Human-readable label for generator. + pub name: String, + + /// Globally unique identifier of generator. + pub id: Uuid, + + /// Object of the codomain model that the generator lies over, if defined. + pub over: Option, +} + +/// Imports another instance of the codomain model into this instance. +/// +/// The generators of the imported instance become available in equations +/// under qualified names, projecting out of the import. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi, missing_as_null)] +pub struct InstanceImport { + /// Human-readable label for the import. + pub name: String, + + /// Globally unique identifier of the import. + pub id: Uuid, + + /// Link to the instance document to import, if defined. + pub instance: Option, +} + +/// Declares an equation between two terms in an instance. +/// +/// The two sides are [instance terms](InstanceTm), built from generators by +/// the actions of the codomain model's morphisms. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi, missing_as_null)] +pub struct InstanceEqnDecl { + /// Human-readable label for equation. + pub name: String, + + /// Globally unique identifier of equation. + pub id: Uuid, + + /// The left-hand side of the equation, if defined. + pub lhs: Option, + + /// The right-hand side of the equation, if defined. + pub rhs: Option, +} + +/// A term in the language of an instance of a model. +/// +/// Terms are given at the syntax level: applications may nest freely, as in +/// `add(sub([x, y]), z)`. Elaboration normalizes a term to a single morphism +/// action applied once to a base of generators. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[serde(tag = "tag", content = "content")] +#[tsify(into_wasm_abi, from_wasm_abi, missing_as_null)] +pub enum InstanceTm { + /// Reference to a generator by qualified name. + /// + /// Generators of imported instances are referenced by paths through the + /// import, e.g. `"."`. + Generator(String), + + /// The action of a morphism of the codomain model on an argument term. + App { + /// The acting morphism. + mor: Mor, + + /// The argument, lying over the morphism's domain. + arg: Box, + }, + + /// List of terms, each possibly ill-defined, in a list modality. + /// + /// Lies over a list object of the codomain model. + List { + /// The list modality. + modality: Modality, + + /// The terms in the list. + terms: Vec>, + }, + + /// Application of an object operation to a term. + /// + /// Lies over the operation applied to the fiber of the argument, e.g. a + /// term over a tensor product of objects. + ObApp { + /// The object operation. + op: ObOp, + + /// The argument term. + tm: Box, + }, +} + +/// A judgment defining part of an instance of a model of a double theory. +/// +/// Instance notebooks target presentations of instances +/// via fibered generators plus equations between morphism actions on them, in +/// contrast to [diagram judgments](super::diagram_judgment::DiagramJudgment), +/// which present instances less efficiently via a model morphism. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[serde(tag = "tag")] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum InstanceJudgment { + /// Declares a generator of the instance. + #[serde(rename = "generator")] + Generator(InstanceGenDecl), + + /// Imports another instance of the codomain model. + #[serde(rename = "import")] + Import(InstanceImport), + + /// Declares an equation between two instance terms. + #[serde(rename = "equation")] + Equation(InstanceEqnDecl), +} From 5bdec7d8a00fff440ffa9ce5de70f30632058a20 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 21:15:01 -0700 Subject: [PATCH 50/53] notebook elaboration --- packages/catlog/src/dbl/model.rs | 15 + packages/catlog/src/tt/batch.rs | 66 +-- packages/catlog/src/tt/modelgen.rs | 20 +- packages/catlog/src/tt/notebook_elab.rs | 581 +++++++++++++++++++++++- 4 files changed, 631 insertions(+), 51 deletions(-) diff --git a/packages/catlog/src/dbl/model.rs b/packages/catlog/src/dbl/model.rs index aa087b97f..9b3156918 100644 --- a/packages/catlog/src/dbl/model.rs +++ b/packages/catlog/src/dbl/model.rs @@ -308,6 +308,18 @@ pub enum InvalidDblModel { /// No link provided for instantiation cell, or wrong type of link. InvalidLink(QualifiedName), + + /// Reference to an undefined generator or import in an instance term. + FiberElement(QualifiedName), + + /// Instance term has an invalid fiber type: an application to an + /// argument over the wrong object, or an equation between elements + /// with inconvertible fiber types. + FiberType(QualifiedName), + + /// Imported instance has a codomain model different from the enclosing + /// instance's. + ImportCodomain(QualifiedName), } /// A failure of an equation in a model of a double theory to be well defined. @@ -355,4 +367,7 @@ pub enum Feature { ComplexMorType, /// Equation between one or more undefined morphisms. PartialEquation, + /// Application of a composite morphism in an instance term. Nested + /// applications express the same thing. + CompositeApplication, } diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index a4f4d4486..228fc0f05 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -23,7 +23,7 @@ use crate::dbl::modal::{ }; use crate::dbl::model_instance::{DblModelInstance, HasInstanceTerm}; use crate::one::path::Path; -use crate::zero::NameSegment; +use crate::zero::{NameSegment, Namespace}; declare_error!(TOP_ERROR, "top", "an error at the top-level"); @@ -75,27 +75,30 @@ impl BatchOutput { } } - fn instance_summary(&self, instance: &ModelInstance) { + fn instance_summary(&self, instance: &ModelInstance, ns: &Namespace) { if let BatchOutput::Snapshot(out) = self { let mut out = out.borrow_mut(); match instance { ModelInstance::Discrete(instance) => write_instance_summary( &mut out, instance, - |fiber| format!("{fiber}"), - format_instance_term, + ns, + |fiber| ns.label_string(fiber), + |tm| format_instance_term(tm, ns), ), ModelInstance::ModalUnital(instance) => write_instance_summary( &mut out, instance, - format_modal_ob, - format_modal_instance_term, + ns, + |ob| format_modal_ob(ob, ns), + |tm| format_modal_instance_term(tm, ns), ), ModelInstance::ModalNonUnital(instance) => write_instance_summary( &mut out, instance, - format_modal_ob, - format_modal_instance_term, + ns, + |ob| format_modal_ob(ob, ns), + |tm| format_modal_instance_term(tm, ns), ), } } @@ -230,7 +233,7 @@ pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result output.instance_summary(&instance), + Ok((instance, ns)) => output.instance_summary(&instance, &ns), Err(msg) => output.instance_error(&msg), } } @@ -298,11 +301,11 @@ fn snapshot_examples() { /// Render an instance term for snapshot output as `f(g(base))`, with /// `f` the outermost (last-applied) model morphism in the path. -fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { - let mut s = format!("{}", tm.base); +pub(crate) fn format_instance_term(tm: &DiscreteInstanceTerm, ns: &Namespace) -> String { + let mut s = ns.label_string(&tm.base); if let Path::Seq(edges) = &tm.path { for mor in edges.iter() { - s = format!("{}({})", mor, s); + s = format!("{}({})", ns.label_string(mor), s); } } s @@ -310,9 +313,10 @@ fn format_instance_term(tm: &DiscreteInstanceTerm) -> String { /// Writes the generators and equations of an instance, using the given /// per-doctrine formatters for fibers and equation terms. -fn write_instance_summary( +pub(crate) fn write_instance_summary( out: &mut String, instance: &DblModelInstance, + ns: &Namespace, fmt_ob: impl Fn(&M::Ob) -> String, fmt_term: impl Fn(&M::Term) -> String, ) { @@ -325,7 +329,7 @@ fn write_instance_summary( if !gens.is_empty() { writeln!(out, "#/ instance generators:").unwrap(); for (name, fiber) in &gens { - writeln!(out, "#/ {name} : {}", fmt_ob(fiber)).unwrap(); + writeln!(out, "#/ {} : {}", ns.label_string(name), fmt_ob(fiber)).unwrap(); } } if !eqns.is_empty() { @@ -338,12 +342,12 @@ fn write_instance_summary( /// Renders a modal object for snapshot output: generators by name, object /// operations as `op(inner)`, and lists as `[a, b, …]`. -fn format_modal_ob(ob: &ModalOb) -> String { +pub(crate) fn format_modal_ob(ob: &ModalOb, ns: &Namespace) -> String { match ob { - ModalOb::Generator(name) => format!("{name}"), - ModalOb::App(inner, op) => format!("{op}({})", format_modal_ob(inner)), + ModalOb::Generator(name) => ns.label_string(name), + ModalOb::App(inner, op) => format!("{op}({})", format_modal_ob(inner, ns)), ModalOb::List(_, obs) => { - let inner: Vec<_> = obs.iter().map(format_modal_ob).collect(); + let inner: Vec<_> = obs.iter().map(|ob| format_modal_ob(ob, ns)).collect(); format!("[{}]", inner.join(", ")) } } @@ -375,18 +379,18 @@ impl Rendered { /// Renders a modal instance term as e.g. `op([x, unit([])])`, re-interleaving /// the morphism with its base (the inverse of the flattening done during /// extraction). -fn format_modal_instance_term(tm: &ModalInstanceTerm) -> String { - apply_mor(&tm.mor, base_rendered(&tm.base)).render() +pub(crate) fn format_modal_instance_term(tm: &ModalInstanceTerm, ns: &Namespace) -> String { + apply_mor(&tm.mor, base_rendered(&tm.base, ns), ns).render() } -fn base_rendered(base: &ModalInstanceBase) -> Rendered { +fn base_rendered(base: &ModalInstanceBase, ns: &Namespace) -> Rendered { match base { - ModalInstanceBase::Generator(name) => Rendered::Gen(format!("{name}")), + ModalInstanceBase::Generator(name) => Rendered::Gen(ns.label_string(name)), ModalInstanceBase::List(_, bases) => { - Rendered::List(bases.iter().map(base_rendered).collect()) + Rendered::List(bases.iter().map(|b| base_rendered(b, ns)).collect()) } ModalInstanceBase::ObApp(op, inner) => { - Rendered::ObApp(format!("{op}"), Box::new(base_rendered(inner))) + Rendered::ObApp(format!("{op}"), Box::new(base_rendered(inner, ns))) } } } @@ -395,12 +399,12 @@ fn base_rendered(base: &ModalInstanceBase) -> Rendered { /// `Composite`/`List` tupling introduced by normalization: a `Composite` path /// folds its morphisms outermost-last, and a list morphism zips into a list /// argument. -fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { +fn apply_mor(mor: &ModalMor, arg: Rendered, ns: &Namespace) -> Rendered { if modal_mor_as_identity(mor).is_some() { return arg; } match mor { - ModalMor::Generator(name) => Rendered::App(format!("{name}"), Box::new(arg)), + ModalMor::Generator(name) => Rendered::App(ns.label_string(name), Box::new(arg)), ModalMor::App(_, op) => Rendered::App(format!("{op}"), Box::new(arg)), // The functorial action of an object operation: it applies to an // `@op [..]` base, and the lifted morphisms act on the operation's @@ -408,15 +412,15 @@ fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { // adding another. ModalMor::HomApp(path, _op) => match arg { Rendered::ObApp(op_name, inner) => { - Rendered::ObApp(op_name, Box::new(apply_path(path, *inner))) + Rendered::ObApp(op_name, Box::new(apply_path(path, *inner, ns))) } // Should not arise: a hom operation applies to an object-op base. other => other, }, - ModalMor::Composite(path) => apply_path(path, arg), + ModalMor::Composite(path) => apply_path(path, arg, ns), ModalMor::List(_, mors) => match arg { Rendered::List(items) if items.len() == mors.len() => { - Rendered::List(mors.iter().zip(items).map(|(m, a)| apply_mor(m, a)).collect()) + Rendered::List(mors.iter().zip(items).map(|(m, a)| apply_mor(m, a, ns)).collect()) } // Should not arise: a list morphism always applies to a list base. other => other, @@ -426,9 +430,9 @@ fn apply_mor(mor: &ModalMor, arg: Rendered) -> Rendered { /// Applies a path of morphisms to a rendered argument, folding outermost-last /// (so `[m1, m2]` renders as `m2(m1(arg))`). -fn apply_path(path: &Path, arg: Rendered) -> Rendered { +fn apply_path(path: &Path, arg: Rendered, ns: &Namespace) -> Rendered { match path { Path::Id(_) => arg, - Path::Seq(edges) => edges.iter().fold(arg, |acc, mor| apply_mor(mor, acc)), + Path::Seq(edges) => edges.iter().fold(arg, |acc, mor| apply_mor(mor, acc, ns)), } } diff --git a/packages/catlog/src/tt/modelgen.rs b/packages/catlog/src/tt/modelgen.rs index 613f5016c..92f868941 100644 --- a/packages/catlog/src/tt/modelgen.rs +++ b/packages/catlog/src/tt/modelgen.rs @@ -475,9 +475,14 @@ impl<'a> ModelGenerator<'a> { if let NameSegment::Uuid(uuid) = name { namespace.set_label(*uuid, *label); } + // Labels of the sub-instance's own generators live in an + // inner namespace keyed by the import's name, mirroring + // the structure of the flattened qualified names. + let mut sub_ns = Namespace::new_for_uuid(); let mut sub_prefix = prefix.to_vec(); sub_prefix.push(*name); - self.extract_modal_record(instance, namespace, &sub_prefix, sub_fields)?; + self.extract_modal_record(instance, &mut sub_ns, &sub_prefix, sub_fields)?; + namespace.add_inner(*name, sub_ns); } FiberTyV_::Id(_, _, _) => {} } @@ -661,11 +666,13 @@ pub fn instance_from_def( inst: &Instance, ) -> Result<(ModelInstance, Namespace), String> { let mut generator = ModelGenerator::new(toplevel, th); - generator.generate(&inst.codomain); + // Seed the namespace with the codomain model's labels, so that names of + // codomain objects and morphisms appearing in fibers and terms resolve + // alongside the instance's own generators. + let mut namespace = generator.generate(&inst.codomain); let FiberTyV_::Record(fields) = &*inst.val else { return Err("expected an instance (a fiber record)".into()); }; - let mut namespace = Namespace::new_for_uuid(); let instance = generator.instance(fields, &mut namespace)?; Ok((instance, namespace)) } @@ -846,9 +853,14 @@ fn extract_instance_record( if let NameSegment::Uuid(uuid) = name { namespace.set_label(*uuid, *label); } + // Labels of the sub-instance's own generators live in an + // inner namespace keyed by the import's name, mirroring the + // structure of the flattened qualified names. + let mut sub_ns = Namespace::new_for_uuid(); let mut sub_prefix = prefix.to_vec(); sub_prefix.push(*name); - extract_instance_record(instance, namespace, &sub_prefix, sub_fields)?; + extract_instance_record(instance, &mut sub_ns, &sub_prefix, sub_fields)?; + namespace.add_inner(*name, sub_ns); } FiberTyV_::Id(_, _, _) => {} } diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 4e0103266..a9b80aa06 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -481,6 +481,397 @@ impl<'a> Elaborator<'a> { } } +/// Instance-notebook elaboration: cells presenting an instance of a model, +/// elaborated to a fiber record packaged as an [`Instance`] — the +/// same target as the text elaborator's `instance NAME : X := [...]` path, +/// whose `instance_body_inner` is the blueprint for everything here. The +/// fiber helpers are deliberate near-duplicates of their text-side namesakes +/// with typed errors; extracting a shared core is planned once the error +/// channels unify. +impl<'a> Elaborator<'a> { + /// Reserved name binding the codomain model in an instance notebook. + /// + /// Kept identical to the text elaborator's private `CODOMAIN_BINDER`: + /// both pipelines bind the codomain first in an empty context, so fiber + /// values from either root at the same de Bruijn level, and the shared + /// name keeps their display consistent. Cell names are UUIDs, so the + /// binder can never be shadowed. + const CODOMAIN_BINDER: &'static str = "instance self"; + + /// Introduce a fiber variable (a generator or import) into the fiber + /// scope, returning its neutral value. + fn intro_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) -> FiberTmV { + let v = FiberTmV::var(self.ctx.fiber_scope.len().into(), name, label); + self.ctx.fiber_env = self.ctx.fiber_env.snoc(v.clone()); + self.ctx.push_fiber(name, label, ty); + v + } + + /// Look up a fiber variable by name, returning its syntax, value, and + /// fiber type. + fn lookup_fiber_tm(&self, name: VarName) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { + let (i, label, ty) = self.ctx.lookup_fiber(name)?; + Some((FiberTmS::var(i, name, label), self.ctx.fiber_env.get(*i).unwrap().clone(), ty)) + } + + fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { + let tm_m = self.fresh_meta(); + let obj_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(BaseTmV::meta(obj_m))) + } + + fn fiber_syn_error(&mut self, error: InvalidDblModel) -> (FiberTmS, FiberTmV, FiberTyV) { + self.errors.push(error); + self.fiber_syn_hole() + } + + fn fiber_chk_error(&mut self, error: InvalidDblModel) -> (FiberTmS, FiberTmV) { + self.errors.push(error); + let tm_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m)) + } + + /// Whether two codomain models agree closely enough to import an + /// instance of one into an instance of the other. Duplicates the text + /// elaborator's `codomains_match`; see there for why this is stricter + /// than record convertibility. + fn codomains_match(&self, a: &BaseTyV, b: &BaseTyV) -> bool { + if let (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) = (&**a, &**b) { + let names_a: Vec<_> = r1.fields.iter().map(|(n, _)| n).collect(); + let names_b: Vec<_> = r2.fields.iter().map(|(n, _)| n).collect(); + if names_a != names_b { + return false; + } + } + self.evaluator().convertible_ty(a, b).is_ok() + } + + /// Resolve a qualified name to a codomain morphism: the path (with + /// labels, for [`FiberTmS::over_app`]) and the morphism's type. The + /// codomain's fields are in the base scope (see + /// [`Self::instance_notebook`]), so the first segment is a context + /// variable and later segments project through records (a morphism of a + /// model instantiated into the codomain). + fn resolve_codomain_mor( + &self, + name: &QualifiedName, + ) -> Option<(Vec<(FieldName, LabelSegment)>, BaseTyV)> { + let (&first, rest) = name.as_slice().split_first()?; + let (i, label, ty) = self.ctx.lookup(first)?; + let mut tm_v = self.ctx.env.get(*i).unwrap().clone(); + let mut ty_v = ty?; + let mut path = vec![(first, label)]; + for &seg in rest { + let BaseTyV_::Record(r) = &*ty_v else { + return None; + }; + let (seg_label, _) = r.fields.get_with_label(seg)?; + path.push((seg, *seg_label)); + let next_ty = self.evaluator().field_ty(&ty_v, &tm_v, seg); + tm_v = self.evaluator().proj(&tm_v, seg, *seg_label); + ty_v = next_ty; + } + Some((path, ty_v)) + } + + /// Resolve a qualified fiber reference: a generator, or a projection + /// path through imports (`hydro.n`). The fiber-scope analogue of + /// [`Self::resolve_name`]. + fn resolve_fiber(&self, segments: &[VarName]) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { + let (&first, rest) = segments.split_first()?; + let (mut tm_s, mut tm_v, mut ty_v) = self.lookup_fiber_tm(first)?; + for &seg in rest { + let FiberTyV_::Record(r) = &*ty_v else { + return None; + }; + let (label, field_ty) = r.get_with_label(seg)?; + tm_s = FiberTmS::proj(tm_s, seg, *label); + tm_v = FiberTmV::proj(tm_v, seg, *label); + ty_v = field_ty.clone(); + } + Some((tm_s, tm_v, ty_v)) + } + + /// Apply a codomain morphism to an already-elaborated fiber argument. + /// Duplicates the text elaborator's `apply_codomain_morphism` with typed + /// errors: the argument's `Over` object must equal the morphism's domain + /// object (compared as base objects, so modal domains need no special + /// handling); the result lies over the morphism's codomain object. + fn apply_codomain_morphism( + &mut self, + cell: &QualifiedName, + mor_name: &QualifiedName, + arg_s: FiberTmS, + arg_v: FiberTmV, + arg_ty: FiberTyV, + ) -> (FiberTmS, FiberTmV, FiberTyV) { + let Some((path, mor_ty)) = self.resolve_codomain_mor(mor_name) else { + return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + }; + let FiberTyV_::Over(arg_obj) = &*arg_ty else { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + }; + let arg_obj = arg_obj.clone(); + let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &*mor_ty else { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + }; + if self.evaluator().equal_tm(&arg_obj, dom_obj).is_err() { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + } + let cod_s = self.evaluator().quote_tm(cod_obj); + ( + FiberTmS::over_app(path.clone(), cod_s, arg_s), + FiberTmV::over_app(path, cod_obj.clone(), arg_v), + FiberTyV::over(cod_obj.clone()), + ) + } + + /// Synthesize a fiber term from a notebook instance term. Mirrors the + /// text elaborator's `fiber_syn`, dispatching on [`nb::InstanceTm`] + /// instead of surface notation. `cell` names the enclosing equation cell + /// for error attribution. + fn fiber_syn_nb( + &mut self, + cell: &QualifiedName, + tm: &nb::InstanceTm, + ) -> (FiberTmS, FiberTmV, FiberTyV) { + match tm { + nb::InstanceTm::Generator(name) => { + let Ok(qname) = QualifiedName::deserialize_str(name) else { + return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + }; + match self.resolve_fiber(qname.as_slice()) { + Some(r) => r, + None => self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())), + } + } + nb::InstanceTm::App { mor, arg } => { + let nb::Mor::Basic(mor_name) = mor else { + return self.fiber_syn_error(InvalidDblModel::UnsupportedFeature( + Feature::CompositeApplication, + )); + }; + let Ok(mor_qname) = QualifiedName::deserialize_str(mor_name) else { + return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + }; + let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(cell, arg); + self.apply_codomain_morphism(cell, &mor_qname, arg_s, arg_v, arg_ty) + } + nb::InstanceTm::List { terms, .. } => { + let mut ss = Vec::with_capacity(terms.len()); + let mut vs = Vec::with_capacity(terms.len()); + let mut objs = Vec::with_capacity(terms.len()); + for term in terms { + let Some(term) = term else { + return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + }; + let (s, v, ty) = self.fiber_syn_nb(cell, term); + let FiberTyV_::Over(o) = &*ty else { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + }; + objs.push(o.clone()); + ss.push(s); + vs.push(v); + } + (FiberTmS::list(ss), FiberTmV::list(vs), FiberTyV::over(BaseTmV::list(objs))) + } + nb::InstanceTm::ObApp { op, tm } => { + let nb::ObOp::Basic(op_name) = op; + let op_seg = name_seg(*op_name); + if self.theory().basic_ob_op([op_seg].into()).is_none() { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + } + let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(cell, tm); + let FiberTyV_::Over(arg_obj) = &*arg_ty else { + return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + }; + let obj = BaseTmV::app(op_seg, arg_obj.clone()); + ( + FiberTmS::ob_app(op_seg, arg_s), + FiberTmV::ob_app(op_seg, arg_v), + FiberTyV::over(obj), + ) + } + } + } + + /// Check a notebook instance term against an expected fiber type. Fiber + /// terms are all synthesizing, so this synthesizes and checks + /// convertibility. + fn fiber_chk_nb( + &mut self, + cell: &QualifiedName, + expected: &FiberTyV, + tm: &nb::InstanceTm, + ) -> (FiberTmS, FiberTmV) { + let (s, v, ty) = self.fiber_syn_nb(cell, tm); + if self.evaluator().convertible_fiber_ty(&ty, expected).is_err() { + return self.fiber_chk_error(InvalidDblModel::FiberType(cell.clone())); + } + (s, v) + } + + /// Elaborate the cells of an instance notebook against the codomain + /// model, producing the instance as a fiber record — the notebook + /// analogue of the text elaborator's `instance_body`. + /// + /// The codomain is bound under `Self::CODOMAIN_BINDER` and each of its + /// fields is pushed into the base scope as a variable projecting out of + /// that binding, so cell references to codomain objects and morphisms + /// (UUID-qualified names) resolve through the ordinary + /// `Self::resolve_name` machinery — including modal objects in `over` + /// and paths through model instantiations. Generators and imports go to + /// the separate fiber scope, exactly as in the text pipeline. Unlike the + /// text pipeline, a bad cell does not abort the instance: the error is + /// recorded against the cell and elaboration continues. + pub fn instance_notebook<'b>( + &mut self, + codomain: &RecordV, + cells: impl Iterator, + ) -> (FiberTyS, FiberTyV) { + let toplevel = self.toplevel; + // Like model notebooks, cells are elaborated in dependency order so + // that UI reordering cannot change the result. + let mut cells: Vec<_> = cells.collect(); + cells.sort_by_key(|judgment| match judgment { + nb::InstanceJudgment::Generator(_) => 0, + nb::InstanceJudgment::Import(_) => 1, + nb::InstanceJudgment::Equation(_) => 2, + }); + + let c = self.checkpoint(); + let codomain_ty = BaseTyV::record(codomain.clone()); + let self_v = self.intro( + name_seg(Self::CODOMAIN_BINDER), + label_seg(Self::CODOMAIN_BINDER), + Some(codomain_ty.clone()), + ); + for (name, (label, _)) in codomain.fields.iter() { + let field_ty = self.evaluator().field_ty(&codomain_ty, &self_v, *name); + let field_v = self.evaluator().proj(&self_v, *name, *label); + self.ctx.push_scope(*name, *label, Some(field_ty)); + self.ctx.env = self.ctx.env.snoc(field_v); + } + + let mut fields_s: Row = Row::empty(); + let mut fields_v: Row = Row::empty(); + + for cell in cells { + match cell { + // A generator lying over a codomain object. + nb::InstanceJudgment::Generator(gen_decl) => { + let name = NameSegment::Uuid(gen_decl.id); + let label = LabelSegment::Text(ustr(&gen_decl.name)); + let over = gen_decl.over.as_ref().and_then(|ob| self.ob_syn(ob)); + let (ty_s, ty_v) = match over { + Some((obj_s, obj_v, _)) => (FiberTyS::over(obj_s), FiberTyV::over(obj_v)), + None => { + self.errors.push(InvalidDblModel::ObType(QualifiedName::single(name))); + let m = self.fresh_meta(); + (FiberTyS::over(BaseTmS::meta(m)), FiberTyV::over(BaseTmV::meta(m))) + } + }; + self.intro_fiber(name, label, ty_v.clone()); + fields_s.insert(name, label, ty_s); + fields_v.insert(name, label, ty_v); + } + // An import of another instance of the same codomain. + nb::InstanceJudgment::Import(import) => { + let name = NameSegment::Uuid(import.id); + let label = LabelSegment::Text(ustr(&import.name)); + let qname = QualifiedName::single(name); + let resolved = import.instance.as_ref().and_then(|link| { + let nb::LinkType::Instantiation = link.r#type else { + return None; + }; + let topname = NameSegment::Text(ustr(&link.stable_ref.id)); + match toplevel.declarations.get(&topname) { + Some(TopDecl::Instance(inst)) if inst.theory == self.theory => { + Some((topname, inst)) + } + _ => None, + } + }); + let Some((topname, inst)) = resolved else { + self.errors.push(InvalidDblModel::InvalidLink(qname)); + continue; + }; + if !self.codomains_match(&codomain_ty, &inst.codomain) { + self.errors.push(InvalidDblModel::ImportCodomain(qname)); + continue; + } + let val = inst.val.clone(); + self.intro_fiber(name, label, val.clone()); + fields_s.insert(name, label, FiberTyS::topvar(topname)); + fields_v.insert(name, label, val); + } + // An equation between fiber elements. + nb::InstanceJudgment::Equation(eqn_decl) => { + let name = NameSegment::Uuid(eqn_decl.id); + let label = LabelSegment::Text(ustr(&eqn_decl.name)); + let qname = QualifiedName::single(name); + let (Some(lhs), Some(rhs)) = (&eqn_decl.lhs, &eqn_decl.rhs) else { + self.errors + .push(InvalidDblModel::UnsupportedFeature(Feature::PartialEquation)); + continue; + }; + let (lhs_s, lhs_v, lhs_ty) = self.fiber_syn_nb(&qname, lhs); + let FiberTyV_::Over(obj) = &*lhs_ty else { + // Only fiber-element equations live in an instance; + // morphism equations constrain the model. + self.errors.push(InvalidDblModel::FiberType(qname)); + continue; + }; + let over_s = FiberTyS::over(self.evaluator().quote_tm(obj)); + let (rhs_s, rhs_v) = self.fiber_chk_nb(&qname, &lhs_ty, rhs); + fields_s.insert(name, label, FiberTyS::id(over_s, lhs_s, rhs_s)); + fields_v.insert(name, label, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); + } + } + } + self.reset_to(c); + (FiberTyS::record(fields_s), FiberTyV::record(fields_v)) + } + + /// Elaborate an instance document into a top-level instance declaration. + /// + /// Resolves the document's `instanceOf` link to a model previously + /// declared in the toplevel (mirroring how instantiation cells resolve + /// their links), elaborates the cells against it, and packages the + /// result exactly as the text pipeline does — ready for + /// [`instance_from_def`](super::modelgen::instance_from_def). + /// + /// Returns `None` (with an error recorded) if the codomain link cannot + /// be resolved at all; cell-level problems are recorded per-cell in + /// [`Self::errors`] and still produce an instance. + pub fn instance_document(&mut self, doc: &nb::InstanceDocumentContent) -> Option { + let toplevel = self.toplevel; + let link = &doc.instance_of; + let link_name = QualifiedName::single(NameSegment::Text(ustr(&link.stable_ref.id))); + let nb::LinkType::InstanceOf = link.r#type else { + self.errors.push(InvalidDblModel::InvalidLink(link_name)); + return None; + }; + let topname = NameSegment::Text(ustr(&link.stable_ref.id)); + let Some(TopDecl::Type(type_def)) = toplevel.declarations.get(&topname) else { + self.errors.push(InvalidDblModel::InvalidLink(link_name)); + return None; + }; + if type_def.theory != self.theory { + self.errors.push(InvalidDblModel::InvalidLink(link_name)); + return None; + } + let BaseTyV_::Record(codomain) = &*type_def.val else { + self.errors.push(InvalidDblModel::InvalidLink(link_name)); + return None; + }; + let codomain = codomain.clone(); + let codomain_ty = type_def.val.clone(); + let (stx, val) = self.instance_notebook(&codomain, doc.notebook.formal_content()); + Some(Instance::new(self.theory.clone(), stx, val, codomain_ty)) + } +} + /// Promotes a modality from notebook type to modality for modal theory. pub fn promote_modality(modality: nb::Modality) -> modal::Modality { match modality { @@ -517,15 +908,17 @@ mod test { use ustr::ustr; use crate::dbl::model::DblModelPrinter; - use crate::stdlib::{th_schema, th_sym_monoidal_category}; + use crate::stdlib::{th_schema, th_sym_monoidal_category, th_sym_multicategory}; use crate::tt::{ - modelgen::Model, + batch::{format_modal_instance_term, format_modal_ob, write_instance_summary}, + modelgen::{Model, ModelInstance, instance_from_def}, notebook_elab::Elaborator, + prelude::*, theory::{Theory, TheoryDef}, - toplevel::Toplevel, + toplevel::{Instance, TopDecl, Toplevel, Type}, }; use crate::zero::name; - use catcolab_document_types::current::ModelDocumentContent; + use catcolab_document_types::current::{InstanceDocumentContent, ModelDocumentContent}; fn elab_example(theory: &Theory, name: &str, expected: Expect) -> Model { let src = fs::read_to_string(format!("examples/tt/notebook/{name}.json")).unwrap(); @@ -592,20 +985,176 @@ mod test { ); } - /// The Klausmeier instance-notebook fixtures deserialize against the - /// document schema. Elaboration of instance notebooks is not implemented - /// yet; this test pins schema/fixture agreement in the meantime. - #[test] - fn klausmeier_fixtures_deserialize() { - use catcolab_document_types::current::InstanceDocumentContent; + /// Elaborate a model document and install it in the toplevel under the + /// given ref id, so instance documents can link to it. + fn install_model(toplevel: &mut Toplevel, theory: &Theory, ref_id: &str, src: &str) { + let doc: ModelDocumentContent = serde_json::from_str(src).unwrap(); + let (ty_s, ty_v) = { + let mut elab = Elaborator::new(theory.clone(), toplevel, ustr(ref_id)); + let r = elab.notebook(doc.notebook.formal_content()); + assert!(elab.errors().is_empty(), "{ref_id}: {:?}", elab.errors()); + r + }; + toplevel.declarations.insert( + NameSegment::Text(ustr(ref_id)), + TopDecl::Type(Type::new(theory.clone(), ty_s, ty_v)), + ); + } + + /// Elaborate an instance document, asserting no errors. + fn elab_instance(toplevel: &Toplevel, theory: &Theory, ref_id: &str, src: &str) -> Instance { + let doc: InstanceDocumentContent = serde_json::from_str(src).unwrap(); + let mut elab = Elaborator::new(theory.clone(), toplevel, ustr(ref_id)); + let inst = elab.instance_document(&doc).expect("codomain should resolve"); + assert!(elab.errors().is_empty(), "{ref_id}: {:?}", elab.errors()); + inst + } + + /// The Klausmeier fixtures: DEC model + hydro/phyto instances installed + /// in a toplevel, ready for tests to elaborate against. + fn klausmeier_setup() -> (Theory, Toplevel) { + let th = + Theory::new(name("ThMulticategory"), TheoryDef::modal_unital(th_sym_multicategory())); + let mut toplevel = Toplevel::new(Default::default()); let src = fs::read_to_string("examples/tt/notebook/klausmeier/dec_model.json").unwrap(); - let _: ModelDocumentContent = serde_json::from_str(&src).unwrap(); - for name in ["hydrodynamics", "phytodynamics", "klausmeier"] { - let src = - fs::read_to_string(format!("examples/tt/notebook/klausmeier/{name}.json")).unwrap(); - let doc: InstanceDocumentContent = serde_json::from_str(&src).unwrap(); - assert!(doc.notebook.formal_content().count() > 0); + install_model(&mut toplevel, &th, "dec_model", &src); + for ref_id in ["hydrodynamics", "phytodynamics"] { + let src = fs::read_to_string(format!("examples/tt/notebook/klausmeier/{ref_id}.json")) + .unwrap(); + let inst = elab_instance(&toplevel, &th, ref_id, &src); + toplevel + .declarations + .insert(NameSegment::Text(ustr(ref_id)), TopDecl::Instance(inst)); } + (th, toplevel) + } + + /// Render an elaborated instance through `instance_from_def` in the + /// batch snapshot format. + fn instance_summary(toplevel: &Toplevel, theory: &Theory, inst: &Instance) -> String { + let (instance, ns) = instance_from_def(toplevel, &theory.definition, inst).unwrap(); + let ModelInstance::ModalUnital(instance) = &instance else { + panic!("expected a modal instance"); + }; + let mut out = String::new(); + write_instance_summary( + &mut out, + instance, + &ns, + |ob| format_modal_ob(ob, &ns), + |tm| format_modal_instance_term(tm, &ns), + ); + out + } + + /// End-to-end: the Klausmeier instance notebooks elaborate to + /// `DblModelInstance`s through the same pipeline as the text examples + /// (compare `examples/tt/text/test_klausmeier.dbltt.snapshot`). + #[test] + fn klausmeier_instance_notebooks() { + let (th, toplevel) = klausmeier_setup(); + + let Some(TopDecl::Instance(hydro)) = + toplevel.declarations.get(&NameSegment::Text(ustr("hydrodynamics"))) + else { + unreachable!() + }; + expect![[r#" + #/ instance generators: + #/ a : Form0 + #/ k : Form0 + #/ dX : Form1 + #/ w : DualForm0 + #/ n : DualForm0 + #/ x0 : DualForm0 + #/ x1 : DualForm0 + #/ x2 : DualForm0 + #/ x3 : DualForm0 + #/ x4 : DualForm0 + #/ x5 : DualForm0 + #/ instance equations: + #/ x0 == sub_d01([w, a]) + #/ x1 == square_d0([n]) + #/ x2 == mult_d0d0([w, x1]) + #/ x3 == sub_d0d0([x0, x2]) + #/ x4 == lie_1d0([dX, w]) + #/ x5 == mult_0d0([k, x4]) + #/ partial_d0([w]) == add_d0d0([x3, x5]) + "#]] + .assert_eq(&instance_summary(&toplevel, &th, hydro)); + + let src = fs::read_to_string("examples/tt/notebook/klausmeier/klausmeier.json").unwrap(); + let klausmeier = elab_instance(&toplevel, &th, "klausmeier", &src); + expect![[r#" + #/ instance generators: + #/ hydro.a : Form0 + #/ hydro.k : Form0 + #/ hydro.dX : Form1 + #/ hydro.w : DualForm0 + #/ hydro.n : DualForm0 + #/ hydro.x0 : DualForm0 + #/ hydro.x1 : DualForm0 + #/ hydro.x2 : DualForm0 + #/ hydro.x3 : DualForm0 + #/ hydro.x4 : DualForm0 + #/ hydro.x5 : DualForm0 + #/ phyto.m : Form0 + #/ phyto.n : DualForm0 + #/ phyto.w : DualForm0 + #/ phyto.y0 : DualForm0 + #/ phyto.y1 : DualForm0 + #/ phyto.y2 : DualForm0 + #/ phyto.y3 : DualForm0 + #/ phyto.y4 : DualForm0 + #/ instance equations: + #/ hydro.x0 == sub_d01([hydro.w, hydro.a]) + #/ hydro.x1 == square_d0([hydro.n]) + #/ hydro.x2 == mult_d0d0([hydro.w, hydro.x1]) + #/ hydro.x3 == sub_d0d0([hydro.x0, hydro.x2]) + #/ hydro.x4 == lie_1d0([hydro.dX, hydro.w]) + #/ hydro.x5 == mult_0d0([hydro.k, hydro.x4]) + #/ partial_d0([hydro.w]) == add_d0d0([hydro.x3, hydro.x5]) + #/ phyto.y0 == square_d0([phyto.n]) + #/ phyto.y1 == mult_d0d0([phyto.w, phyto.y0]) + #/ phyto.y2 == mult_0d0([phyto.m, phyto.n]) + #/ phyto.y3 == sub_d0d0([phyto.y1, phyto.y2]) + #/ phyto.y4 == lapl_d0([phyto.n]) + #/ partial_d0([phyto.w]) == add_d0d0([phyto.y3, phyto.y4]) + #/ hydro.n == phyto.n + #/ hydro.w == phyto.w + "#]] + .assert_eq(&instance_summary(&toplevel, &th, &klausmeier)); + } + + /// Importing an instance of a different model into an instance notebook + /// is an error (notebook twin of the text suite's MismatchedImport). + #[test] + fn instance_import_codomain_mismatch() { + use crate::dbl::model::InvalidDblModel; + let (th, mut toplevel) = klausmeier_setup(); + let other_model = r##"{"type":"model","name":"Other","theory":"multicategory","version":"2", + "notebook":{"cellContents":{"11111111-1111-1111-1111-111111111111":{ + "tag":"formal","id":"11111111-1111-1111-1111-111111111111", + "content":{"tag":"object","name":"X","id":"22222222-2222-2222-2222-222222222222", + "obType":{"tag":"Basic","content":"Object"}}}}, + "cellOrder":["11111111-1111-1111-1111-111111111111"]}}"##; + install_model(&mut toplevel, &th, "other_model", other_model); + let bad_import = r##"{"type":"instance","name":"Bad","version":"2", + "instanceOf":{"_id":"other_model","_version":null,"_server":"catcolab.org","type":"instance-of"}, + "notebook":{"cellContents":{"33333333-3333-3333-3333-333333333333":{ + "tag":"formal","id":"33333333-3333-3333-3333-333333333333", + "content":{"tag":"import","name":"h","id":"44444444-4444-4444-4444-444444444444", + "instance":{"_id":"hydrodynamics","_version":null,"_server":"catcolab.org","type":"instantiation"}}}}, + "cellOrder":["33333333-3333-3333-3333-333333333333"]}}"##; + let doc: InstanceDocumentContent = serde_json::from_str(bad_import).unwrap(); + let mut elab = Elaborator::new(th.clone(), &toplevel, ustr("bad_import")); + let inst = elab.instance_document(&doc); + assert!(inst.is_some()); + assert!( + elab.errors().iter().any(|e| matches!(e, InvalidDblModel::ImportCodomain(_))), + "expected an ImportCodomain error, got {:?}", + elab.errors() + ); } /// Test a notebook with an equation. From c57838a1a5205310e3d27b30da3dad3b8b1801e2 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Thu, 16 Jul 2026 21:23:28 -0700 Subject: [PATCH 51/53] add backup test for nb examples --- packages/catlog/src/tt/notebook_elab.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index a9b80aa06..307124a5f 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -985,6 +985,45 @@ mod test { ); } + /// Every notebook fixture under `examples/tt/notebook` deserializes + /// against the document schema for its declared `type`. Elaboration + /// coverage is per-file opt-in (each fixture needs a theory and, for + /// instances, a populated toplevel), but this sweep catches schema + /// drift and orphaned fixtures that no named test reads. + #[test] + fn notebook_fixtures_deserialize() { + fn walk(dir: &std::path::Path, checked: &mut usize) { + for entry in fs::read_dir(dir).unwrap().flatten() { + let path = entry.path(); + if path.is_dir() { + walk(&path, checked); + continue; + } + if path.extension().is_none_or(|e| e != "json") { + continue; + } + let src = fs::read_to_string(&path).unwrap(); + let value: serde_json::Value = serde_json::from_str(&src).unwrap(); + let display = path.display(); + match value.get("type").and_then(|t| t.as_str()) { + Some("model") => { + serde_json::from_str::(&src) + .unwrap_or_else(|e| panic!("{display}: {e}")); + } + Some("instance") => { + serde_json::from_str::(&src) + .unwrap_or_else(|e| panic!("{display}: {e}")); + } + other => panic!("{display}: unexpected document type {other:?}"), + } + *checked += 1; + } + } + let mut checked = 0; + walk(std::path::Path::new("examples/tt/notebook"), &mut checked); + assert!(checked >= 8, "expected at least 8 fixtures, found {checked}"); + } + /// Elaborate a model document and install it in the toplevel under the /// given ref id, so instance documents can link to it. fn install_model(toplevel: &mut Toplevel, theory: &Theory, ref_id: &str, src: &str) { From 19838f8cbb7e5e705d491e9196cbea8f5f17e7df Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 17 Jul 2026 16:50:42 -0700 Subject: [PATCH 52/53] separate out shared fiber elaboration structure --- packages/catlog/src/tt/batch.rs | 8 +- packages/catlog/src/tt/fiber_elab.rs | 289 +++++++++++++++++++++++ packages/catlog/src/tt/mod.rs | 1 + packages/catlog/src/tt/notebook_elab.rs | 214 +++++++---------- packages/catlog/src/tt/text_elab.rs | 301 +++++++++++------------- 5 files changed, 513 insertions(+), 300 deletions(-) create mode 100644 packages/catlog/src/tt/fiber_elab.rs diff --git a/packages/catlog/src/tt/batch.rs b/packages/catlog/src/tt/batch.rs index 228fc0f05..51ff0f84e 100644 --- a/packages/catlog/src/tt/batch.rs +++ b/packages/catlog/src/tt/batch.rs @@ -301,7 +301,7 @@ fn snapshot_examples() { /// Render an instance term for snapshot output as `f(g(base))`, with /// `f` the outermost (last-applied) model morphism in the path. -pub(crate) fn format_instance_term(tm: &DiscreteInstanceTerm, ns: &Namespace) -> String { +pub(in crate::tt) fn format_instance_term(tm: &DiscreteInstanceTerm, ns: &Namespace) -> String { let mut s = ns.label_string(&tm.base); if let Path::Seq(edges) = &tm.path { for mor in edges.iter() { @@ -313,7 +313,7 @@ pub(crate) fn format_instance_term(tm: &DiscreteInstanceTerm, ns: &Namespace) -> /// Writes the generators and equations of an instance, using the given /// per-doctrine formatters for fibers and equation terms. -pub(crate) fn write_instance_summary( +pub(in crate::tt) fn write_instance_summary( out: &mut String, instance: &DblModelInstance, ns: &Namespace, @@ -342,7 +342,7 @@ pub(crate) fn write_instance_summary( /// Renders a modal object for snapshot output: generators by name, object /// operations as `op(inner)`, and lists as `[a, b, …]`. -pub(crate) fn format_modal_ob(ob: &ModalOb, ns: &Namespace) -> String { +pub(in crate::tt) fn format_modal_ob(ob: &ModalOb, ns: &Namespace) -> String { match ob { ModalOb::Generator(name) => ns.label_string(name), ModalOb::App(inner, op) => format!("{op}({})", format_modal_ob(inner, ns)), @@ -379,7 +379,7 @@ impl Rendered { /// Renders a modal instance term as e.g. `op([x, unit([])])`, re-interleaving /// the morphism with its base (the inverse of the flattening done during /// extraction). -pub(crate) fn format_modal_instance_term(tm: &ModalInstanceTerm, ns: &Namespace) -> String { +pub(in crate::tt) fn format_modal_instance_term(tm: &ModalInstanceTerm, ns: &Namespace) -> String { apply_mor(&tm.mor, base_rendered(&tm.base, ns), ns).render() } diff --git a/packages/catlog/src/tt/fiber_elab.rs b/packages/catlog/src/tt/fiber_elab.rs new file mode 100644 index 000000000..7314d1bb4 --- /dev/null +++ b/packages/catlog/src/tt/fiber_elab.rs @@ -0,0 +1,289 @@ +//! The fiber-elaboration core shared by the text and notebook elaborators. +//! +//! Both elaborators build instances the same way, by introducing fiber variables for +//! generators and sub-instance imports, fiber terms for elements, and `Id` +//! fields for equations. But they report errors through different channels: the +//! [text elaborator](super::text_elab) emits located messages through a +//! `Reporter`, while the [notebook elaborator](super::notebook_elab) collects +//! typed [`InvalidDblModel`](crate::dbl::model::InvalidDblModel) values +//! attributed to cell UUIDs. [`FiberElab`] carries the shared machinery as +//! provided methods, and [`FiberError`] is the neutral currency that each +//! implementor maps into its own channel via [`FiberElab::report_fiber`]. +//! +//! Everything here is `pub(in crate::tt)`. The module boundary enforced today +//! is the crate boundary of a prospective standalone `tt` crate. + +use super::{context::*, eval::*, prelude::*, stx::*, theory::*, val::*}; + +/// Reserved name under which an instance's codomain model is bound as a +/// context variable. +/// +/// It contains a space, so the text elaborator's lexer — which restricts +/// identifiers to alphanumerics and `_` — can never produce it, and notebook +/// cell names are UUIDs; hence a user-declared generator, sub-instance, or +/// field can never shadow the codomain binding. Both elaborators bind the +/// codomain first in an empty context, so fiber values from either pipeline +/// root at the same de Bruijn level and their neutrals compare equal. +pub(in crate::tt) const CODOMAIN_BINDER: &str = "instance self"; + +/// Renders a codomain path for display in error messages. +pub(in crate::tt) fn path_str(path: &[(FieldName, LabelSegment)]) -> String { + path.iter().map(|(_, label)| label.to_string()).collect::>().join(".") +} + +/// An error in fiber elaboration. +/// +/// Unlike the validation enums in [`crate::dbl`] (`InvalidDblModel` and +/// friends), which are QualifiedName-keyed results serialized across the wasm +/// boundary, this enum is elaborator-internal plumbing: it exists only to be +/// mapped into each elaborator's reporting channel by +/// [`FiberElab::report_fiber`] and never escapes `tt` — hence no serde +/// derives. Payloads are pre-formatted strings because the text channel's +/// messages appear verbatim in committed snapshots.. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(in crate::tt) enum FiberError { + /// Reference to an unknown fiber element (generator or import). + UnknownElement(String), + /// Projection out of a fiber element that is not a sub-instance. + ProjNonRecord, + /// Projection of a generator absent from the sub-instance. + UnknownProj(String), + /// Object operation not present in the theory: (operation, theory name). + UnknownObOp(String, String), + /// Object operation applied to a non-element. + ObOpOnNonElement(String), + /// Fiber list element that is not an element over an object. + ListElementNotOver, + /// A hole in a notebook term (an unfilled editor slot). + MissingTerm, + /// Morphism-application argument that is not an element, with its + /// display name when the surface syntax provides one. + ArgNotElement(Option), + /// Codomain field used as a morphism that is not one, by path. + NotAMorphism(String), + /// Morphism applied to an argument over the wrong object. + ArgMismatch { + /// Display path of the morphism. + path: String, + /// The object the argument lies over (quoted). + got: String, + /// The morphism's domain object (quoted). + expected: String, + /// Pretty-printed equality failure. + detail: String, + }, + /// Fiber term whose type is inconvertible with the expected type. + WrongFiberType(String), + /// LHS of a `mor(arg) := target` clause that is not an element. + MappingLhsNotOver, + /// Equation between things that are not fiber elements. + EquationNotOver, + /// Equation sides with inconvertible fiber types. + InconvertibleEquationSides(String), + /// Import of an instance of a different model, by display name. + ImportCodomainMismatch(String), +} + +/// The fiber-elaboration core. +/// +/// Implementors supply context and theory access, meta generation, and an +/// error channel; the provided methods are the machinery both elaborators +/// share. Surface-syntax dispatch (f-notation, notebook instance terms, +/// diagram object references) stays with the implementors, whose arms +/// delegate here. +pub(in crate::tt) trait FiberElab { + /// The elaboration context. + fn ctx(&self) -> &Context; + + /// The elaboration context, mutably. + fn ctx_mut(&mut self) -> &mut Context; + + /// The theory elaboration happens in. + fn elab_theory(&self) -> &Theory; + + /// An evaluator for the current context. + fn evaluator(&self) -> Evaluator<'_>; + + /// A fresh metavariable. + fn fresh_meta(&mut self) -> MetaVar; + + /// Report a fiber-elaboration error through this elaborator's channel. + fn report_fiber(&mut self, err: FiberError); + + /// The definition of the ambient theory. + fn theory_def(&self) -> &TheoryDef { + &self.elab_theory().definition + } + + /// Introduce a fiber variable (a generator or sub-instance import) into + /// the fiber scope, returning its neutral value. + fn intro_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) -> FiberTmV { + let ctx = self.ctx_mut(); + let v = FiberTmV::var(ctx.fiber_scope.len().into(), name, label); + ctx.fiber_env = ctx.fiber_env.snoc(v.clone()); + ctx.push_fiber(name, label, ty); + v + } + + /// Look up a fiber variable by name, returning its syntax, value, and + /// fiber type. + fn lookup_fiber_tm(&self, name: VarName) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { + let (i, label, ty) = self.ctx().lookup_fiber(name)?; + Some((FiberTmS::var(i, name, label), self.ctx().fiber_env.get(*i).unwrap().clone(), ty)) + } + + /// A fiber term standing in for a failed synthesis. + fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { + let tm_m = self.fresh_meta(); + let obj_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(BaseTmV::meta(obj_m))) + } + + /// Report an error and return a synthesis hole. + fn fiber_syn_error(&mut self, err: FiberError) -> (FiberTmS, FiberTmV, FiberTyV) { + self.report_fiber(err); + self.fiber_syn_hole() + } + + /// Report an error and return a checking hole. + fn fiber_chk_error(&mut self, err: FiberError) -> (FiberTmS, FiberTmV) { + self.report_fiber(err); + let tm_m = self.fresh_meta(); + (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m)) + } + + /// Whether two codomain models agree closely enough to import an + /// instance of one into an instance of the other: identical top-level + /// field names (in order) and convertible field types. + /// + /// This is deliberately stricter than [`Evaluator::convertible_ty`], + /// which for records is positional and ignores field names and arity + /// — so it would wrongly accept e.g. `[V : Entity, E : Entity]` as + /// convertible with `[W : Entity, F : Entity]`. + fn codomains_match(&self, a: &BaseTyV, b: &BaseTyV) -> bool { + if let (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) = (&**a, &**b) { + let names_a: Vec<_> = r1.fields.iter().map(|(n, _)| n).collect(); + let names_b: Vec<_> = r2.fields.iter().map(|(n, _)| n).collect(); + if names_a != names_b { + return false; + } + } + self.evaluator().convertible_ty(a, b).is_ok() + } + + /// Project a generator out of a sub-instance import, e.g. `we.e`. The + /// field's label comes from the import's record row. + fn fiber_proj( + &mut self, + recv_s: FiberTmS, + recv_v: FiberTmV, + recv_ty: &FiberTyV, + field: FieldName, + ) -> (FiberTmS, FiberTmV, FiberTyV) { + let FiberTyV_::Record(r) = &**recv_ty else { + return self.fiber_syn_error(FiberError::ProjNonRecord); + }; + let Some((label, field_ty)) = r.get_with_label(field) else { + return self.fiber_syn_error(FiberError::UnknownProj(field.to_string())); + }; + let (label, field_ty) = (*label, field_ty.clone()); + ( + FiberTmS::proj(recv_s, field, label), + FiberTmV::proj(recv_v, field, label), + field_ty, + ) + } + + /// Whether the theory has the given object operation, reporting an error + /// if not. Checked by dispatchers *before* synthesizing the argument, to + /// preserve error order. + fn check_ob_op(&mut self, op: VarName) -> bool { + if self.theory_def().basic_ob_op([op].into()).is_none() { + let th = self.elab_theory().name.to_string(); + self.report_fiber(FiberError::UnknownObOp(op.to_string(), th)); + return false; + } + true + } + + /// Apply a theory object-operation to an already-synthesized fiber + /// element, e.g. `@tensor [a, b]`. The resulting element lies over the + /// operation applied to the argument's base object. + fn fiber_ob_app( + &mut self, + op: VarName, + arg_s: FiberTmS, + arg_v: FiberTmV, + arg_ty: &FiberTyV, + ) -> (FiberTmS, FiberTmV, FiberTyV) { + let FiberTyV_::Over(arg_obj) = &**arg_ty else { + return self.fiber_syn_error(FiberError::ObOpOnNonElement(op.to_string())); + }; + let obj = BaseTmV::app(op, arg_obj.clone()); + (FiberTmS::ob_app(op, arg_s), FiberTmV::ob_app(op, arg_v), FiberTyV::over(obj)) + } + + /// Apply an already-resolved codomain morphism to a fiber argument + /// already known to lie over `arg_obj`. The argument's object must equal + /// the morphism's domain object (compared as base objects, so modal + /// domains — lists, tensors — need no special handling); the result lies + /// over the morphism's codomain object. Resolution of the morphism (and + /// the check that the argument is an element at all) stays with the + /// caller, which knows its surface syntax and error order. + fn fiber_mor_app( + &mut self, + path: &[(FieldName, LabelSegment)], + mor_ty: &BaseTyV, + arg_s: FiberTmS, + arg_v: FiberTmV, + arg_obj: &BaseTmV, + ) -> (FiberTmS, FiberTmV, FiberTyV) { + let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &**mor_ty else { + return self.fiber_syn_error(FiberError::NotAMorphism(path_str(path))); + }; + if let Err(e) = self.evaluator().equal_tm(arg_obj, dom_obj) { + let ev = self.evaluator(); + let err = FiberError::ArgMismatch { + path: path_str(path), + got: ev.quote_tm(arg_obj).to_string(), + expected: ev.quote_tm(dom_obj).to_string(), + detail: e.pretty().to_string(), + }; + return self.fiber_syn_error(err); + } + let cod_s = self.evaluator().quote_tm(cod_obj); + ( + FiberTmS::over_app(path.to_vec(), cod_s, arg_s), + FiberTmV::over_app(path.to_vec(), cod_obj.clone(), arg_v), + FiberTyV::over(cod_obj.clone()), + ) + } + + /// Check a synthesized fiber term against an expected fiber type. + fn check_fiber( + &mut self, + syn: (FiberTmS, FiberTmV, FiberTyV), + expected: &FiberTyV, + ) -> (FiberTmS, FiberTmV) { + let (s, v, ty) = syn; + if let Err(e) = self.evaluator().convertible_fiber_ty(&ty, expected) { + return self.fiber_chk_error(FiberError::WrongFiberType(e.pretty().to_string())); + } + (s, v) + } + + /// Assemble an equation ([`Id`](FiberTyS_::Id)) field from an + /// already-checked pair of sides, the LHS lying over `over_obj`. + fn fiber_id_field( + &self, + lhs_ty: &FiberTyV, + over_obj: &BaseTmV, + lhs_s: FiberTmS, + lhs_v: FiberTmV, + rhs_s: FiberTmS, + rhs_v: FiberTmV, + ) -> (FiberTyS, FiberTyV) { + let over_s = FiberTyS::over(self.evaluator().quote_tm(over_obj)); + (FiberTyS::id(over_s, lhs_s, rhs_s), FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)) + } +} diff --git a/packages/catlog/src/tt/mod.rs b/packages/catlog/src/tt/mod.rs index 8983b48f1..866d0d786 100644 --- a/packages/catlog/src/tt/mod.rs +++ b/packages/catlog/src/tt/mod.rs @@ -195,6 +195,7 @@ pub mod batch; pub mod context; pub mod eval; +pub(in crate::tt) mod fiber_elab; pub mod modelgen; pub mod notebook_elab; pub mod prelude; diff --git a/packages/catlog/src/tt/notebook_elab.rs b/packages/catlog/src/tt/notebook_elab.rs index 307124a5f..d65d09974 100644 --- a/packages/catlog/src/tt/notebook_elab.rs +++ b/packages/catlog/src/tt/notebook_elab.rs @@ -10,6 +10,7 @@ use nonempty::NonEmpty; use std::str::FromStr; use uuid::Uuid; +use super::fiber_elab::{CODOMAIN_BINDER, FiberElab, FiberError}; use super::{context::*, eval::*, prelude::*, stx::*, theory::*, toplevel::*, val::*}; use crate::dbl::{ modal, @@ -27,6 +28,8 @@ pub struct Elaborator<'a> { errors: Vec, ref_id: Ustr, next_meta: usize, + /// The cell currently being elaborated, for error attribution. + current_cell: Option, } struct ElaboratorCheckpoint { @@ -43,6 +46,7 @@ impl<'a> Elaborator<'a> { errors: Vec::new(), ref_id, next_meta: 0, + current_cell: None, } } @@ -489,63 +493,6 @@ impl<'a> Elaborator<'a> { /// with typed errors; extracting a shared core is planned once the error /// channels unify. impl<'a> Elaborator<'a> { - /// Reserved name binding the codomain model in an instance notebook. - /// - /// Kept identical to the text elaborator's private `CODOMAIN_BINDER`: - /// both pipelines bind the codomain first in an empty context, so fiber - /// values from either root at the same de Bruijn level, and the shared - /// name keeps their display consistent. Cell names are UUIDs, so the - /// binder can never be shadowed. - const CODOMAIN_BINDER: &'static str = "instance self"; - - /// Introduce a fiber variable (a generator or import) into the fiber - /// scope, returning its neutral value. - fn intro_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) -> FiberTmV { - let v = FiberTmV::var(self.ctx.fiber_scope.len().into(), name, label); - self.ctx.fiber_env = self.ctx.fiber_env.snoc(v.clone()); - self.ctx.push_fiber(name, label, ty); - v - } - - /// Look up a fiber variable by name, returning its syntax, value, and - /// fiber type. - fn lookup_fiber_tm(&self, name: VarName) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { - let (i, label, ty) = self.ctx.lookup_fiber(name)?; - Some((FiberTmS::var(i, name, label), self.ctx.fiber_env.get(*i).unwrap().clone(), ty)) - } - - fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { - let tm_m = self.fresh_meta(); - let obj_m = self.fresh_meta(); - (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(BaseTmV::meta(obj_m))) - } - - fn fiber_syn_error(&mut self, error: InvalidDblModel) -> (FiberTmS, FiberTmV, FiberTyV) { - self.errors.push(error); - self.fiber_syn_hole() - } - - fn fiber_chk_error(&mut self, error: InvalidDblModel) -> (FiberTmS, FiberTmV) { - self.errors.push(error); - let tm_m = self.fresh_meta(); - (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m)) - } - - /// Whether two codomain models agree closely enough to import an - /// instance of one into an instance of the other. Duplicates the text - /// elaborator's `codomains_match`; see there for why this is stricter - /// than record convertibility. - fn codomains_match(&self, a: &BaseTyV, b: &BaseTyV) -> bool { - if let (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) = (&**a, &**b) { - let names_a: Vec<_> = r1.fields.iter().map(|(n, _)| n).collect(); - let names_b: Vec<_> = r2.fields.iter().map(|(n, _)| n).collect(); - if names_a != names_b { - return false; - } - } - self.evaluator().convertible_ty(a, b).is_ok() - } - /// Resolve a qualified name to a codomain morphism: the path (with /// labels, for [`FiberTmS::over_app`]) and the morphism's type. The /// codomain's fields are in the base scope (see @@ -592,70 +539,53 @@ impl<'a> Elaborator<'a> { Some((tm_s, tm_v, ty_v)) } - /// Apply a codomain morphism to an already-elaborated fiber argument. - /// Duplicates the text elaborator's `apply_codomain_morphism` with typed - /// errors: the argument's `Over` object must equal the morphism's domain - /// object (compared as base objects, so modal domains need no special - /// handling); the result lies over the morphism's codomain object. + /// Apply a codomain morphism to an already-elaborated fiber argument: + /// resolve the morphism against the codomain fields in scope, then + /// delegate the checks and construction to the shared + /// [`FiberElab::fiber_mor_app`]. fn apply_codomain_morphism( &mut self, - cell: &QualifiedName, mor_name: &QualifiedName, arg_s: FiberTmS, arg_v: FiberTmV, arg_ty: FiberTyV, ) -> (FiberTmS, FiberTmV, FiberTyV) { let Some((path, mor_ty)) = self.resolve_codomain_mor(mor_name) else { - return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + return self.fiber_syn_error(FiberError::UnknownElement(mor_name.to_string())); }; let FiberTyV_::Over(arg_obj) = &*arg_ty else { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + return self.fiber_syn_error(FiberError::ArgNotElement(None)); }; let arg_obj = arg_obj.clone(); - let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &*mor_ty else { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); - }; - if self.evaluator().equal_tm(&arg_obj, dom_obj).is_err() { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); - } - let cod_s = self.evaluator().quote_tm(cod_obj); - ( - FiberTmS::over_app(path.clone(), cod_s, arg_s), - FiberTmV::over_app(path, cod_obj.clone(), arg_v), - FiberTyV::over(cod_obj.clone()), - ) + self.fiber_mor_app(&path, &mor_ty, arg_s, arg_v, &arg_obj) } /// Synthesize a fiber term from a notebook instance term. Mirrors the /// text elaborator's `fiber_syn`, dispatching on [`nb::InstanceTm`] - /// instead of surface notation. `cell` names the enclosing equation cell - /// for error attribution. - fn fiber_syn_nb( - &mut self, - cell: &QualifiedName, - tm: &nb::InstanceTm, - ) -> (FiberTmS, FiberTmV, FiberTyV) { + /// instead of surface notation; errors are attributed to the cell in + /// [`Self::current_cell`]. + fn fiber_syn_nb(&mut self, tm: &nb::InstanceTm) -> (FiberTmS, FiberTmV, FiberTyV) { match tm { nb::InstanceTm::Generator(name) => { let Ok(qname) = QualifiedName::deserialize_str(name) else { - return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + return self.fiber_syn_error(FiberError::UnknownElement(name.clone())); }; match self.resolve_fiber(qname.as_slice()) { Some(r) => r, - None => self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())), + None => self.fiber_syn_error(FiberError::UnknownElement(name.clone())), } } nb::InstanceTm::App { mor, arg } => { let nb::Mor::Basic(mor_name) = mor else { - return self.fiber_syn_error(InvalidDblModel::UnsupportedFeature( - Feature::CompositeApplication, - )); + self.errors + .push(InvalidDblModel::UnsupportedFeature(Feature::CompositeApplication)); + return self.fiber_syn_hole(); }; let Ok(mor_qname) = QualifiedName::deserialize_str(mor_name) else { - return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + return self.fiber_syn_error(FiberError::UnknownElement(mor_name.clone())); }; - let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(cell, arg); - self.apply_codomain_morphism(cell, &mor_qname, arg_s, arg_v, arg_ty) + let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(arg); + self.apply_codomain_morphism(&mor_qname, arg_s, arg_v, arg_ty) } nb::InstanceTm::List { terms, .. } => { let mut ss = Vec::with_capacity(terms.len()); @@ -663,11 +593,11 @@ impl<'a> Elaborator<'a> { let mut objs = Vec::with_capacity(terms.len()); for term in terms { let Some(term) = term else { - return self.fiber_syn_error(InvalidDblModel::FiberElement(cell.clone())); + return self.fiber_syn_error(FiberError::MissingTerm); }; - let (s, v, ty) = self.fiber_syn_nb(cell, term); + let (s, v, ty) = self.fiber_syn_nb(term); let FiberTyV_::Over(o) = &*ty else { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + return self.fiber_syn_error(FiberError::ListElementNotOver); }; objs.push(o.clone()); ss.push(s); @@ -678,19 +608,11 @@ impl<'a> Elaborator<'a> { nb::InstanceTm::ObApp { op, tm } => { let nb::ObOp::Basic(op_name) = op; let op_seg = name_seg(*op_name); - if self.theory().basic_ob_op([op_seg].into()).is_none() { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); + if !self.check_ob_op(op_seg) { + return self.fiber_syn_hole(); } - let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(cell, tm); - let FiberTyV_::Over(arg_obj) = &*arg_ty else { - return self.fiber_syn_error(InvalidDblModel::FiberType(cell.clone())); - }; - let obj = BaseTmV::app(op_seg, arg_obj.clone()); - ( - FiberTmS::ob_app(op_seg, arg_s), - FiberTmV::ob_app(op_seg, arg_v), - FiberTyV::over(obj), - ) + let (arg_s, arg_v, arg_ty) = self.fiber_syn_nb(tm); + self.fiber_ob_app(op_seg, arg_s, arg_v, &arg_ty) } } } @@ -698,24 +620,16 @@ impl<'a> Elaborator<'a> { /// Check a notebook instance term against an expected fiber type. Fiber /// terms are all synthesizing, so this synthesizes and checks /// convertibility. - fn fiber_chk_nb( - &mut self, - cell: &QualifiedName, - expected: &FiberTyV, - tm: &nb::InstanceTm, - ) -> (FiberTmS, FiberTmV) { - let (s, v, ty) = self.fiber_syn_nb(cell, tm); - if self.evaluator().convertible_fiber_ty(&ty, expected).is_err() { - return self.fiber_chk_error(InvalidDblModel::FiberType(cell.clone())); - } - (s, v) + fn fiber_chk_nb(&mut self, expected: &FiberTyV, tm: &nb::InstanceTm) -> (FiberTmS, FiberTmV) { + let syn = self.fiber_syn_nb(tm); + self.check_fiber(syn, expected) } /// Elaborate the cells of an instance notebook against the codomain /// model, producing the instance as a fiber record — the notebook /// analogue of the text elaborator's `instance_body`. /// - /// The codomain is bound under `Self::CODOMAIN_BINDER` and each of its + /// The codomain is bound under `CODOMAIN_BINDER` and each of its /// fields is pushed into the base scope as a variable projecting out of /// that binding, so cell references to codomain objects and morphisms /// (UUID-qualified names) resolve through the ordinary @@ -742,8 +656,8 @@ impl<'a> Elaborator<'a> { let c = self.checkpoint(); let codomain_ty = BaseTyV::record(codomain.clone()); let self_v = self.intro( - name_seg(Self::CODOMAIN_BINDER), - label_seg(Self::CODOMAIN_BINDER), + name_seg(CODOMAIN_BINDER), + label_seg(CODOMAIN_BINDER), Some(codomain_ty.clone()), ); for (name, (label, _)) in codomain.fields.iter() { @@ -762,6 +676,7 @@ impl<'a> Elaborator<'a> { nb::InstanceJudgment::Generator(gen_decl) => { let name = NameSegment::Uuid(gen_decl.id); let label = LabelSegment::Text(ustr(&gen_decl.name)); + self.current_cell = Some(QualifiedName::single(name)); let over = gen_decl.over.as_ref().and_then(|ob| self.ob_syn(ob)); let (ty_s, ty_v) = match over { Some((obj_s, obj_v, _)) => (FiberTyS::over(obj_s), FiberTyV::over(obj_v)), @@ -780,6 +695,7 @@ impl<'a> Elaborator<'a> { let name = NameSegment::Uuid(import.id); let label = LabelSegment::Text(ustr(&import.name)); let qname = QualifiedName::single(name); + self.current_cell = Some(qname.clone()); let resolved = import.instance.as_ref().and_then(|link| { let nb::LinkType::Instantiation = link.r#type else { return None; @@ -809,23 +725,25 @@ impl<'a> Elaborator<'a> { nb::InstanceJudgment::Equation(eqn_decl) => { let name = NameSegment::Uuid(eqn_decl.id); let label = LabelSegment::Text(ustr(&eqn_decl.name)); - let qname = QualifiedName::single(name); + self.current_cell = Some(QualifiedName::single(name)); let (Some(lhs), Some(rhs)) = (&eqn_decl.lhs, &eqn_decl.rhs) else { self.errors .push(InvalidDblModel::UnsupportedFeature(Feature::PartialEquation)); continue; }; - let (lhs_s, lhs_v, lhs_ty) = self.fiber_syn_nb(&qname, lhs); + let (lhs_s, lhs_v, lhs_ty) = self.fiber_syn_nb(lhs); let FiberTyV_::Over(obj) = &*lhs_ty else { // Only fiber-element equations live in an instance; // morphism equations constrain the model. - self.errors.push(InvalidDblModel::FiberType(qname)); + self.report_fiber(FiberError::EquationNotOver); continue; }; - let over_s = FiberTyS::over(self.evaluator().quote_tm(obj)); - let (rhs_s, rhs_v) = self.fiber_chk_nb(&qname, &lhs_ty, rhs); - fields_s.insert(name, label, FiberTyS::id(over_s, lhs_s, rhs_s)); - fields_v.insert(name, label, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); + let obj = obj.clone(); + let (rhs_s, rhs_v) = self.fiber_chk_nb(&lhs_ty, rhs); + let (id_s, id_v) = + self.fiber_id_field(&lhs_ty, &obj, lhs_s, lhs_v, rhs_s, rhs_v); + fields_s.insert(name, label, id_s); + fields_v.insert(name, label, id_v); } } } @@ -872,6 +790,46 @@ impl<'a> Elaborator<'a> { } } +impl<'a> FiberElab for Elaborator<'a> { + fn ctx(&self) -> &Context { + &self.ctx + } + + fn ctx_mut(&mut self) -> &mut Context { + &mut self.ctx + } + + fn elab_theory(&self) -> &Theory { + &self.theory + } + + fn evaluator(&self) -> Evaluator<'_> { + Elaborator::evaluator(self) + } + + fn fresh_meta(&mut self) -> MetaVar { + Elaborator::fresh_meta(self) + } + + /// Attribute fiber errors to the cell currently being elaborated, as + /// typed [`InvalidDblModel`] values for the notebook interface. + fn report_fiber(&mut self, err: FiberError) { + let cell = self + .current_cell + .clone() + .unwrap_or_else(|| QualifiedName::single(name_seg("unknown cell"))); + let error = match err { + FiberError::UnknownElement(_) + | FiberError::ProjNonRecord + | FiberError::UnknownProj(_) + | FiberError::MissingTerm => InvalidDblModel::FiberElement(cell), + FiberError::ImportCodomainMismatch(_) => InvalidDblModel::ImportCodomain(cell), + _ => InvalidDblModel::FiberType(cell), + }; + self.errors.push(error); + } +} + /// Promotes a modality from notebook type to modality for modal theory. pub fn promote_modality(modality: nb::Modality) -> modal::Modality { match modality { diff --git a/packages/catlog/src/tt/text_elab.rs b/packages/catlog/src/tt/text_elab.rs index b4bac8af7..a012f2fd1 100644 --- a/packages/catlog/src/tt/text_elab.rs +++ b/packages/catlog/src/tt/text_elab.rs @@ -6,6 +6,7 @@ use scopeguard::{ScopeGuard, guard}; use fnotation::{ParseConfig, parser::Prec}; use tattle::declare_error; +use super::fiber_elab::{CODOMAIN_BINDER, FiberElab, FiberError, path_str}; use super::{ context::*, eval::*, modelgen::*, prelude::*, stx::*, theory::*, toplevel::*, val::*, wd::*, }; @@ -342,23 +343,15 @@ impl<'a> Elaborator<'a> { } } - /// Reserved name under which an instance's codomain model is bound - /// as a context variable (see [`Self::instance_body`]). It contains - /// a space, so the lexer — which restricts identifiers to - /// alphanumerics and `_` — can never produce it; hence a - /// user-declared generator, sub-instance, or field can never shadow - /// the codomain binding. - const CODOMAIN_BINDER: &'static str = "instance self"; - /// The codomain model of the instance body currently being /// elaborated, if any. Its fields are the codomain's generators, /// looked up by name by the instance-clause arms. /// /// The model is held as a record variable in the context under the - /// reserved [`Self::CODOMAIN_BINDER`] name (see + /// reserved [`CODOMAIN_BINDER`] name (see /// [`Self::instance_body`]). fn instance_codomain(&self) -> Option> { - let (_, _, ty) = self.ctx.lookup(name_seg(Self::CODOMAIN_BINDER))?; + let (_, _, ty) = self.ctx.lookup(name_seg(CODOMAIN_BINDER))?; match &*ty? { BaseTyV_::Record(r) => Some(Rc::new(r.clone())), _ => None, @@ -450,39 +443,14 @@ impl<'a> Elaborator<'a> { v } - /// Introduce a fiber variable (a generator or sub-instance import) - /// into the fiber scope, returning its neutral value. - fn intro_fiber(&mut self, name: VarName, label: LabelSegment, ty: FiberTyV) -> FiberTmV { - let v = FiberTmV::var(self.ctx.fiber_scope.len().into(), name, label); - self.ctx.fiber_env = self.ctx.fiber_env.snoc(v.clone()); - self.ctx.push_fiber(name, label, ty); - v - } - - /// Look up a fiber variable by name, returning its syntax, value, and - /// fiber type. - fn lookup_fiber_tm(&self, name: VarName) -> Option<(FiberTmS, FiberTmV, FiberTyV)> { - let (i, label, ty) = self.ctx.lookup_fiber(name)?; - Some((FiberTmS::var(i, name, label), self.ctx.fiber_env.get(*i).unwrap().clone(), ty)) - } - - fn fiber_syn_hole(&mut self) -> (FiberTmS, FiberTmV, FiberTyV) { - let tm_m = self.fresh_meta(); - let obj_m = self.fresh_meta(); - (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m), FiberTyV::over(BaseTmV::meta(obj_m))) - } - - fn fiber_syn_error(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV, FiberTyV) { + /// Report a text-surface error and return a synthesis hole. Errors + /// from the shared fiber machinery go through + /// [`FiberElab::report_fiber`] instead. + fn fiber_syn_error_msg(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV, FiberTyV) { self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); self.fiber_syn_hole() } - fn fiber_chk_error(&mut self, msg: impl Into) -> (FiberTmS, FiberTmV) { - self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg.into()); - let tm_m = self.fresh_meta(); - (FiberTmS::meta(tm_m), FiberTmV::meta(tm_m)) - } - /// Synthesize a fiber term and its fiber type. A fiber term is a /// generator/import variable, a projection out of a sub-instance /// (`we.e`), or a codomain-morphism application (`src(we.e)`). @@ -491,53 +459,30 @@ impl<'a> Elaborator<'a> { match n.ast0() { Var(name) => match elab.lookup_fiber_tm(name_seg(*name)) { Some(r) => r, - None => elab.fiber_syn_error(format!("no such fiber element {name}")), + None => elab.fiber_syn_error(FiberError::UnknownElement(name.to_string())), }, // Projection of a generator out of a sub-instance import: `we.e`. App1(recv_n, L(_, Field(f))) => { let (recv_s, recv_v, recv_ty) = elab.fiber_syn(recv_n); - let FiberTyV_::Record(r) = &*recv_ty else { - return elab - .fiber_syn_error("can only project a generator out of a sub-instance"); - }; - let fname = name_seg(*f); - let flabel = label_seg(*f); - let Some(field_ty) = r.get(fname).cloned() else { - return elab - .fiber_syn_error(format!("no such generator {fname} in sub-instance")); - }; - ( - FiberTmS::proj(recv_s, fname, flabel), - FiberTmV::proj(recv_v, fname, flabel), - field_ty, - ) + elab.fiber_proj(recv_s, recv_v, &recv_ty, name_seg(*f)) } // A theory object-operation on a fiber element, e.g. // `@tensor [a, b]`. The resulting element lies over the // operation applied to the argument's base object. App1(L(_, Prim(op)), arg_n) => { let op_name = name_seg(*op); - if elab.theory().basic_ob_op([op_name].into()).is_none() { - let th = elab.theory.name.to_string(); - return elab.fiber_syn_error(format!("operation @{op} not in theory {th}")); + if !elab.check_ob_op(op_name) { + return elab.fiber_syn_hole(); } let (arg_s, arg_v, arg_ty) = elab.fiber_syn(arg_n); - let FiberTyV_::Over(arg_obj) = &*arg_ty else { - return elab.fiber_syn_error(format!("@{op} applied to a non-fiber-element")); - }; - let obj = BaseTmV::app(op_name, arg_obj.clone()); - ( - FiberTmS::ob_app(op_name, arg_s), - FiberTmV::ob_app(op_name, arg_v), - FiberTyV::over(obj), - ) + elab.fiber_ob_app(op_name, arg_s, arg_v, &arg_ty) } // Codomain-morphism application `f(arg)`. The morphism `f` may // be a nested path into the codomain (e.g. `Add.op`), so its // head is a projection chain, not just a bare variable. App1(head_n, arg_n) => { let Some(path) = morphism_path(head_n) else { - return elab.fiber_syn_error( + return elab.fiber_syn_error_msg( "expected a codomain morphism (a name or path like `Add.op`) applied \ to a fiber element", ); @@ -561,9 +506,7 @@ impl<'a> Elaborator<'a> { for e in elems.iter() { let (s, v, ty) = elab.fiber_syn(e); let FiberTyV_::Over(o) = &*ty else { - return elab.fiber_syn_error( - "fiber list elements must be elements over an object", - ); + return elab.fiber_syn_error(FiberError::ListElementNotOver); }; objs.push(o.clone()); ss.push(s); @@ -571,7 +514,7 @@ impl<'a> Elaborator<'a> { } (FiberTmS::list(ss), FiberTmV::list(vs), FiberTyV::over(BaseTmV::list(objs))) } - _ => elab.fiber_syn_error( + _ => elab.fiber_syn_error_msg( "expected a fiber element: a generator, a projection `we.e`, a fiber list \ `[..]`, an object operation `@op [..]`, or a morphism application `f[..]`", ), @@ -581,31 +524,8 @@ impl<'a> Elaborator<'a> { /// Check a fiber term against an expected fiber type. Fiber terms are /// all synthesizing, so this synthesizes and checks convertibility. fn fiber_chk(&mut self, expected: &FiberTyV, n: &FNtn) -> (FiberTmS, FiberTmV) { - let (s, v, ty) = self.fiber_syn(n); - if let Err(e) = self.evaluator().convertible_fiber_ty(&ty, expected) { - return self - .fiber_chk_error(format!("fiber element has the wrong type:\n{}", e.pretty())); - } - (s, v) - } - - /// Whether two codomain models agree closely enough to import an - /// instance of one into an instance of the other: identical top-level - /// field names (in order) and convertible field types. - /// - /// This is deliberately stricter than [`Evaluator::convertible_ty`], - /// which for records is positional and ignores field names and arity - /// — so it would wrongly accept e.g. `[V : Entity, E : Entity]` as - /// convertible with `[W : Entity, F : Entity]`. - fn codomains_match(&self, a: &BaseTyV, b: &BaseTyV) -> bool { - if let (BaseTyV_::Record(r1), BaseTyV_::Record(r2)) = (&**a, &**b) { - let names_a: Vec<_> = r1.fields.iter().map(|(n, _)| n).collect(); - let names_b: Vec<_> = r2.fields.iter().map(|(n, _)| n).collect(); - if names_a != names_b { - return false; - } - } - self.evaluator().convertible_ty(a, b).is_ok() + let syn = self.fiber_syn(n); + self.check_fiber(syn, expected) } /// Elaborate a fiber-type annotation. Used for sub-instance imports @@ -630,10 +550,8 @@ impl<'a> Elaborator<'a> { if let Some(cod) = self.instance_codomain() { let enclosing = BaseTyV::record((*cod).clone()); if !self.codomains_match(&enclosing, &imported_codomain) { - return self.error(format!( - "cannot import {name}: it is an instance of a different model than \ - the enclosing instance" - )); + self.report_fiber(FiberError::ImportCodomainMismatch(name.to_string())); + return None; } } // The syntax keeps the instance's name (for display); the @@ -646,20 +564,16 @@ impl<'a> Elaborator<'a> { let (a_s, a_v, a_ty) = self.fiber_syn(a_n); let (b_s, b_v, b_ty) = self.fiber_syn(b_n); if let Err(e) = self.evaluator().convertible_fiber_ty(&a_ty, &b_ty) { - return self.error(format!( - "equation sides have inconvertible fiber types:\n{}", - e.pretty() + self.report_fiber(FiberError::InconvertibleEquationSides( + e.pretty().to_string(), )); + return None; } let FiberTyV_::Over(obj) = &*a_ty else { - return self.error( - "instance equations must be between elements over an object \ - (fiber elements); morphism equations constrain the model, not \ - an instance", - ); + self.report_fiber(FiberError::EquationNotOver); + return None; }; - let over_s = FiberTyS::over(self.evaluator().quote_tm(obj)); - Some((FiberTyS::id(over_s, a_s, b_s), FiberTyV::id(a_ty.clone(), a_v, b_v))) + Some(self.fiber_id_field(&a_ty, obj, a_s, a_v, b_s, b_v)) } _ => self.error("expected an instance name or an equation `a == b`"), } @@ -678,7 +592,7 @@ impl<'a> Elaborator<'a> { /// every codomain object is rooted at the same `self` neutral and thus /// compares equal under [`Evaluator::equal_tm`]. fn codomain_self_value(&self) -> Option { - let (i, _, _) = self.ctx.lookup(name_seg(Self::CODOMAIN_BINDER))?; + let (i, _, _) = self.ctx.lookup(name_seg(CODOMAIN_BINDER))?; self.ctx.env.get(*i).cloned() } @@ -702,18 +616,17 @@ impl<'a> Elaborator<'a> { arg_label_str: &str, ) -> (FiberTmS, FiberTmV, FiberTyV) { let Some(codomain) = self.instance_codomain() else { - return self.fiber_syn_error( + return self.fiber_syn_error_msg( "applied codomain morphism is only allowed inside an instance body", ); }; let FiberTyV_::Over(arg_obj) = &*arg_ty else { - return self.fiber_syn_error(format!( - "argument {arg_label_str} is not an element over an object", - )); + return self + .fiber_syn_error(FiberError::ArgNotElement(Some(arg_label_str.to_string()))); }; let arg_obj = arg_obj.clone(); let Some(self_val) = self.codomain_self_value() else { - return self.fiber_syn_error( + return self.fiber_syn_error_msg( "applied codomain morphism is only allowed inside an instance body", ); }; @@ -723,30 +636,13 @@ impl<'a> Elaborator<'a> { let mor_ty = match self.evaluator().path_ty(&record_ty, &self_val, path) { Ok(ty) => ty, Err(e) => { - return self - .fiber_syn_error(format!("no such codomain morphism {}: {e}", path_str(path))); + return self.fiber_syn_error_msg(format!( + "no such codomain morphism {}: {e}", + path_str(path) + )); } }; - let BaseTyV_::Morphism(_, dom_obj, cod_obj) = &*mor_ty else { - return self - .fiber_syn_error(format!("codomain field {} is not a morphism", path_str(path))); - }; - if let Err(e) = self.evaluator().equal_tm(&arg_obj, dom_obj) { - let ev = self.evaluator(); - return self.fiber_syn_error(format!( - "argument to {} lies over {}, but it expects {}:\n{}", - path_str(path), - ev.quote_tm(&arg_obj), - ev.quote_tm(dom_obj), - e.pretty() - )); - } - let cod_s = self.evaluator().quote_tm(cod_obj); - ( - FiberTmS::over_app(path.to_vec(), cod_s, arg_s), - FiberTmV::over_app(path.to_vec(), cod_obj.clone(), arg_v), - FiberTyV::over(cod_obj.clone()), - ) + self.fiber_mor_app(path, &mor_ty, arg_s, arg_v, &arg_obj) } /// Elaborate an instance body — a tuple of `name : type`, `field @@ -764,12 +660,8 @@ impl<'a> Elaborator<'a> { /// scope. fn instance_body(&mut self, codomain: &RecordV, n: &FNtn) -> (FiberTyS, FiberTyV) { let c = self.checkpoint(); - let binder = name_seg(Self::CODOMAIN_BINDER); - self.intro( - binder, - label_seg(Self::CODOMAIN_BINDER), - Some(BaseTyV::record(codomain.clone())), - ); + let binder = name_seg(CODOMAIN_BINDER); + self.intro(binder, label_seg(CODOMAIN_BINDER), Some(BaseTyV::record(codomain.clone()))); let result = self.instance_body_inner(n); self.reset_to(c); result @@ -782,8 +674,8 @@ impl<'a> Elaborator<'a> { /// sub-instance import into the fiber scope by its original name. fn enter_instance(&mut self, inst: &Instance) -> Option<()> { self.intro( - name_seg(Self::CODOMAIN_BINDER), - label_seg(Self::CODOMAIN_BINDER), + name_seg(CODOMAIN_BINDER), + label_seg(CODOMAIN_BINDER), Some(inst.codomain.clone()), ); let FiberTyV_::Record(fields) = &*inst.val else { @@ -921,11 +813,13 @@ impl<'a> Elaborator<'a> { entry_failed = true; break; }; - let over_s = FiberTyS::over(elab.evaluator().quote_tm(cod_obj)); + let cod_obj = cod_obj.clone(); let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, target_n); + let (id_s, id_v) = + elab.fiber_id_field(&lhs_ty, &cod_obj, lhs_s, lhs_v, rhs_s, rhs_v); let (eqn, eql) = next_eq_field(&mut eq_count); - fields_s.insert(eqn, eql, FiberTyS::id(over_s, lhs_s, rhs_s)); - fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); + fields_s.insert(eqn, eql, id_s); + fields_v.insert(eqn, eql, id_v); } if entry_failed { failed = true; @@ -985,23 +879,19 @@ impl<'a> Elaborator<'a> { // `mor(arg) := target` — a single equation witness. App2(L(_, Keyword(":=")), lhs_n, rhs_n) => { let (lhs_s, lhs_v, lhs_ty) = elab.fiber_syn(lhs_n); - let over_s = match &*lhs_ty { - FiberTyV_::Over(obj) => FiberTyS::over(elab.evaluator().quote_tm(obj)), - _ => { - elab.loc = Some(lhs_n.loc()); - elab.error::<()>( - "mapping-entry clause `mor(arg) := target` requires the LHS \ - to be an element over an object (a fiber element); morphism \ - equations constrain the model, not an instance", - ); - failed = true; - continue; - } + let FiberTyV_::Over(obj) = &*lhs_ty else { + elab.loc = Some(lhs_n.loc()); + elab.report_fiber(FiberError::MappingLhsNotOver); + failed = true; + continue; }; + let obj = obj.clone(); let (rhs_s, rhs_v) = elab.fiber_chk(&lhs_ty, rhs_n); + let (id_s, id_v) = + elab.fiber_id_field(&lhs_ty, &obj, lhs_s, lhs_v, rhs_s, rhs_v); let (eqn, eql) = next_eq_field(&mut eq_count); - fields_s.insert(eqn, eql, FiberTyS::id(over_s, lhs_s, rhs_s)); - fields_v.insert(eqn, eql, FiberTyV::id(lhs_ty.clone(), lhs_v, rhs_v)); + fields_s.insert(eqn, eql, id_s); + fields_v.insert(eqn, eql, id_v); } _ => { elab.error::<()>( @@ -1509,6 +1399,85 @@ impl<'a> Elaborator<'a> { /// Extract the path to a codomain morphism from the head of an /// application: a bare variable `f` gives `[f]`, and a projection chain /// `Add.op` gives `[Add, op]`. Returns `None` for any other shape. +impl<'a> FiberElab for Elaborator<'a> { + fn ctx(&self) -> &Context { + &self.ctx + } + + fn ctx_mut(&mut self) -> &mut Context { + &mut self.ctx + } + + fn elab_theory(&self) -> &Theory { + &self.theory + } + + fn evaluator(&self) -> Evaluator<'_> { + Elaborator::evaluator(self) + } + + fn fresh_meta(&mut self) -> MetaVar { + Elaborator::fresh_meta(self) + } + + /// Formats fiber errors into the exact messages this elaborator has + /// always reported; they appear verbatim in committed snapshots, so the + /// strings must not drift. + fn report_fiber(&mut self, err: FiberError) { + let msg = match err { + FiberError::UnknownElement(name) => format!("no such fiber element {name}"), + FiberError::ProjNonRecord => { + "can only project a generator out of a sub-instance".to_string() + } + FiberError::UnknownProj(field) => { + format!("no such generator {field} in sub-instance") + } + FiberError::UnknownObOp(op, th) => format!("operation @{op} not in theory {th}"), + FiberError::ObOpOnNonElement(op) => format!("@{op} applied to a non-fiber-element"), + FiberError::ListElementNotOver => { + "fiber list elements must be elements over an object".to_string() + } + // Only notebook cells can contain unfilled slots. + FiberError::MissingTerm => "missing term".to_string(), + FiberError::ArgNotElement(label) => match label { + Some(label) => format!("argument {label} is not an element over an object"), + None => "argument is not an element over an object".to_string(), + }, + FiberError::NotAMorphism(path) => { + format!("codomain field {path} is not a morphism") + } + FiberError::ArgMismatch { path, got, expected, detail } => { + format!("argument to {path} lies over {got}, but it expects {expected}:\n{detail}") + } + FiberError::WrongFiberType(detail) => { + format!("fiber element has the wrong type:\n{detail}") + } + FiberError::MappingLhsNotOver => { + "mapping-entry clause `mor(arg) := target` requires the LHS \ + to be an element over an object (a fiber element); morphism \ + equations constrain the model, not an instance" + .to_string() + } + FiberError::EquationNotOver => { + "instance equations must be between elements over an object \ + (fiber elements); morphism equations constrain the model, not \ + an instance" + .to_string() + } + FiberError::InconvertibleEquationSides(detail) => { + format!("equation sides have inconvertible fiber types:\n{detail}") + } + FiberError::ImportCodomainMismatch(name) => { + format!( + "cannot import {name}: it is an instance of a different model than \ + the enclosing instance" + ) + } + }; + self.reporter.error_option_loc(self.loc, ELAB_ERROR, msg); + } +} + fn morphism_path(n: &FNtn) -> Option> { match n.ast0() { Var(f) => Some(vec![(name_seg(*f), label_seg(*f))]), @@ -1523,10 +1492,6 @@ fn morphism_path(n: &FNtn) -> Option> { /// Render a morphism/object path as dotted labels (e.g. `Add.op`), for /// error messages. -fn path_str(path: &[(FieldName, LabelSegment)]) -> String { - path.iter().map(|(_, label)| label.to_string()).collect::>().join(".") -} - /// The synthetic field name/label `_eqN` for the next auto-named equation /// field of an instance record, advancing the counter. fn next_eq_field(eq_count: &mut usize) -> (FieldName, LabelSegment) { From ed76bffdf2cdd79da837adee541651750f66c372 Mon Sep 17 00:00:00 2001 From: Kevin Carlson Date: Fri, 17 Jul 2026 16:59:57 -0700 Subject: [PATCH 53/53] Getting utils that aren't special to tt out of tt --- packages/catlog/src/dbl/discrete/model.rs | 2 +- .../src/dbl/discrete_tabulator/model.rs | 2 +- .../src/dbl/discrete_tabulator/theory.rs | 2 +- packages/catlog/src/dbl/modal/model.rs | 2 +- packages/catlog/src/dbl/model.rs | 2 +- packages/catlog/src/tt/fiber_elab.rs | 10 ++ packages/catlog/src/tt/mod.rs | 1 - packages/catlog/src/tt/prelude.rs | 2 +- packages/catlog/src/tt/util/mod.rs | 13 --- packages/catlog/src/tt/wd.rs | 3 +- packages/catlog/src/wd/undirected.rs | 2 +- packages/catlog/src/{tt/util => zero}/dtry.rs | 22 ++-- packages/catlog/src/{tt/util => zero}/idx.rs | 0 packages/catlog/src/zero/mod.rs | 4 + .../catlog/src/{tt/util => zero}/pretty.rs | 0 packages/catlog/src/zero/qualified.rs | 2 +- packages/catlog/src/{tt/util => zero}/row.rs | 31 ++--- rfc/preview.sh | 107 ++++++++++++++++++ 18 files changed, 159 insertions(+), 48 deletions(-) delete mode 100644 packages/catlog/src/tt/util/mod.rs rename packages/catlog/src/{tt/util => zero}/dtry.rs (83%) rename packages/catlog/src/{tt/util => zero}/idx.rs (100%) rename packages/catlog/src/{tt/util => zero}/pretty.rs (100%) rename packages/catlog/src/{tt/util => zero}/row.rs (67%) create mode 100755 rfc/preview.sh diff --git a/packages/catlog/src/dbl/discrete/model.rs b/packages/catlog/src/dbl/discrete/model.rs index 15f6e374d..f81391147 100644 --- a/packages/catlog/src/dbl/discrete/model.rs +++ b/packages/catlog/src/dbl/discrete/model.rs @@ -7,8 +7,8 @@ use derivative::Derivative; use super::theory::DiscreteDblTheory; use crate::dbl::{category::*, model::*, theory::DblTheory}; use crate::one::{fp_category::QualifiedFpCategory, *}; -use crate::tt::util::pretty::*; use crate::validate::{self, Validate}; +use crate::zero::pretty::*; use crate::zero::*; /// A finitely presented model of a discrete double theory. diff --git a/packages/catlog/src/dbl/discrete_tabulator/model.rs b/packages/catlog/src/dbl/discrete_tabulator/model.rs index 1c9b65744..bf8759596 100644 --- a/packages/catlog/src/dbl/discrete_tabulator/model.rs +++ b/packages/catlog/src/dbl/discrete_tabulator/model.rs @@ -7,8 +7,8 @@ use derive_more::From; use super::theory::*; use crate::dbl::{category::*, model::*, theory::DblTheory}; -use crate::tt::util::pretty::*; use crate::validate::{self, Validate}; +use crate::zero::pretty::*; use crate::{one::*, zero::*}; /// Object in a model of a discrete tabulator theory. diff --git a/packages/catlog/src/dbl/discrete_tabulator/theory.rs b/packages/catlog/src/dbl/discrete_tabulator/theory.rs index c276f76eb..ef105d195 100644 --- a/packages/catlog/src/dbl/discrete_tabulator/theory.rs +++ b/packages/catlog/src/dbl/discrete_tabulator/theory.rs @@ -8,7 +8,7 @@ use ref_cast::RefCast; use crate::dbl::{category::*, graph::ProedgeGraph, tree::DblTree}; use crate::one::{Graph, Path}; -use crate::tt::util::pretty::*; +use crate::zero::pretty::*; use crate::zero::*; /// Object type in a discrete tabulator theory. diff --git a/packages/catlog/src/dbl/modal/model.rs b/packages/catlog/src/dbl/modal/model.rs index 11f3c9363..ebf71cbae 100644 --- a/packages/catlog/src/dbl/modal/model.rs +++ b/packages/catlog/src/dbl/modal/model.rs @@ -12,8 +12,8 @@ use ref_cast::RefCast; use super::theory::*; use crate::dbl::theory::DblTheoryKind; use crate::dbl::{graph::VDblGraph, model::*, theory::DblTheory}; -use crate::tt::util::pretty::*; use crate::validate::{self, Validate}; +use crate::zero::pretty::*; use crate::{one::computad::*, one::*, zero::*}; /// Object in a model of a modal double theory. diff --git a/packages/catlog/src/dbl/model.rs b/packages/catlog/src/dbl/model.rs index 9b3156918..f859e1b00 100644 --- a/packages/catlog/src/dbl/model.rs +++ b/packages/catlog/src/dbl/model.rs @@ -43,7 +43,7 @@ use tsify::Tsify; use super::theory::DblTheory; use crate::one::{Category, FgCategory, InvalidPathEq, Path}; -use crate::tt::util::pretty::*; +use crate::zero::pretty::*; use crate::zero::{Namespace, QualifiedName}; pub use super::discrete::model::*; diff --git a/packages/catlog/src/tt/fiber_elab.rs b/packages/catlog/src/tt/fiber_elab.rs index 7314d1bb4..f822ec571 100644 --- a/packages/catlog/src/tt/fiber_elab.rs +++ b/packages/catlog/src/tt/fiber_elab.rs @@ -12,6 +12,16 @@ //! //! Everything here is `pub(in crate::tt)`. The module boundary enforced today //! is the crate boundary of a prospective standalone `tt` crate. +//! +//! The *base* world has an analogous but so-far-unextracted duplication: the +//! notebook elaborator's composite-morphism typing (`mor_syn`), equation +//! formation (`equation_cell_ty`), and instantiation specialization each +//! parallel text-side logic, with the same split between `Reporter` strings +//! and typed `InvalidDblModel` values — and the notebook's base errors are +//! *coarser* (bare `DomType`/`CodType` with no detail). This trait plus its +//! neutral error enum is the template for a future `BaseElab`/`BaseError` +//! pass, worth doing when notebook model diagnostics need text-quality +//! precision; the same snapshot byte-stability constraint applies. use super::{context::*, eval::*, prelude::*, stx::*, theory::*, val::*}; diff --git a/packages/catlog/src/tt/mod.rs b/packages/catlog/src/tt/mod.rs index 866d0d786..d5c379631 100644 --- a/packages/catlog/src/tt/mod.rs +++ b/packages/catlog/src/tt/mod.rs @@ -203,7 +203,6 @@ pub mod stx; pub mod text_elab; pub mod theory; pub mod toplevel; -pub mod util; pub mod val; pub mod wd; diff --git a/packages/catlog/src/tt/prelude.rs b/packages/catlog/src/tt/prelude.rs index e358d9c54..a7c9e7634 100644 --- a/packages/catlog/src/tt/prelude.rs +++ b/packages/catlog/src/tt/prelude.rs @@ -1,10 +1,10 @@ //! Common imports for [`tt`](crate::tt). -pub use crate::tt::util::*; pub use crate::zero::{ LabelSegment, qualified::{label_seg, name_seg}, }; +pub use crate::zero::{dtry::*, idx::*, pretty::*, row::*}; pub use crate::{one::Path, zero::NameSegment}; pub use indexmap::IndexMap; pub use std::collections::HashMap; diff --git a/packages/catlog/src/tt/util/mod.rs b/packages/catlog/src/tt/util/mod.rs deleted file mode 100644 index 4ada5c21e..000000000 --- a/packages/catlog/src/tt/util/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Various utilities that are not strictly tied to the specific type theory. -//! -//! Perhaps some of these could move to [crate::zero]. - -pub mod dtry; -pub mod idx; -pub mod pretty; -pub mod row; - -pub use dtry::*; -pub use idx::*; -pub use pretty::*; -pub use row::*; diff --git a/packages/catlog/src/tt/wd.rs b/packages/catlog/src/tt/wd.rs index 52b70c7dd..01861bfe5 100644 --- a/packages/catlog/src/tt/wd.rs +++ b/packages/catlog/src/tt/wd.rs @@ -1,8 +1,9 @@ //! Extract wiring diagrams from record types. -use super::{eval::*, theory::*, toplevel::*, util::*, val::*}; +use super::{eval::*, theory::*, toplevel::*, val::*}; use crate::wd::UWD; use crate::zero::QualifiedName; +use crate::zero::dtry::*; /// Extracts an undirected wiring diagram from a record type. /// diff --git a/packages/catlog/src/wd/undirected.rs b/packages/catlog/src/wd/undirected.rs index 3e34d596c..1f1beee25 100644 --- a/packages/catlog/src/wd/undirected.rs +++ b/packages/catlog/src/wd/undirected.rs @@ -3,9 +3,9 @@ use derivative::Derivative; use std::{fmt, hash::Hash}; -use crate::tt::util::{Row, pretty::*}; use crate::validate::{self, Validate}; use crate::zero::{Column, HashColumn, LabelSegment, Mapping, MutMapping, NameSegment}; +use crate::zero::{pretty::*, row::Row}; /// Ports of a wiring diagram. /// diff --git a/packages/catlog/src/tt/util/dtry.rs b/packages/catlog/src/zero/dtry.rs similarity index 83% rename from packages/catlog/src/tt/util/dtry.rs rename to packages/catlog/src/zero/dtry.rs index 509f8e595..7994a7fee 100644 --- a/packages/catlog/src/tt/util/dtry.rs +++ b/packages/catlog/src/zero/dtry.rs @@ -1,9 +1,9 @@ //! Directories. -use crate::{ - tt::prelude::*, - zero::{LabelSegment, QualifiedLabel, QualifiedName}, -}; +use indexmap::IndexMap; + +use super::qualified::{LabelSegment, NameSegment, QualifiedLabel, QualifiedName}; +use super::row::Row; /// An entry in a [Dtry]. /// @@ -27,7 +27,7 @@ impl DtryEntry { } } - fn singleton(path: &[(FieldName, LabelSegment)], val: T) -> Self { + fn singleton(path: &[(NameSegment, LabelSegment)], val: T) -> Self { if path.is_empty() { DtryEntry::File(val) } else { @@ -52,7 +52,7 @@ impl DtryEntry { /// A directory. /// -/// A `Dtry` consists of a mapping from `FieldName`s to directory +/// A `Dtry` consists of a mapping from `NameSegment`s to directory /// entries, where a directory entry is either a "File" ([DtryEntry::File]), /// that is an element of `T`, or a "subdirectory" ([DtryEntry::SubDir]), /// which is just another directory. @@ -86,17 +86,17 @@ impl Dtry { } /// Iterate through the entries of the directory. - pub fn entries(&self) -> impl Iterator))> { + pub fn entries(&self) -> impl Iterator))> { self.0.iter() } /// Get the entry for `field` if it exists. - pub fn entry(&self, field: &FieldName) -> Option<&DtryEntry> { + pub fn entry(&self, field: &NameSegment) -> Option<&DtryEntry> { self.0.get(*field) } /// Create a singleton directory with just one entry at the given path. - pub fn singleton(path: &[(FieldName, LabelSegment)], val: T) -> Self { + pub fn singleton(path: &[(NameSegment, LabelSegment)], val: T) -> Self { assert!(!path.is_empty()); let ((field, label), path) = (path[0], &path[1..]); Dtry([(field, (label, DtryEntry::singleton(path, val)))].into_iter().collect()) @@ -124,8 +124,8 @@ impl Dtry { } } -impl From)>> for Dtry { - fn from(value: IndexMap)>) -> Self { +impl From)>> for Dtry { + fn from(value: IndexMap)>) -> Self { Self(value.into()) } } diff --git a/packages/catlog/src/tt/util/idx.rs b/packages/catlog/src/zero/idx.rs similarity index 100% rename from packages/catlog/src/tt/util/idx.rs rename to packages/catlog/src/zero/idx.rs diff --git a/packages/catlog/src/zero/mod.rs b/packages/catlog/src/zero/mod.rs index b8faee6db..6500e4959 100644 --- a/packages/catlog/src/zero/mod.rs +++ b/packages/catlog/src/zero/mod.rs @@ -2,8 +2,12 @@ pub mod alg; pub mod column; +pub mod dtry; +pub mod idx; +pub mod pretty; pub mod qualified; pub mod rig; +pub mod row; pub mod set; pub use self::column::*; diff --git a/packages/catlog/src/tt/util/pretty.rs b/packages/catlog/src/zero/pretty.rs similarity index 100% rename from packages/catlog/src/tt/util/pretty.rs rename to packages/catlog/src/zero/pretty.rs diff --git a/packages/catlog/src/zero/qualified.rs b/packages/catlog/src/zero/qualified.rs index 908f76d23..eaeda123a 100644 --- a/packages/catlog/src/zero/qualified.rs +++ b/packages/catlog/src/zero/qualified.rs @@ -14,7 +14,7 @@ use serde::{self, Deserialize, Serialize}; use tsify::Tsify; use super::column::{Column, IndexedHashColumn, Mapping, MutMapping}; -use crate::tt::util::pretty::*; +use super::pretty::*; /// A segment in a [qualified name](QualifiedName). /// diff --git a/packages/catlog/src/tt/util/row.rs b/packages/catlog/src/zero/row.rs similarity index 67% rename from packages/catlog/src/tt/util/row.rs rename to packages/catlog/src/zero/row.rs index baf0ada01..a357e54ec 100644 --- a/packages/catlog/src/tt/util/row.rs +++ b/packages/catlog/src/zero/row.rs @@ -4,12 +4,15 @@ use derivative::Derivative; use derive_more::From; use std::ops::Index; -use crate::{tt::prelude::*, zero::LabelSegment}; +use indexmap::IndexMap; -/// An insertion-ordered map from `FieldName` to `T`. +use super::qualified::{LabelSegment, NameSegment, label_seg, name_seg}; +use ustr::Ustr; + +/// An insertion-ordered map from `NameSegment` to `T`. /// /// Also stores a "label" for each entry, which may not be the same as the -/// FieldName in the case that the FieldName is a UUID. +/// NameSegment in the case that the NameSegment is a UUID. /// /// This is called "row" because it's a short name, and it corresponds to the idea /// of a row in a database, which is a map from fields to values. @@ -17,11 +20,11 @@ use crate::{tt::prelude::*, zero::LabelSegment}; /// Create this using the [FromIterator] implementation. #[derive(Clone, Derivative, PartialEq, Eq, From)] #[derivative(Default(bound = ""))] -pub struct Row(IndexMap); +pub struct Row(IndexMap); -impl Index for Row { +impl Index for Row { type Output = T; - fn index(&self, index: FieldName) -> &Self::Output { + fn index(&self, index: NameSegment) -> &Self::Output { self.get(index).unwrap() } } @@ -30,22 +33,22 @@ impl Row { /// Lookup the field `name` if it exists. /// /// Also see the [Index] implementation, which just `unwrap`s this. - pub fn get(&self, name: FieldName) -> Option<&T> { + pub fn get(&self, name: NameSegment) -> Option<&T> { self.0.get(&name).map(|p| &p.1) } /// Lookup the field `name` by mutable reference. - pub fn get_mut(&mut self, name: FieldName) -> Option<&mut T> { + pub fn get_mut(&mut self, name: NameSegment) -> Option<&mut T> { self.0.get_mut(&name).map(|p| &mut p.1) } /// Lookup the field `name` if it exists, and get its value and label. - pub fn get_with_label(&self, name: FieldName) -> Option<&(LabelSegment, T)> { + pub fn get_with_label(&self, name: NameSegment) -> Option<&(LabelSegment, T)> { self.0.get(&name) } /// Iterate through the fields in insertion order. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0.iter() } @@ -60,7 +63,7 @@ impl Row { } /// Return whether the row contains the given field. - pub fn has(&self, field_name: FieldName) -> bool { + pub fn has(&self, field_name: NameSegment) -> bool { self.0.contains_key(&field_name) } @@ -75,13 +78,13 @@ impl Row { } /// Insert a new field. - pub fn insert(&mut self, field: FieldName, label: LabelSegment, value: T) { + pub fn insert(&mut self, field: NameSegment, label: LabelSegment, value: T) { self.0.insert(field, (label, value)); } } -impl FromIterator<(FieldName, (LabelSegment, T))> for Row { - fn from_iter>(iter: I) -> Self { +impl FromIterator<(NameSegment, (LabelSegment, T))> for Row { + fn from_iter>(iter: I) -> Self { Row(iter.into_iter().collect()) } } diff --git a/rfc/preview.sh b/rfc/preview.sh new file mode 100755 index 000000000..63d3a5065 --- /dev/null +++ b/rfc/preview.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Lightweight RFC previewer: render a Quarto-flavored .md to standalone HTML +# with MathJax, without invoking Quarto. +# +# ./preview.sh 0009 # build once and open +# ./preview.sh 0009 --watch # serve + live-reload on save +set -euo pipefail +cd "$(dirname "$0")" + +num="${1:?usage: preview.sh [--watch]}" +mode="${2:-}" +src="${num}.md" +out="${num}.preview.html" +port=8765 + +# Live-reload snippet: poll this page's Last-Modified over HTTP and reload only +# when it changes, preserving scroll position across reloads. +reload_js="$(cat <<'EOF' + +EOF +)" + +build() { + local tmp after mfile + tmp="$(mktemp -t rfc-preview-XXXX).md" + after="$(mktemp -t rfc-reload-XXXX).html" + mfile="$(mktemp -t rfc-macros-XXXX).js" + printf '%s\n' "$reload_js" > "$after" + + # Collect \newcommand macros from each {{< include FILE.qmd >}} as KaTeX + # `macros` option entries ("\\name": "body"), mirroring how Quarto feeds + # _macros.qmd to KaTeX. KaTeX infers arity from #1.. in the body, so the + # optional [n] is dropped; backslashes/quotes are escaped for the JS string. + : > "$mfile" + grep -oE '\{\{< include [^ ]+\.qmd' "$src" | awk '{print $3}' | while read -r inc; do + [ -f "$inc" ] && perl -ne ' + next if /^\s*