Skip to main content

logicaffeine_kernel/
structure_eta.rs

1//! Structure η — the one door that grants it.
2//!
3//! Registering [`StructInfo`] for an inductive `I` switches on the conversion rule
4//! `x ≡ mk p̄ (proj₀ p̄ x) … (projₙ p̄ x)` for every `x : I p̄` (both kernels' `try_structure_eta`).
5//! That rule is sound only when `I` really is a structure and the named projections really are
6//! its projections, so η is granted only after [`Context::register_structure_checked`] has
7//! decided, in this order:
8//!
9//! 1. `I` is a declared inductive, not already a structure, and not sealed: a native nested
10//!    positivity check that depends on `I` forbids changing it, and the door answers `Ok` only
11//!    for a grant it has made;
12//! 2. `I` has exactly one constructor and no indices;
13//! 3. the constructor binds all of `I`'s parameters;
14//! 4. `I`'s mutual block is not recursive: no constructor of any member takes an argument whose
15//!    type mentions a member, even under a container or through a definition (Lean's `isRec`,
16//!    which its `isStructureLike` requires false — η is for structures, never recursive types);
17//! 5. there is exactly one projection per field;
18//! 6. each projection is a transparent definition over exactly `I`'s universe parameters;
19//! 7. for a generic constructor application `mk p̄ f̄` — parameters and fields as fresh local
20//!    variables — `projᵢ p̄ (mk p̄ f̄)` is definitionally `fᵢ`, and
21//! 8. well typed at the field's own type `Fᵢ`. Reduction preserves types, so a well-typed
22//!    projection that computes its field always passes; this refuses the ill-typed application
23//!    that still reduces to `fᵢ` (a projection over a different instance of the structure).
24//!
25//! The vernacular `Structure` command, the environment certificate's `structure` entry and the
26//! Lean reader all call this function; none of them registers η any other way. The public metadata
27//! door [`Context::register_struct_info`] asks the same decision before it registers anything, so
28//! the only η granted without it is the kernel's own structure builder's, whose constructor and
29//! projections it generates itself.
30use std::fmt;
31
32use crate::context::{Context, StructInfo};
33use crate::term::{Term, Universe};
34
35/// Why structure η was refused. Every variant names the first condition that failed.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum StructureRefusal {
38    /// The named inductive is not declared.
39    NotAnInductive { inductive: String },
40    /// The inductive already has structure η.
41    AlreadyAStructure { inductive: String },
42    /// A native nested positivity check depends on the inductive, so it cannot change.
43    Sealed { inductive: String },
44    /// A structure has exactly one constructor and no indices.
45    NotStructureShaped {
46        inductive: String,
47        constructors: usize,
48        indices: usize,
49    },
50    /// The constructor's type binds fewer binders than the inductive has parameters.
51    ShortConstructor { constructor: String, params: usize },
52    /// The inductive's mutual block is recursive.
53    Recursive { inductive: String },
54    /// The number of projections differs from the number of fields.
55    ProjectionCount {
56        inductive: String,
57        fields: usize,
58        projections: usize,
59    },
60    /// A projection is not a transparent definition.
61    NotADefinition { projection: String },
62    /// A projection's universe telescope is not the inductive's.
63    UniverseArity {
64        projection: String,
65        inductive: String,
66        expected: usize,
67        found: usize,
68    },
69    /// `projᵢ p̄ (mk p̄ f̄)` is not definitionally `fᵢ`.
70    DoesNotCompute {
71        projection: String,
72        field: usize,
73        inductive: String,
74    },
75    /// `projᵢ p̄ (mk p̄ f̄)` computes `fᵢ` but is ill typed, or not of the field's type.
76    WrongFieldType {
77        projection: String,
78        field: usize,
79        inductive: String,
80    },
81}
82
83impl fmt::Display for StructureRefusal {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            StructureRefusal::NotAnInductive { inductive } => {
87                write!(f, "structure η names `{inductive}`, which is not a declared inductive")
88            }
89            StructureRefusal::AlreadyAStructure { inductive } => {
90                write!(f, "`{inductive}` already has structure η")
91            }
92            StructureRefusal::Sealed { inductive } => write!(
93                f,
94                "`{inductive}` is sealed by a native nested positivity check that depends on it, so it cannot gain structure η"
95            ),
96            StructureRefusal::NotStructureShaped { inductive, constructors, indices } => write!(
97                f,
98                "`{inductive}` is not a structure: it has {constructors} constructors and {indices} indices, \
99                 where a structure has exactly one constructor and no indices"
100            ),
101            StructureRefusal::ShortConstructor { constructor, params } => {
102                write!(f, "`{constructor}` binds fewer than its inductive's {params} parameters")
103            }
104            StructureRefusal::Recursive { inductive } => {
105                write!(f, "`{inductive}` is recursive, and structure η is for structures")
106            }
107            StructureRefusal::ProjectionCount { inductive, fields, projections } => {
108                write!(f, "`{inductive}` has {fields} fields but names {projections} projections")
109            }
110            StructureRefusal::NotADefinition { projection } => {
111                write!(f, "`{projection}` is not a transparent definition")
112            }
113            StructureRefusal::UniverseArity { projection, inductive, expected, found } => write!(
114                f,
115                "`{projection}` binds {found} universe parameters, not `{inductive}`'s {expected}"
116            ),
117            StructureRefusal::WrongFieldType { projection, field, inductive } => write!(
118                f,
119                "`{projection}` applied to a constructor of `{inductive}` does not have the type of field {field}"
120            ),
121            StructureRefusal::DoesNotCompute { projection, field, inductive } => {
122                write!(f, "`{projection}` does not compute field {field} of `{inductive}`")
123            }
124        }
125    }
126}
127
128impl std::error::Error for StructureRefusal {}
129
130impl Context {
131    /// Grant structure η to `inductive` through `projections`, after deciding each condition a
132    /// `StructureRefusal` variant names: one constructor, no indices, no recursion, and
133    /// projections that compute their fields at the fields' types. On refusal the context is
134    /// unchanged.
135    pub fn register_structure_checked(
136        &mut self,
137        inductive: &str,
138        projections: &[String],
139    ) -> Result<(), StructureRefusal> {
140        let info = self.structure_eta_evidence(inductive, projections)?;
141        self.insert_struct_info(inductive, info);
142        Ok(())
143    }
144
145    /// Lean's `isRec`, decided for the whole mutual block as Lean's kernel decides it: some
146    /// constructor of some member takes an argument whose type mentions a member, directly, under
147    /// a container, or through a definition. The door refuses such a family
148    /// ([`StructureRefusal::Recursive`]); a caller may ask first to avoid building projections
149    /// the door would refuse.
150    pub(crate) fn block_is_recursive(&self, inductive: &str) -> bool {
151        let block: Vec<&str> = match self.mutual_block_of(inductive) {
152            Some(members) => members.iter().map(String::as_str).collect(),
153            None => vec![inductive],
154        };
155        block.iter().any(|member| {
156            self.get_constructors(member).into_iter().any(|(_, ty)| {
157                let mut rest = ty;
158                while let Term::Pi {
159                    param_type,
160                    body_type,
161                    ..
162                } = rest
163                {
164                    if crate::positivity::mentions_inductive_block(self, &block, param_type) {
165                        return true;
166                    }
167                    rest = body_type;
168                }
169                false
170            })
171        })
172    }
173
174    /// The whole decision, without registering anything: the [`StructInfo`] η would be granted
175    /// with, or the first condition that failed. [`Context::register_struct_info`] asks it too.
176    pub(crate) fn structure_eta_evidence(
177        &self,
178        inductive: &str,
179        projections: &[String],
180    ) -> Result<StructInfo, StructureRefusal> {
181        let named = || inductive.to_string();
182        if !self.is_inductive(inductive) {
183            return Err(StructureRefusal::NotAnInductive { inductive: named() });
184        }
185        if self.struct_info(inductive).is_some() {
186            return Err(StructureRefusal::AlreadyAStructure { inductive: named() });
187        }
188        if self.check_native_dependency_mutation(inductive).is_err() {
189            return Err(StructureRefusal::Sealed { inductive: named() });
190        }
191        let constructors = self.get_constructors(inductive);
192        let indices = self.inductive_num_indices(inductive);
193        if constructors.len() != 1 || indices != 0 {
194            return Err(StructureRefusal::NotStructureShaped {
195                inductive: named(),
196                constructors: constructors.len(),
197                indices,
198            });
199        }
200        let (mk, mk_ty) = (constructors[0].0.to_string(), constructors[0].1.clone());
201        let levels: Vec<Universe> = self
202            .poly_declaration(inductive)
203            .map(|(params, _)| params.iter().cloned().map(Universe::Var).collect())
204            .unwrap_or_default();
205        let constant = |name: &str| {
206            if levels.is_empty() {
207                Term::Global(name.to_string())
208            } else {
209                Term::Const {
210                    name: name.to_string(),
211                    levels: levels.clone(),
212                }
213            }
214        };
215
216        let num_params = self.inductive_num_params(inductive);
217        let mut params = Vec::with_capacity(num_params);
218        let mut rest = mk_ty;
219        for i in 0..num_params {
220            let Term::Pi {
221                param,
222                param_type,
223                body_type,
224            } = rest
225            else {
226                return Err(StructureRefusal::ShortConstructor {
227                    constructor: mk,
228                    params: num_params,
229                });
230            };
231            let fresh = format!("structure.parameter.{i}");
232            rest = crate::substitute(&body_type, &param, &Term::Var(fresh.clone()));
233            params.push((fresh, *param_type));
234        }
235        let mut fields = Vec::new();
236        while let Term::Pi {
237            param,
238            param_type,
239            body_type,
240        } = rest
241        {
242            let fresh = format!("structure.field.{}", fields.len());
243            rest = crate::substitute(&body_type, &param, &Term::Var(fresh.clone()));
244            fields.push((fresh, *param_type));
245        }
246
247        if self.block_is_recursive(inductive) {
248            return Err(StructureRefusal::Recursive { inductive: named() });
249        }
250        if projections.len() != fields.len() {
251            return Err(StructureRefusal::ProjectionCount {
252                inductive: named(),
253                fields: fields.len(),
254                projections: projections.len(),
255            });
256        }
257
258        let mut local = self.clone();
259        for (name, ty) in params.iter().chain(fields.iter()) {
260            local = local.extend(name, ty.clone());
261        }
262        let apply_all = |head: Term, args: &mut dyn Iterator<Item = Term>| {
263            args.fold(head, |f, a| Term::App(Box::new(f), Box::new(a)))
264        };
265        let param_terms: Vec<Term> = params.iter().map(|(n, _)| Term::Var(n.clone())).collect();
266        let value = apply_all(
267            constant(&mk),
268            &mut param_terms
269                .iter()
270                .cloned()
271                .chain(fields.iter().map(|(n, _)| Term::Var(n.clone()))),
272        );
273        for (i, projection) in projections.iter().enumerate() {
274            let transparent = self.get_definition_body(projection).is_some()
275                || self.get_universe_poly(projection).is_some();
276            if !transparent {
277                return Err(StructureRefusal::NotADefinition {
278                    projection: projection.clone(),
279                });
280            }
281            let arity = self
282                .poly_declaration(projection)
283                .map(|(p, _)| p.len())
284                .unwrap_or(0);
285            if arity != levels.len() {
286                return Err(StructureRefusal::UniverseArity {
287                    projection: projection.clone(),
288                    inductive: named(),
289                    expected: levels.len(),
290                    found: arity,
291                });
292            }
293            let applied = apply_all(
294                constant(projection),
295                &mut param_terms.iter().cloned().chain([value.clone()]),
296            );
297            if !crate::type_checker::def_eq(&local, &applied, &Term::Var(fields[i].0.clone())) {
298                return Err(StructureRefusal::DoesNotCompute {
299                    projection: projection.clone(),
300                    field: i,
301                    inductive: named(),
302                });
303            }
304            let typed = crate::infer_type(&local, &applied)
305                .is_ok_and(|ty| crate::type_checker::def_eq(&local, &ty, &fields[i].1));
306            if !typed {
307                return Err(StructureRefusal::WrongFieldType {
308                    projection: projection.clone(),
309                    field: i,
310                    inductive: named(),
311                });
312            }
313        }
314        Ok(StructInfo {
315            mk,
316            num_params,
317            projections: projections.to_vec(),
318            params,
319            fields,
320            parents: Vec::new(),
321            out_params: Vec::new(),
322        })
323    }
324}