Skip to main content

rustyfi_lang/
types.rs

1//! The type language: base types, the mutable (union-find) representation
2//! of type/row variables, monomorphic and polymorphic types, and
3//! level-based generalization. Mirrors `mono_type_main` / `poly_type` /
4//! `kind` in v0.0.6's `src/frontend/types.cppo.ml`, with two deliberate
5//! departures documented at their definitions:
6//!
7//! 1. **Generalization is level-based (Rémy levels)**, not v0.0.6's
8//!    `quantifiability` flag. See [`TypeContext`], [`generalize`] and
9//!    [`instantiate`].
10//! 2. **Extensible records are a first-class row type** (`Row::Empty` /
11//!    `Row::Var` / `Row::Cons`), not v0.0.6's closed `RecordType` plus a
12//!    plain type variable carrying a `RecordKind` label-subset constraint.
13//!    See [`Row`].
14
15use std::borrow::Cow;
16use std::cell::RefCell;
17use std::collections::{BTreeSet, HashMap};
18use std::fmt;
19use std::rc::Rc;
20use std::sync::atomic::{AtomicU64, Ordering};
21
22// ============================================================================
23// Base types
24// ============================================================================
25
26/// Primitive types with no internal structure — the subset of v0.0.6's
27/// `base_type` (`types.cppo.ml:255`) that this port's primitives
28/// need. (`EnvType`/`RegExpType`/`InputPosType` are unused and left out;
29/// add them when a primitive needs them.)
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum BaseType {
32    Unit,
33    Bool,
34    Int,
35    Float,
36    Length,
37    String,
38    /// `inline-text` (v0.0.6: `TextRowType`).
39    InlineText,
40    /// `block-text` (v0.0.6: `TextColType`).
41    BlockText,
42    /// `math` (v0.0.6: `MathType`) — quoted math text. Reused, unmodified,
43    /// as V0_1's `math-text` (upstream literally renamed 0.0.6's `math`);
44    /// see `MathBoxes` for the new V0_1-only half of the split.
45    MathText,
46    /// `math-boxes` (V0_1 only; `dev-0-1-0` `MathBoxesType`) — the
47    /// evaluated math tree, bridged from `MathText` by the V0_1 primitive
48    /// `read-math`. 0.0.6 has no name for this type (its `math` conflates
49    /// both halves) and no value ever types as this under V0_0.
50    MathBoxes,
51    /// `image` (v0.0.6: `ImageType`) — a decoded raster image resource
52    /// (`load-image`'s result).
53    Image,
54    /// `inline-boxes` (v0.0.6: `BoxRowType`).
55    InlineBoxes,
56    /// `block-boxes` (v0.0.6: `BoxColType`).
57    BlockBoxes,
58    Context,
59    Document,
60    /// `pre-path` (v0.0.6: `PrePathType`).
61    PrePath,
62    /// `path` (v0.0.6: `PathType`).
63    Path,
64    /// `graphics` (v0.0.6: `GraphicsType`).
65    Graphics,
66    /// `font` (**V0_1 only**; upstream `saphe-split`
67    /// `types.cppo.ml`'s `FontType`, registered in that generation's
68    /// `base_type_hash_table` as `("font", FontType)` and spelled `tFONTKEY`
69    /// in `primitives.cppo.ml:45`) — an OPAQUE handle on one loaded face.
70    /// Its value is [`Value::Font`](crate::value::Value::Font), a
71    /// `rustyfi_backend::FontKey` index into the metrics provider's font
72    /// store, matching upstream's `BCFontKey of FontKey.t`.
73    ///
74    /// **0.0.6 has no such type at all** — verified against upstream
75    /// `v0.0.6 src/frontend/types.cppo.ml:280-303`, whose
76    /// `base_type_hash_table` has no `"font"` row, and against
77    /// `lib-satysfi/dist/packages/*.satyh`, which declare no `type font`
78    /// either. What 0.0.6 calls "a font" is the bare product
79    /// `string * float * float` (`primitives.cppo.ml:69`'s `tFONT = tPROD
80    /// [tS; tFL; tFL]`), whose head is an ABBREV naming a row of
81    /// `dist/hash/fonts.satysfi-hash`. So under `V0_0` the NAME `font`
82    /// falls through `name_to_mono` to the opaque user-nominal
83    /// `Variant("font", [])` — an unrelated type that happens to share a
84    /// spelling. That disagreement is exactly what keeps `font` inside
85    /// `typecheck::forked_type_names()`, and it is a REPRESENTATION fork,
86    /// not a missing feature: see `v1::xver_adapt::forked_note`.
87    Font,
88    /// `text-info` (v0.0.6: `TextInfoType`) — the text-mode context
89    /// (`deepen-indent`/`get-initial-text-info`/`break`; sliver — see
90    /// primitives.rs for the scoping note: the text/html backends
91    /// themselves are out of scope).
92    TextInfo,
93}
94
95impl BaseType {
96    /// The SATySFi surface-syntax name, used by `Display`.
97    pub fn name(self) -> &'static str {
98        match self {
99            BaseType::Unit => "unit",
100            BaseType::Bool => "bool",
101            BaseType::Int => "int",
102            BaseType::Float => "float",
103            BaseType::Length => "length",
104            BaseType::String => "string",
105            BaseType::InlineText => "inline-text",
106            BaseType::BlockText => "block-text",
107            BaseType::MathText => "math",
108            BaseType::MathBoxes => "math-boxes",
109            BaseType::Image => "image",
110            BaseType::InlineBoxes => "inline-boxes",
111            BaseType::BlockBoxes => "block-boxes",
112            BaseType::Context => "context",
113            BaseType::Document => "document",
114            BaseType::PrePath => "pre-path",
115            BaseType::Path => "path",
116            BaseType::Graphics => "graphics",
117            BaseType::Font => "font",
118            BaseType::TextInfo => "text-info",
119        }
120    }
121}
122
123impl fmt::Display for BaseType {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.write_str(self.name())
126    }
127}
128
129// ============================================================================
130// A process-wide id source for variables minted where no `TypeContext` is
131// available (see the doc comment on `instantiate` for why that happens).
132// ============================================================================
133
134/// `TypeContext` hands out ids from its own small counter for ordinary
135/// inference-time freshness. `instantiate` and `unify`'s row extension
136/// have fixed signatures carrying no `TypeContext`, yet still need to mint
137/// fresh variables, so they draw ids from this separate, process-wide
138/// counter instead.
139///
140/// This is purely cosmetic: variable *identity* is always pointer equality
141/// (`TyVarRef::same`/`RowVarRef::same`), never id equality, so the two
142/// counters can never collide in any way that affects correctness — at
143/// worst two unrelated variables print with the same debug id. Seeded far
144/// from `TypeContext`'s own counter to make that overlap unlikely in small
145/// examples.
146static FRESH_ID: AtomicU64 = AtomicU64::new(1 << 32);
147
148fn fresh_id() -> u64 {
149    FRESH_ID.fetch_add(1, Ordering::Relaxed)
150}
151
152// ============================================================================
153// Kind (mirrors v0.0.6's `mono_kind` / `FreeID_.kind`, types.cppo.ml:330-333)
154// ============================================================================
155
156/// The kind of a free type variable.
157///
158/// `Record(labels)` mirrors v0.0.6's `RecordKind`: it constrains a variable
159/// not yet known to be anything in particular, but which field access
160/// (`e#lbl`) has already shown must resolve to *some* record type
161/// containing (at least) `labels`. Unlike v0.0.6, which pairs each
162/// required label with its field type directly in the kind, this port
163/// stores only the label *names* here — the field types are tracked by
164/// the [`Row`] the variable eventually binds to (`unify::bind_var`'s
165/// `Kind::Record` branch). This loses nothing because a concrete record's
166/// structure here is *always* a first-class `Row`; v0.0.6 needed field
167/// types in the kind because its closed `RecordType` has no notion of
168/// "the type of label `l`" apart from the whole association list.
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub enum Kind {
171    Universal,
172    Record(BTreeSet<String>),
173}
174
175// ============================================================================
176// Type variables (mirrors v0.0.6's `FreeID_`/`mono_type_variable_info`,
177// types.cppo.ml:121-191, 347-349)
178// ============================================================================
179
180/// The mutable union-find cell behind a type variable.
181#[derive(Debug)]
182enum TyVarLink {
183    Free {
184        id: u64,
185        level: u32,
186        kind: Kind,
187    },
188    /// This variable has been unified with a concrete type; `resolve`
189    /// chases through this exactly like v0.0.6's `MonoLink`.
190    Bound(MonoType),
191}
192
193/// A reference-counted handle to a type variable's union-find cell. Cloning
194/// a `TyVarRef` shares the same cell (this is the union-find "pointer");
195/// identity (not structure) is what `unify` and `generalize` compare.
196#[derive(Clone, Debug)]
197pub struct TyVarRef(Rc<RefCell<TyVarLink>>);
198
199impl TyVarRef {
200    pub(crate) fn new(id: u64, level: u32, kind: Kind) -> Self {
201        TyVarRef(Rc::new(RefCell::new(TyVarLink::Free { id, level, kind })))
202    }
203
204    /// Identity comparison — the only correct notion of "same variable"
205    /// once links can be mutated in place.
206    pub fn same(&self, other: &TyVarRef) -> bool {
207        Rc::ptr_eq(&self.0, &other.0)
208    }
209
210    pub(crate) fn ptr_key(&self) -> usize {
211        Rc::as_ptr(&self.0) as usize
212    }
213
214    /// `None` if this variable has already been bound.
215    pub fn id(&self) -> Option<u64> {
216        match &*self.0.borrow() {
217            TyVarLink::Free { id, .. } => Some(*id),
218            TyVarLink::Bound(_) => None,
219        }
220    }
221
222    pub fn level(&self) -> Option<u32> {
223        match &*self.0.borrow() {
224            TyVarLink::Free { level, .. } => Some(*level),
225            TyVarLink::Bound(_) => None,
226        }
227    }
228
229    /// No-op if this variable has already been bound.
230    pub fn set_level(&self, new_level: u32) {
231        if let TyVarLink::Free { level, .. } = &mut *self.0.borrow_mut() {
232            *level = new_level;
233        }
234    }
235
236    /// `Kind::Universal` if this variable has already been bound (asking a
237    /// bound variable for its kind is meaningless; callers should `resolve`
238    /// first).
239    pub fn kind(&self) -> Kind {
240        match &*self.0.borrow() {
241            TyVarLink::Free { kind, .. } => kind.clone(),
242            TyVarLink::Bound(_) => Kind::Universal,
243        }
244    }
245
246    pub fn set_kind(&self, new_kind: Kind) {
247        if let TyVarLink::Free { kind, .. } = &mut *self.0.borrow_mut() {
248            *kind = new_kind;
249        }
250    }
251
252    /// Link this variable to a concrete type. Callers (`unify::bind_var`)
253    /// are responsible for the occurs check; this just performs the store.
254    pub fn bind(&self, ty: MonoType) {
255        *self.0.borrow_mut() = TyVarLink::Bound(ty);
256    }
257
258    pub fn is_free(&self) -> bool {
259        matches!(&*self.0.borrow(), TyVarLink::Free { .. })
260    }
261}
262
263impl PartialEq for TyVarRef {
264    fn eq(&self, other: &Self) -> bool {
265        self.same(other)
266    }
267}
268impl Eq for TyVarRef {}
269
270pub(crate) fn new_ty_var(level: u32) -> TyVarRef {
271    TyVarRef::new(fresh_id(), level, Kind::Universal)
272}
273
274// ============================================================================
275// Row variables — the tail of an extensible record row. Structurally a
276// mirror of `TyVarRef`/`TyVarLink`, except its "kind" is the set of labels
277// already known to appear in whatever row it resolves to (no `Universal`
278// case: an empty set already means "no labels required yet").
279// ============================================================================
280
281#[derive(Debug)]
282enum RowVarLink {
283    Free {
284        id: u64,
285        level: u32,
286        kind: BTreeSet<String>,
287    },
288    Bound(Row),
289}
290
291#[derive(Clone, Debug)]
292pub struct RowVarRef(Rc<RefCell<RowVarLink>>);
293
294impl RowVarRef {
295    pub(crate) fn new(id: u64, level: u32, kind: BTreeSet<String>) -> Self {
296        RowVarRef(Rc::new(RefCell::new(RowVarLink::Free { id, level, kind })))
297    }
298
299    pub fn same(&self, other: &RowVarRef) -> bool {
300        Rc::ptr_eq(&self.0, &other.0)
301    }
302
303    pub(crate) fn ptr_key(&self) -> usize {
304        Rc::as_ptr(&self.0) as usize
305    }
306
307    pub fn id(&self) -> Option<u64> {
308        match &*self.0.borrow() {
309            RowVarLink::Free { id, .. } => Some(*id),
310            RowVarLink::Bound(_) => None,
311        }
312    }
313
314    pub fn level(&self) -> Option<u32> {
315        match &*self.0.borrow() {
316            RowVarLink::Free { level, .. } => Some(*level),
317            RowVarLink::Bound(_) => None,
318        }
319    }
320
321    pub fn set_level(&self, new_level: u32) {
322        if let RowVarLink::Free { level, .. } = &mut *self.0.borrow_mut() {
323            *level = new_level;
324        }
325    }
326
327    pub fn kind(&self) -> BTreeSet<String> {
328        match &*self.0.borrow() {
329            RowVarLink::Free { kind, .. } => kind.clone(),
330            RowVarLink::Bound(_) => BTreeSet::new(),
331        }
332    }
333
334    pub fn set_kind(&self, new_kind: BTreeSet<String>) {
335        if let RowVarLink::Free { kind, .. } = &mut *self.0.borrow_mut() {
336            *kind = new_kind;
337        }
338    }
339
340    pub fn bind(&self, row: Row) {
341        *self.0.borrow_mut() = RowVarLink::Bound(row);
342    }
343
344    pub fn is_free(&self) -> bool {
345        matches!(&*self.0.borrow(), RowVarLink::Free { .. })
346    }
347}
348
349impl PartialEq for RowVarRef {
350    fn eq(&self, other: &Self) -> bool {
351        self.same(other)
352    }
353}
354impl Eq for RowVarRef {}
355
356pub(crate) fn new_row_var(level: u32) -> RowVarRef {
357    RowVarRef::new(fresh_id(), level, BTreeSet::new())
358}
359
360// ============================================================================
361// Monomorphic types
362// ============================================================================
363
364/// Which stage an expression is being read at (upstream's `stage`,
365/// `types.cppo.ml:400-403`).
366///
367/// SATySFi is a two-stage language: a document is typeset at **stage 1**, and
368/// **stage 0** is the earlier stage that can compute *code* to be run at stage
369/// 1. `&e` quotes (stage 0 -> a `code` value), `~e` splices (stage 1 -> runs
370/// `e` at stage 0 and drops its code in). **Persistent** bindings are the only
371/// ones nameable from both.
372///
373/// A 0.0.6 file declares its stage in its `@stage:` header and every binding in
374/// it takes that stage; a document is always stage 1.
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
376pub enum Stage {
377    /// `@stage: persistent` — usable from either stage.
378    Persistent0,
379    /// `@stage: 0` — the program stage, run before typesetting.
380    Stage0,
381    /// `@stage: 1` — the document stage, and what a document must be.
382    #[default]
383    Stage1,
384}
385
386impl Stage {
387    pub fn as_str(self) -> &'static str {
388        match self {
389            Stage::Persistent0 => "persistent stage",
390            Stage::Stage0 => "stage 0",
391            Stage::Stage1 => "stage 1",
392        }
393    }
394
395    /// Parse a `@stage:` header's value.
396    pub fn parse(s: &str) -> Option<Stage> {
397        match s.trim() {
398            "persistent" => Some(Stage::Persistent0),
399            "0" => Some(Stage::Stage0),
400            "1" => Some(Stage::Stage1),
401            _ => None,
402        }
403    }
404
405    /// May an expression being read at `self` name a binding introduced at
406    /// `bound`? The whole staging discipline for *occurrences*, as opposed to
407    /// the `&`/`~` operator rules — upstream's `UTContentOf` arm, whose
408    /// accepting cases are written out one by one and whose `_` fallthrough
409    /// raises `InvalidOccurrenceAsToStaging`:
410    ///
411    /// | use \ bind | `persistent` | `0` | `1` |
412    /// |------------|--------------|-----|-----|
413    /// | `persistent` | yes        | NO  | NO  |
414    /// | `0`          | yes        | yes | NO  |
415    /// | `1`          | yes (lift) | NO  | yes |
416    ///
417    /// So: a `persistent` binding is nameable from everywhere, every other
418    /// binding only from its own stage. The two generations agree on the
419    /// accept/reject split and differ only in which *node* an accepted
420    /// persistent occurrence compiles to — 0.0.6 emits `Persistent(rng, evid)`
421    /// for all three uses (`typechecker.ml:667-681`), `dev-0-1-0` only for the
422    /// `Stage1` use (`typechecker.ml:340-353`), against `ContentOf(rng, evid)`
423    /// everywhere else.
424    ///
425    /// **What that node does upstream, and why this port needs no counterpart.**
426    /// It is capture-avoidance bookkeeping for a FIRST-ORDER code value, not a
427    /// semantic distinction:
428    ///
429    /// * upstream's stage-1 pass (`interpret_1`, `evaluator.cppo.ml:429` in
430    ///   0.0.6 / `:609` on `dev-0-1-0`) does not evaluate — it BUILDS a
431    ///   `code_value`, minting a fresh `CodeSymbol` for every binder it walks
432    ///   under and resolving an ordinary `ContentOf` through `find_symbol`;
433    /// * a persistent binding is not one of those binders: it was already
434    ///   evaluated at stage 0 (`interpret_bindings_0`'s `Persistent0 | Stage0`
435    ///   arm, `dev-0-1-0 evaluator.cppo.ml:1177-1195`) and lives in the VALUE
436    ///   environment, so `find_symbol` would miss it and upstream would
437    ///   `report_bug_ast "symbol not found"`;
438    /// * hence `CdPersistent(rng, evid)`, carried through verbatim and mapped
439    ///   straight back to `ContentOf(rng, evid)` by `unlift_code`
440    ///   (`types.cppo.ml:1506` in 0.0.6, `:1340` on `dev-0-1-0`) — an ordinary
441    ///   environment lookup, by the SAME `EvalVarID` the typechecker resolved,
442    ///   once the generated code finally runs. (`bytecomp` does not implement
443    ///   it at all on `dev-0-1-0`: `ir.cppo.ml:565` is `failwith "TODO"`.)
444    ///
445    /// This port has no such pass and no renaming: a quote is a CLOSURE
446    /// (`compile.rs`'s `Ast::Next` — the compiled body paired with the
447    /// environment reaching it), and every free name in it was already resolved
448    /// against the scope the quote was WRITTEN in, a top-level binding to its
449    /// own `Globals` slot. That slot is this port's `EvalVarID`: it fixes the
450    /// reference to the BINDING rather than the name, which is the one property
451    /// `CdPersistent` exists to preserve. So the verdict below is the whole of
452    /// what is needed — pinned end to end, as values, by `tests/staging.rs`'s
453    /// "`(Stage1, Persistent0)` cell, as a VALUE" block and its 0.1 twins in
454    /// `tests/staging_v1.rs`.
455    pub fn can_reference(self, bound: Stage) -> bool {
456        matches!(
457            (self, bound),
458            (_, Stage::Persistent0) | (Stage::Stage0, Stage::Stage0) | (Stage::Stage1, Stage::Stage1)
459        )
460    }
461}
462
463/// A monomorphic type. Mirrors v0.0.6's `mono_type` (the `type_main`
464/// variant instantiated at `mono_type_variable_info ref`), minus
465/// `SynonymType` (no type synonyms in this port) and with `Row`-based
466/// records instead of a closed `RecordType` (see [`Row`]).
467///
468/// The `#[subast]` list names every *other* type in this family reachable
469/// from a field; see [`crate::visit`] for what the generated traversal
470/// covers and — importantly — what it deliberately does not.
471///
472/// [`MonoType::Var`]'s [`TyVarRef`] is **not** listed, so the traversal
473/// treats a type variable as a leaf and never follows a `Bound` link. That
474/// is not an oversight: see [`crate::visit`].
475#[derive(Clone, Debug, syan::visit::Ast)]
476#[subast(crate::types::Row, crate::types::CmdArgType)]
477pub enum MonoType {
478    Var(TyVarRef),
479    Base(BaseType),
480    /// `?(row) dom -> cod` — a function type carrying a labeled
481    /// optional-argument [`Row`] (upstream `FuncType of row * typ * typ`,
482    /// SATySFi 0.1). The row is `Row::Empty` for every 0.0.6-constructed
483    /// function ([`crate::prim_types::arrow`]), printing nothing and
484    /// unifying trivially, so 0.0.6 behavior is byte-identical. A
485    /// non-empty row (`Cons(label, option-payload-type, …)`) records the
486    /// value-level `?(l = e)` labeled optional arguments the function
487    /// accepts. The field is **positional** (no `..` in any destructure)
488    /// deliberately: widening this variant makes the compiler flag every
489    /// match site, guarding against a silently-dropped row in the
490    /// sealed-module subsumption path.
491    ///
492    /// The row is **boxed** (`Box<Row>`, not inline) so widening `Func`
493    /// does not enlarge `MonoType` itself: `Row` is a ~40-byte enum, and
494    /// inlining it would make `Func` the largest variant, growing every
495    /// stack frame holding a `MonoType` by value enough to tip a deep
496    /// recursive typecheck over the default stack. `Box<Row>` keeps
497    /// `MonoType` at its pre-widening size, so 0.0.6 stack usage is
498    /// unchanged.
499    Func(Box<Row>, Box<MonoType>, Box<MonoType>),
500    /// A tuple type, always with at least two elements.
501    Product(Vec<MonoType>),
502    List(Box<MonoType>),
503    Ref(Box<MonoType>),
504    Record(Row),
505    /// A user-defined variant type applied to its arguments, e.g.
506    /// `Variant("option", [int])` for `int option`. Identified by name
507    /// rather than by a fresh `TypeID.t` as in v0.0.6 (`types.cppo.ml:318`)
508    /// — this port has no notion of shadowing/re-declaring a variant
509    /// type under the same name within one compilation, so a `String` is
510    /// a simpler stand-in for v0.0.6's globally-fresh `TypeID.t`.
511    Variant(String, Vec<MonoType>),
512    /// `code ty` — the type of a quoted (`&e`) fragment awaiting the next
513    /// stage. Upstream's `CodeType` (`types.cppo.ml:324`). Structurally it
514    /// behaves exactly like [`MonoType::Ref`]: one covariant argument,
515    /// unified pointwise.
516    Code(Box<MonoType>),
517    /// `[...] inline-cmd` (v0.0.6: `HorzCommandType`).
518    InlineCmd(Vec<CmdArgType>),
519    /// `[...] block-cmd` (v0.0.6: `VertCommandType`).
520    BlockCmd(Vec<CmdArgType>),
521    /// `[...] math-cmd` (v0.0.6: `MathCommandType`).
522    MathCmd(Vec<CmdArgType>),
523}
524
525/// One command argument type: `ty` for a mandatory argument, or `ty?` for
526/// an optional one (v0.0.6: `MandatoryArgumentType` / `OptionalArgumentType`,
527/// types.cppo.ml:326-328). `optional`/`opt_labels` are version-discriminated
528/// by construction: under `V0_0`
529/// (positional model) `optional` marks a whole-slot `ty?` optional and
530/// `opt_labels` is always empty; under `V0_1` (labeled model, upstream
531/// `CommandArgType of typ LabelMap.t * typ`, `types.cppo.ml:214`) `optional`
532/// is always `false` and `opt_labels` carries this slot's `?(l:τ,…)` bundle —
533/// a CLOSED map (no row variable: upstream discards one if written,
534/// `parser.mly:866`'s `TODO (error)`). Kept **sorted by label** at every
535/// producer (`command_scheme`'s harvest, `lower_type_atom`'s sig lowering) so
536/// `unify`/`Display`/sealing are order-insensitive — see `unify_cmd_args`'s
537/// zip-equal equal-domain test.
538///
539/// See [`MonoType`] for what the `#[subast]` list means. `opt_labels` is the
540/// field four hand-written walks forgot; the generated traversal cannot.
541#[derive(Clone, Debug, syan::visit::Ast)]
542#[subast(crate::types::MonoType)]
543pub struct CmdArgType {
544    pub optional: bool,
545    pub opt_labels: Vec<(String, MonoType)>,
546    pub ty: MonoType,
547}
548
549/// An extensible record row: a sequence of `label : type` bindings ending
550/// either in `Empty` (a *closed* record — exactly these labels and no
551/// others) or in `Var` (an *open* record — at least these labels, plus
552/// whatever the row variable's eventual binding adds).
553///
554/// **Deviation from v0.0.6**: its `RecordType` (types.cppo.ml:319) is
555/// always closed; the only record polymorphism is indirect, via a plain
556/// type variable carrying a `RecordKind` (a label-typed lower bound) that
557/// unifies against a closed `RecordType` when the kind's labels are a
558/// subset of the record's (typechecker.ml:480-500,
559/// `Assoc.domain_included`) — which cannot express an open record type
560/// standing on its own (only a variable can be "open"). Giving rows their
561/// own recursive type former (`Row::Cons`/`Var`/`Empty`, Rémy-style row
562/// polymorphism) is strictly more general and lets `unify` do genuine
563/// label-subsumption with a *remainder* row variable
564/// (`unify::row_extract`). `Kind::Record` is kept for the one case v0.0.6
565/// also has it for: a variable not yet known to be a record at all.
566///
567/// See [`MonoType`] for what the `#[subast]` list means. As with
568/// `MonoType::Var`, [`RowVarRef`] is not listed and a row variable is a leaf.
569#[derive(Clone, Debug, syan::visit::Ast)]
570#[subast(crate::types::MonoType)]
571pub enum Row {
572    Empty,
573    Var(RowVarRef),
574    Cons(String, Box<MonoType>, Box<Row>),
575}
576
577// ============================================================================
578// resolve / shallow_follow — chase `Bound` links, union-find "find".
579// ============================================================================
580
581/// Follow `Var(_)` → `Bound(ty)` links until reaching either a free
582/// variable or a non-variable type. Does **not** recurse into the
583/// structure of compound types (that's what makes it "shallow": a `Func`
584/// whose domain is itself a bound variable is returned as-is, domain still
585/// unresolved) — callers that need a fully dereferenced tree should
586/// `resolve` again at each level as they recurse, which is exactly what
587/// `unify` and `Display` do.
588///
589/// # Why `Cow`
590///
591/// This is the hottest function in the typechecker — ~316k calls on a corpus
592/// document, and its cost is dominated by COPYING types, not by following
593/// links. Do NOT make it return an owned `MonoType`: that makes the common
594/// case (the argument is already resolved — not a variable, or a free one)
595/// end in `ty.clone()`, a full deep copy produced solely to hand back an owned
596/// value the caller then only inspects. Measured, that pointless tail copy was
597/// **87-89% of all type nodes cloned during typechecking**, and typecheck time
598/// tracks cloned-node volume near-linearly across the whole corpus.
599///
600/// So the common case borrows. Only the link-following path allocates,
601/// and only because the `Bound` payload lives behind a `RefCell` whose guard
602/// cannot outlive this frame. Callers that just match on the result want
603/// `&*resolve(..)`; the few that keep it want `.into_owned()`.
604pub fn resolve(ty: &MonoType) -> Cow<'_, MonoType> {
605    if let MonoType::Var(v) = ty {
606        let next = match &*v.0.borrow() {
607            TyVarLink::Bound(inner) => Some(inner.clone()),
608            TyVarLink::Free { .. } => None,
609        };
610        if let Some(inner) = next {
611            return Cow::Owned(resolve(&inner).into_owned());
612        }
613    }
614    Cow::Borrowed(ty)
615}
616
617/// The row analogue of [`resolve`], `Cow` for the same reason.
618pub fn resolve_row(row: &Row) -> Cow<'_, Row> {
619    if let Row::Var(v) = row {
620        let next = match &*v.0.borrow() {
621            RowVarLink::Bound(inner) => Some(inner.clone()),
622            RowVarLink::Free { .. } => None,
623        };
624        if let Some(inner) = next {
625            return Cow::Owned(resolve_row(&inner).into_owned());
626        }
627    }
628    Cow::Borrowed(row)
629}
630
631// ============================================================================
632// Polymorphic types and level-based generalization
633// ============================================================================
634
635/// A type scheme: a monomorphic body plus the set of that body's free
636/// variables which are quantified over it.
637///
638/// **Deviation from v0.0.6**: v0.0.6 (types.cppo.ml:351-364) converts a
639/// generalized variable's `MonoFree` cell into a `PolyBound` id, so the
640/// same physical type reads differently as "a mono type" vs "a poly type",
641/// and instantiating rebuilds `PolyBound` occurrences into fresh
642/// `MonoFree` cells. This port instead keeps quantified variables as
643/// ordinary (still-`Free`) `TyVarRef`/`RowVarRef` cells and just remembers
644/// which ones they are (`vars`/`row_vars` below); `instantiate` deep-copies
645/// `body`, replacing each remembered variable (by pointer identity) with a
646/// fresh one and leaving everything else shared untouched. This is the
647/// standard "generalization via levels" technique, and it replaces
648/// v0.0.6's `quantifiability` flag (`Quantifiable`/`Unquantifiable`,
649/// types.cppo.ml:54) — which guards against generalizing a variable
650/// unification already linked outside the current let binding — with a
651/// consequence of levels instead: a variable unification touches from an
652/// outer scope gets its level lowered (`unify::occurs_var`/
653/// `occurs_var_in_row`), so by the time `generalize` runs it no longer
654/// looks "deep enough" to quantify.
655#[derive(Clone, Debug)]
656pub struct PolyType {
657    vars: Vec<TyVarRef>,
658    row_vars: Vec<RowVarRef>,
659    body: MonoType,
660}
661
662impl PolyType {
663    /// A trivial scheme with no quantified variables at all.
664    pub fn mono(ty: MonoType) -> PolyType {
665        PolyType {
666            vars: Vec::new(),
667            row_vars: Vec::new(),
668            body: ty,
669        }
670    }
671
672    /// Build a scheme by hand, explicitly naming which variables (which
673    /// must occur free in `body`) are quantified. Used by `prim_types`,
674    /// which constructs polymorphic primitive signatures (`::`, `!`)
675    /// directly rather than via `generalize` (there is no enclosing
676    /// inference level to generalize *from* at primitive-table
677    /// construction time).
678    pub(crate) fn from_vars(
679        vars: Vec<TyVarRef>,
680        row_vars: Vec<RowVarRef>,
681        body: MonoType,
682    ) -> PolyType {
683        PolyType {
684            vars,
685            row_vars,
686            body,
687        }
688    }
689
690    /// The scheme's body, before instantiation — exposed for inspection
691    /// (e.g. arity-checking) without needing to mint fresh variables.
692    pub fn body(&self) -> &MonoType {
693        &self.body
694    }
695
696    pub fn is_monomorphic(&self) -> bool {
697        self.vars.is_empty() && self.row_vars.is_empty()
698    }
699}
700
701impl fmt::Display for PolyType {
702    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703        fmt::Display::fmt(&self.body, f)
704    }
705}
706
707/// Per-inference-run state: the level stack for generalization, and a
708/// counter for fresh variable ids (see `FRESH_ID`'s doc comment for why
709/// `instantiate`/`unify` use a *different* counter than this one — the two
710/// never need to agree, since identity is always by pointer).
711pub struct TypeContext {
712    next_id: u64,
713    level: u32,
714}
715
716impl TypeContext {
717    pub fn new() -> Self {
718        TypeContext {
719            next_id: 0,
720            level: 0,
721        }
722    }
723
724    fn next_id(&mut self) -> u64 {
725        let id = self.next_id;
726        self.next_id += 1;
727        id
728    }
729
730    pub fn level(&self) -> u32 {
731        self.level
732    }
733
734    /// Enter a new `let`-nesting level. Call before inferring the
735    /// right-hand side of a `let`.
736    pub fn enter_level(&mut self) {
737        self.level += 1;
738    }
739
740    /// Leave the current level. Call after inferring the right-hand side
741    /// of a `let`, before calling `generalize`.
742    pub fn leave_level(&mut self) {
743        self.level -= 1;
744    }
745
746    pub fn fresh_var(&mut self) -> TyVarRef {
747        self.fresh_var_with_kind(Kind::Universal)
748    }
749
750    pub fn fresh_var_with_kind(&mut self, kind: Kind) -> TyVarRef {
751        TyVarRef::new(self.next_id(), self.level, kind)
752    }
753
754    pub fn fresh_row_var(&mut self) -> RowVarRef {
755        self.fresh_row_var_with_kind(BTreeSet::new())
756    }
757
758    pub fn fresh_row_var_with_kind(&mut self, kind: BTreeSet<String>) -> RowVarRef {
759        RowVarRef::new(self.next_id(), self.level, kind)
760    }
761}
762
763impl Default for TypeContext {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769/// Quantify every free variable in `ty` whose level is deeper than `level`
770/// (i.e. was created after entering the let binding being generalized).
771/// Typical usage:
772///
773/// ```ignore
774/// ctx.enter_level();
775/// let ty = infer(ctx, rhs)?;
776/// ctx.leave_level();
777/// let scheme = generalize(ctx.level(), &ty);
778/// ```
779pub fn generalize(level: u32, ty: &MonoType) -> PolyType {
780    let mut vars = Vec::new();
781    let mut row_vars = Vec::new();
782    collect_generalizable(level, ty, &mut vars, &mut row_vars);
783    PolyType {
784        vars,
785        row_vars,
786        body: ty.clone(),
787    }
788}
789
790fn collect_generalizable(
791    level: u32,
792    ty: &MonoType,
793    vars: &mut Vec<TyVarRef>,
794    row_vars: &mut Vec<RowVarRef>,
795) {
796    match &*resolve(ty) {
797        MonoType::Var(v) => {
798            if let Some(lv) = v.level() {
799                if lv > level && !vars.iter().any(|x| x.same(v)) {
800                    vars.push(v.clone());
801                }
802            }
803        }
804        MonoType::Base(_) => {}
805        MonoType::Func(row, a, b) => {
806            collect_generalizable_row(level, &row, vars, row_vars);
807            collect_generalizable(level, &a, vars, row_vars);
808            collect_generalizable(level, &b, vars, row_vars);
809        }
810        MonoType::Product(ts) => {
811            for t in ts {
812                collect_generalizable(level, t, vars, row_vars);
813            }
814        }
815        MonoType::List(t) | MonoType::Ref(t) | MonoType::Code(t) => {
816            collect_generalizable(level, &t, vars, row_vars)
817        }
818        MonoType::Record(row) => collect_generalizable_row(level, &row, vars, row_vars),
819        MonoType::Variant(_, args) => {
820            for t in args {
821                collect_generalizable(level, t, vars, row_vars);
822            }
823        }
824        MonoType::InlineCmd(cs) | MonoType::BlockCmd(cs) | MonoType::MathCmd(cs) => {
825            for c in cs {
826                collect_generalizable(level, &c.ty, vars, row_vars);
827                for (_, lty) in &c.opt_labels {
828                    collect_generalizable(level, lty, vars, row_vars);
829                }
830            }
831        }
832    }
833}
834
835fn collect_generalizable_row(
836    level: u32,
837    row: &Row,
838    vars: &mut Vec<TyVarRef>,
839    row_vars: &mut Vec<RowVarRef>,
840) {
841    match &*resolve_row(row) {
842        Row::Empty => {}
843        Row::Var(v) => {
844            if let Some(lv) = v.level() {
845                if lv > level && !row_vars.iter().any(|x| x.same(v)) {
846                    row_vars.push(v.clone());
847                }
848            }
849        }
850        Row::Cons(_, t, rest) => {
851            collect_generalizable(level, &t, vars, row_vars);
852            collect_generalizable_row(level, &rest, vars, row_vars);
853        }
854    }
855}
856
857/// Instantiate a scheme: replace every quantified variable with a fresh
858/// one at `level`, leaving everything else in the body shared as-is.
859///
860/// This takes no `&mut TypeContext` (per this module's contract) — see
861/// `FRESH_ID`'s doc comment for how it still mints fresh, correctly
862/// leveled variables.
863pub fn instantiate(poly: &PolyType, level: u32) -> MonoType {
864    let mut var_map: HashMap<usize, MonoType> = HashMap::new();
865    for v in &poly.vars {
866        let fresh = TyVarRef::new(fresh_id(), level, v.kind());
867        var_map.insert(v.ptr_key(), MonoType::Var(fresh));
868    }
869    let mut row_map: HashMap<usize, Row> = HashMap::new();
870    for v in &poly.row_vars {
871        let fresh = RowVarRef::new(fresh_id(), level, v.kind());
872        row_map.insert(v.ptr_key(), Row::Var(fresh));
873    }
874    substitute(&poly.body, &var_map, &row_map)
875}
876
877/// Deep-copy `ty`, replacing any (resolved) variable found in `var_map`/
878/// `row_map` by pointer identity with its mapped replacement, and cloning
879/// everything else structurally. Shared by `instantiate` (mapping
880/// quantified variables to fresh ones) and by `prim_types::VariantDecl`
881/// (mapping a declaration's parameter placeholders to the concrete
882/// arguments of one particular constructor application).
883pub(crate) fn substitute(
884    ty: &MonoType,
885    var_map: &HashMap<usize, MonoType>,
886    row_map: &HashMap<usize, Row>,
887) -> MonoType {
888    match &*resolve(ty) {
889        MonoType::Var(v) => var_map
890            .get(&v.ptr_key())
891            .cloned()
892            .unwrap_or_else(|| MonoType::Var(v.clone())),
893        MonoType::Base(b) => MonoType::Base(*b),
894        MonoType::Func(row, a, b) => MonoType::Func(
895            Box::new(substitute_row(&row, var_map, row_map)),
896            Box::new(substitute(&a, var_map, row_map)),
897            Box::new(substitute(&b, var_map, row_map)),
898        ),
899        MonoType::Product(ts) => {
900            MonoType::Product(ts.iter().map(|t| substitute(t, var_map, row_map)).collect())
901        }
902        MonoType::List(t) => MonoType::List(Box::new(substitute(&t, var_map, row_map))),
903        MonoType::Ref(t) => MonoType::Ref(Box::new(substitute(&t, var_map, row_map))),
904        MonoType::Code(t) => MonoType::Code(Box::new(substitute(&t, var_map, row_map))),
905        MonoType::Record(row) => MonoType::Record(substitute_row(&row, var_map, row_map)),
906        MonoType::Variant(name, args) => MonoType::Variant(
907            name.clone(),
908            args.iter()
909                .map(|t| substitute(t, var_map, row_map))
910                .collect(),
911        ),
912        MonoType::InlineCmd(cs) => MonoType::InlineCmd(substitute_cmd_args(&cs, var_map, row_map)),
913        MonoType::BlockCmd(cs) => MonoType::BlockCmd(substitute_cmd_args(&cs, var_map, row_map)),
914        MonoType::MathCmd(cs) => MonoType::MathCmd(substitute_cmd_args(&cs, var_map, row_map)),
915    }
916}
917
918pub(crate) fn substitute_row(
919    row: &Row,
920    var_map: &HashMap<usize, MonoType>,
921    row_map: &HashMap<usize, Row>,
922) -> Row {
923    match &*resolve_row(row) {
924        Row::Empty => Row::Empty,
925        Row::Var(v) => row_map
926            .get(&v.ptr_key())
927            .cloned()
928            .unwrap_or_else(|| Row::Var(v.clone())),
929        Row::Cons(label, t, rest) => Row::Cons(
930            label.clone(),
931            Box::new(substitute(&t, var_map, row_map)),
932            Box::new(substitute_row(&rest, var_map, row_map)),
933        ),
934    }
935}
936
937fn substitute_cmd_args(
938    cs: &[CmdArgType],
939    var_map: &HashMap<usize, MonoType>,
940    row_map: &HashMap<usize, Row>,
941) -> Vec<CmdArgType> {
942    cs.iter()
943        .map(|c| CmdArgType {
944            optional: c.optional,
945            opt_labels: c
946                .opt_labels
947                .iter()
948                .map(|(l, t)| (l.clone(), substitute(t, var_map, row_map)))
949                .collect(),
950            ty: substitute(&c.ty, var_map, row_map),
951        })
952        .collect()
953}
954
955pub(crate) fn ptr_key(v: &TyVarRef) -> usize {
956    v.ptr_key()
957}
958
959// ============================================================================
960// Display — a SATySFi-syntax-ish pretty printer for error messages.
961//
962// Intentionally not byte-for-byte identical to v0.0.6's own printer
963// (`display.ml`); it exists to make unification errors readable, with a
964// simple parenthesization convention: atoms never need parens;
965// `list`/`ref`/single-argument variants are postfix and only parenthesize
966// a compound (function/product) operand; a function's codomain is
967// parenthesized whenever it isn't itself an atom — so `int -> (string
968// list)` gets parens around the list even though `list` binds tighter
969// than `->` (there's no source-level ambiguity; it's purely for
970// readability).
971// ============================================================================
972
973struct VarNamer {
974    names: HashMap<usize, String>,
975    next: usize,
976}
977
978impl VarNamer {
979    fn new() -> Self {
980        VarNamer {
981            names: HashMap::new(),
982            next: 0,
983        }
984    }
985
986    fn name_for(&mut self, key: usize) -> String {
987        if let Some(n) = self.names.get(&key) {
988            return n.clone();
989        }
990        let n = Self::letter(self.next);
991        self.next += 1;
992        self.names.insert(key, n.clone());
993        n
994    }
995
996    fn letter(i: usize) -> String {
997        let letter = (b'a' + (i % 26) as u8) as char;
998        let suffix = i / 26;
999        if suffix == 0 {
1000            format!("'{letter}")
1001        } else {
1002            format!("'{letter}{suffix}")
1003        }
1004    }
1005}
1006
1007fn is_atomic(ty: &MonoType) -> bool {
1008    match ty {
1009        MonoType::Base(_) | MonoType::Var(_) | MonoType::Record(_) => true,
1010        MonoType::Variant(_, args) => args.is_empty(),
1011        MonoType::Func(_, _, _)
1012        | MonoType::Product(_)
1013        | MonoType::List(_)
1014        | MonoType::Ref(_)
1015        | MonoType::Code(_)
1016        | MonoType::InlineCmd(_)
1017        | MonoType::BlockCmd(_)
1018        | MonoType::MathCmd(_) => false,
1019    }
1020}
1021
1022fn needs_parens_as_operand(ty: &MonoType) -> bool {
1023    matches!(ty, MonoType::Func(_, _, _) | MonoType::Product(_))
1024}
1025
1026fn fmt_operand(ty: &MonoType, f: &mut fmt::Formatter<'_>, namer: &mut VarNamer) -> fmt::Result {
1027    if needs_parens_as_operand(&resolve(ty)) {
1028        f.write_str("(")?;
1029        fmt_mono(ty, f, namer)?;
1030        f.write_str(")")
1031    } else {
1032        fmt_mono(ty, f, namer)
1033    }
1034}
1035
1036fn fmt_mono(ty: &MonoType, f: &mut fmt::Formatter<'_>, namer: &mut VarNamer) -> fmt::Result {
1037    let ty = resolve(ty);
1038    match &*ty {
1039        MonoType::Var(v) => write!(f, "{}", namer.name_for(v.ptr_key())),
1040        MonoType::Base(b) => write!(f, "{b}"),
1041        MonoType::Func(row, dom, cod) => {
1042            fmt_func_row(row, f, namer)?;
1043            fmt_operand(dom, f, namer)?;
1044            f.write_str(" -> ")?;
1045            let rcod = resolve(cod);
1046            if is_atomic(&rcod) {
1047                fmt_mono(cod, f, namer)
1048            } else {
1049                f.write_str("(")?;
1050                fmt_mono(cod, f, namer)?;
1051                f.write_str(")")
1052            }
1053        }
1054        MonoType::Product(ts) => {
1055            for (i, t) in ts.iter().enumerate() {
1056                if i > 0 {
1057                    f.write_str(" * ")?;
1058                }
1059                fmt_operand(t, f, namer)?;
1060            }
1061            Ok(())
1062        }
1063        MonoType::List(t) => fmt_postfix(t, "list", f, namer),
1064        MonoType::Ref(t) => fmt_postfix(t, "ref", f, namer),
1065        MonoType::Code(t) => fmt_postfix(t, "code", f, namer),
1066        MonoType::Record(row) => fmt_row(row, f, namer),
1067        MonoType::Variant(name, args) => match args.as_slice() {
1068            [] => write!(f, "{name}"),
1069            [one] => fmt_postfix(one, name, f, namer),
1070            many => {
1071                f.write_str("(")?;
1072                for (i, t) in many.iter().enumerate() {
1073                    if i > 0 {
1074                        f.write_str(", ")?;
1075                    }
1076                    fmt_mono(t, f, namer)?;
1077                }
1078                write!(f, ") {name}")
1079            }
1080        },
1081        MonoType::InlineCmd(cs) => fmt_cmd(cs, "inline-cmd", f, namer),
1082        MonoType::BlockCmd(cs) => fmt_cmd(cs, "block-cmd", f, namer),
1083        MonoType::MathCmd(cs) => fmt_cmd(cs, "math-cmd", f, namer),
1084    }
1085}
1086
1087fn fmt_postfix(
1088    operand: &MonoType,
1089    suffix: &str,
1090    f: &mut fmt::Formatter<'_>,
1091    namer: &mut VarNamer,
1092) -> fmt::Result {
1093    fmt_operand(operand, f, namer)?;
1094    write!(f, " {suffix}")
1095}
1096
1097fn fmt_cmd(
1098    cs: &[CmdArgType],
1099    suffix: &str,
1100    f: &mut fmt::Formatter<'_>,
1101    namer: &mut VarNamer,
1102) -> fmt::Result {
1103    f.write_str("[")?;
1104    for (i, c) in cs.iter().enumerate() {
1105        if i > 0 {
1106            f.write_str("; ")?;
1107        }
1108        fmt_opt_labels(&c.opt_labels, f, namer)?;
1109        fmt_mono(&c.ty, f, namer)?;
1110        if c.optional {
1111            f.write_str("?")?;
1112        }
1113    }
1114    write!(f, "] {suffix}")
1115}
1116
1117/// Prefix-print a command argument slot's closed optional-label map (0.1's
1118/// `CmdArgType.opt_labels`): `?(l : τ, …) ` before the slot's mandatory `ty`,
1119/// or nothing at all when the map is empty (guaranteeing byte-identical
1120/// output for every 0.0.6-reachable `CmdArgType`, since those are always
1121/// `opt_labels == []`) — the command-type analogue of `fmt_func_row`, minus
1122/// the row-variable tail (command optional maps are closed, never open).
1123fn fmt_opt_labels(
1124    labels: &[(String, MonoType)],
1125    f: &mut fmt::Formatter<'_>,
1126    namer: &mut VarNamer,
1127) -> fmt::Result {
1128    if labels.is_empty() {
1129        return Ok(());
1130    }
1131    let mut fields = labels.to_vec();
1132    fields.sort_by(|a, b| a.0.cmp(&b.0));
1133    f.write_str("?(")?;
1134    for (i, (label, ty)) in fields.iter().enumerate() {
1135        if i > 0 {
1136            f.write_str(", ")?;
1137        }
1138        write!(f, "{label} : ")?;
1139        fmt_mono(ty, f, namer)?;
1140    }
1141    f.write_str(") ")
1142}
1143
1144/// Prefix-print a function type's optional-argument row: nothing at all for
1145/// an empty (0.0.6) row — guaranteeing byte-identical output — or `?(l : τ,
1146/// …) ` (a free-var tail adding `| ?'rN`) for a non-empty 0.1 row.
1147fn fmt_func_row(row: &Row, f: &mut fmt::Formatter<'_>, namer: &mut VarNamer) -> fmt::Result {
1148    let mut fields: Vec<(String, MonoType)> = Vec::new();
1149    let mut cur = resolve_row(row).into_owned();
1150    let tail_name = loop {
1151        match cur {
1152            Row::Empty => break None,
1153            Row::Var(v) => break Some(namer.name_for(v.ptr_key())),
1154            Row::Cons(label, ty, rest) => {
1155                fields.push((label, *ty));
1156                cur = resolve_row(&rest).into_owned();
1157            }
1158        }
1159    };
1160    if fields.is_empty() && tail_name.is_none() {
1161        return Ok(());
1162    }
1163    fields.sort_by(|a, b| a.0.cmp(&b.0));
1164    f.write_str("?(")?;
1165    for (i, (label, ty)) in fields.iter().enumerate() {
1166        if i > 0 {
1167            f.write_str(", ")?;
1168        }
1169        write!(f, "{label} : ")?;
1170        fmt_mono(ty, f, namer)?;
1171    }
1172    if let Some(name) = tail_name {
1173        if !fields.is_empty() {
1174            f.write_str(" ")?;
1175        }
1176        write!(f, "| ?{name}")?;
1177    }
1178    f.write_str(") ")
1179}
1180
1181fn fmt_row(row: &Row, f: &mut fmt::Formatter<'_>, namer: &mut VarNamer) -> fmt::Result {
1182    let mut fields: Vec<(String, MonoType)> = Vec::new();
1183    let mut cur = resolve_row(row).into_owned();
1184    let tail_name = loop {
1185        match cur {
1186            Row::Empty => break None,
1187            Row::Var(v) => break Some(namer.name_for(v.ptr_key())),
1188            Row::Cons(label, ty, rest) => {
1189                fields.push((label, *ty));
1190                cur = resolve_row(&rest).into_owned();
1191            }
1192        }
1193    };
1194    fields.sort_by(|a, b| a.0.cmp(&b.0));
1195    f.write_str("(| ")?;
1196    for (i, (label, ty)) in fields.iter().enumerate() {
1197        if i > 0 {
1198            f.write_str("; ")?;
1199        }
1200        write!(f, "{label} : ")?;
1201        fmt_mono(ty, f, namer)?;
1202    }
1203    if let Some(name) = tail_name {
1204        if !fields.is_empty() {
1205            f.write_str(" ")?;
1206        }
1207        write!(f, "| {name}")?;
1208    }
1209    f.write_str(" |)")
1210}
1211
1212impl fmt::Display for MonoType {
1213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1214        let mut namer = VarNamer::new();
1215        fmt_mono(self, f, &mut namer)
1216    }
1217}
1218
1219impl fmt::Display for Row {
1220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1221        let mut namer = VarNamer::new();
1222        fmt_row(self, f, &mut namer)
1223    }
1224}