rustyfi_lang/typecheck.rs
1//! The Hindley–Milner type inferencer: walks an
2//! [`crate::elaborate::Program`] and reports the first type error it finds,
3//! mirroring v0.0.6's `typecheck`/`typecheck_sub`
4//! (`src/frontend/typechecker.ml`) — unification itself lives in
5//! `crate::unify`, generalization/instantiation in `crate::types`, this
6//! module only walks the AST applying those primitives at each rule, exactly
7//! as `typechecker.ml` does over its own `unify`/`Typeenv`.
8//!
9//! This is validation only: `typecheck` returns `Result<(), TypeError>` and
10//! never touches the untyped evaluator.
11//!
12//! **Deviations from v0.0.6 and permissive corners** are called out inline
13//! at each rule with a `PERMISSIVE:` comment. The short version: math-mode
14//! command/embed typing, and unbound type-name/type-variable references
15//! inside a `type` declaration's payload, are accepted with a fresh/nominal
16//! stand-in type rather than rejected.
17
18use crate::ast::branded::{Ast, BText, CmdArg, IText, MathElem, Pattern};
19use crate::elaborate::{Program, UserSynonymDecl, UserTypeDecl};
20pub use crate::exhaustive::MatchWarning;
21use crate::prim_types::{
22 self, arrow, builtin_variants_with_version, labeled, list, mandatory, optional, product, reff,
23 t_block_boxes, t_block_text, t_bool, t_context, t_deco, t_decoset, t_document, t_float,
24 t_font_key, t_graphics, t_image, t_inline_boxes, t_inline_text, t_int, t_length, t_math_boxes,
25 t_math_text, t_option, t_paren, t_path, t_prepath, t_string, t_unit, VariantDecl,
26};
27use crate::symbol::{Symbol, SymbolStore};
28use crate::types::{
29 self, generalize, instantiate, resolve, resolve_row, BaseType, CmdArgType, Kind, MonoType,
30 PolyType, Row, Stage, TypeContext,
31};
32use crate::unify::{unify, UnifyError};
33use rustyfi_syntax::cst::ast::{CmdTypeKind, TypeApp, TypeAtom, TypeExpr, TypeProd};
34use rustyfi_syntax::cst::{RecordKind, SigConstraint, SigItem};
35use rustyfi_syntax::span::Span;
36use rustyfi_syntax::RustyfiVersion;
37use std::collections::{BTreeSet, HashMap};
38use std::fmt;
39use std::rc::Rc;
40
41// ============================================================================
42// Errors
43// ============================================================================
44
45/// A type error: a best-effort span (see `ast.rs`'s module doc comment — only
46/// `Var`/command/embed nodes carry spans, so most rules fall back to `None`),
47/// a "while typing …" context message, and — for anything that actually came
48/// from a failed [`unify`] call — the [`UnifyError`] itself, whose `Display`
49/// already renders both types involved.
50#[derive(Debug)]
51pub struct TypeError {
52 pub span: Option<Span>,
53 pub message: String,
54 pub source: Option<UnifyError>,
55}
56
57impl TypeError {
58 fn from_unify(span: Option<Span>, what: impl Into<String>, source: UnifyError) -> TypeError {
59 TypeError {
60 span,
61 message: format!("while typing {}", what.into()),
62 source: Some(source),
63 }
64 }
65
66 fn simple(span: Option<Span>, message: impl Into<String>) -> TypeError {
67 TypeError {
68 span,
69 message: message.into(),
70 source: None,
71 }
72 }
73}
74
75impl fmt::Display for TypeError {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self.span {
78 Some(span) => write!(f, "{span}: {}", self.message)?,
79 None => write!(f, "{}", self.message)?,
80 }
81 if let Some(src) = &self.source {
82 write!(f, ": {src}")?;
83 }
84 Ok(())
85 }
86}
87
88impl std::error::Error for TypeError {
89 fn source<'s>(&self) -> Option<&(dyn std::error::Error + 'static)> {
90 self.source
91 .as_ref()
92 .map(|e| e as &(dyn std::error::Error + 'static))
93 }
94}
95
96// ============================================================================
97// The primitive name table.
98//
99// `prim_types::primitive_type` is a pure name -> scheme lookup with no way to
100// enumerate its own domain, and `primitives.rs`'s `PRIM_DEFS` table (the
101// actual source of truth) is private to that module, so this list is
102// hand-kept in sync and cross-checked against `primitives.rs`'s source text
103// by a test (`tests/typecheck.rs`) rather than derived mechanically. It
104// matches `types_unify.rs`'s `every_registered_primitive_has_a_type` test's
105// own `NAMES` list.
106// ============================================================================
107
108pub const PRIMITIVE_NAMES: &[&str] = &[
109 "read-inline",
110 "read-block",
111 "line-break",
112 "page-break",
113 "page-break-multicolumn",
114 "page-break-two-column",
115 "+",
116 "-",
117 "*",
118 "/",
119 "mod",
120 "==",
121 "<>",
122 "<",
123 ">",
124 "<=",
125 ">=",
126 "&&",
127 "||",
128 "not",
129 "+.",
130 "-.",
131 "*.",
132 "/.",
133 "float",
134 "round",
135 "+'",
136 "-'",
137 "*'",
138 "/'",
139 "<'",
140 ">'",
141 "^",
142 "arabic",
143 "string-same",
144 "::",
145 "!",
146 "string-length",
147 "string-sub",
148 "string-explode",
149 "regexp-of-string",
150 "string-match",
151 "split-on-regexp",
152 "embed-string",
153 "inline-fil",
154 // ---- context ops / box combinators ----
155 "set-font-size",
156 "get-font-size",
157 "set-leading",
158 "set-paragraph-margin",
159 "get-text-width",
160 "get-initial-context",
161 "++",
162 "+++",
163 "inline-nil",
164 "block-nil",
165 "inline-skip",
166 "inline-glue",
167 "block-skip",
168 // ---- the reflow marker-box
169 // constructors (`primitives.rs`'s `prims!` table, `Both` versions) ----
170 "list-mark",
171 "inline-mark",
172 // ---- see primitives.rs's `prims!` table comment on `"set-font-key"` ----
173 "set-font-key",
174 // ---- the 0.1 `font` build-out: the LOCAL stand-in for upstream's
175 // internal `LoadSingleFont` node (see `primitives.rs`'s
176 // `prim_load_single_font`). V0_1-only, so
177 // `primitive_type_with_version` returns `None` for it under V0_0 and
178 // the seeding loops skip it there. ----
179 "load-single-font",
180 // ---- the ~18 pure primitives ----
181 // (`|>` excluded — see primitives.rs's `prims!` table comment; it has
182 // no primitive of its own, so it never belongs in this list).
183 "sin",
184 "asin",
185 "cos",
186 "acos",
187 "tan",
188 "atan",
189 "atan2",
190 "log",
191 "exp",
192 "ceil",
193 "floor",
194 "show-float",
195 "string-byte-length",
196 "string-sub-bytes",
197 "string-unexplode",
198 "display-message",
199 "abort-with-message",
200 // ---- raster images ----
201 "load-image",
202 "load-pdf-image",
203 "use-image-by-width",
204 // ---- graphics primitives ----
205 "start-path",
206 "line-to",
207 "terminate-path",
208 "close-with-line",
209 "fill",
210 "stroke",
211 "inline-graphics",
212 // ---- tables ----
213 "tabular",
214 "inline-graphics-outer",
215 // ---- gr.satyh primitives ----
216 "bezier-to",
217 "close-with-bezier",
218 "shift-path",
219 "linear-transform-path",
220 "shift-graphics",
221 "linear-transform-graphics",
222 "get-graphics-bbox",
223 "get-path-bbox",
224 "dashed-stroke",
225 "draw-text",
226 // ---- pervasives.satyh unblockers ----
227 "get-natural-metrics",
228 "inline-frame-outer",
229 // vminst.ml:1807 `BackendInnerFrame` — same signature as
230 // `inline-frame-outer` above; the primitive (`primitives.rs`) and its
231 // type (`prim_types.rs`) were both already registered for both
232 // versions, this list simply omitted the name.
233 "inline-frame-inner",
234 "set-manual-rising",
235 "script-guard",
236 "discretionary",
237 // ---- Tier-2 decoration/graphics packages ----
238 "get-axis-height",
239 // ---- hooks / annotations / cross-references ----
240 "hook-page-break",
241 "hook-page-break-block",
242 "register-cross-reference",
243 "get-cross-reference",
244 // ---- the hooks/annotations/cross-reference closer ----
245 "probe-cross-reference",
246 // ---- (annot.satyh) ----
247 "get-leftmost-script",
248 "get-rightmost-script",
249 "inline-frame-breakable",
250 "register-destination",
251 "register-link-to-uri",
252 "register-link-to-location",
253 // ---- the faithful math primitive layer ----
254 "math-char",
255 "math-big-char",
256 "math-char-with-kern",
257 "math-big-char-with-kern",
258 "math-concat",
259 "math-group",
260 "math-sup",
261 "math-sub",
262 "math-frac",
263 "math-radical",
264 "math-lower",
265 "math-upper",
266 "math-pull-in-scripts",
267 "math-color",
268 "math-char-class",
269 "math-variant-char",
270 "set-math-variant-char",
271 "get-left-math-class",
272 "get-right-math-class",
273 "math-paren",
274 "math-paren-with-middle",
275 "text-in-math",
276 "convert-string-for-math",
277 "embed-math",
278 "set-math-command",
279 "set-math-font",
280 "space-between-maths",
281 "raise-inline",
282 "embed-block-breakable",
283 "unite-path",
284 "set-min-gap-of-lines",
285 "omit-skip-after",
286 "set-text-color",
287 "get-text-color",
288 "set-hyphen-penalty",
289 "set-hyphen-min",
290 "set-space-ratio",
291 "set-space-ratio-between-scripts",
292 "split-into-lines",
293 "block-frame-breakable",
294 "embed-block-top",
295 "set-font",
296 // vminstdef.yaml:1350 `PrimitiveGetFont` — the reader for the slot
297 // `set-font` writes (`ruby` and `quotation` want the CJK face's size
298 // ratio); forked at the result head exactly like `set-font`'s second
299 // argument.
300 "get-font",
301 "set-code-text-command",
302 "get-natural-length",
303 "set-dominant-wide-script",
304 "set-dominant-narrow-script",
305 "set-language",
306 "set-every-word-break",
307 "register-outline",
308 "extract-string",
309 // ---- dominant-script/language getters ----
310 "get-dominant-wide-script",
311 "get-dominant-narrow-script",
312 "get-language",
313 // ---- text-mode-context sliver ----
314 "get-initial-text-info",
315 "deepen-indent",
316 "break",
317 // ---- proof.satyh/footnote-scheme.satyh unblockers ----
318 "embed-block-bottom",
319 "line-stack-bottom",
320 // vminstdef.yaml:1109 `BackendLineStackTop` — the top-anchored twin
321 // (`ruby` stacks its annotation above the base run).
322 "line-stack-top",
323 "add-footnote",
324 // ---- page-level prims blocking mitou-report/stdjareport ----
325 "clear-page",
326 // ---- added in 0.1 (`math-text`/`math-boxes` split + `read-math`) ----
327 "read-math",
328 "stringify-math",
329 "set-math-char",
330 "set-math-char-class",
331 "get-math-char-class",
332 "embed-inline-to-math",
333 "get-math-axis-height-ratio",
334 "%math-attach-scripts",
335 // ---- hyphenation/unidata loader + setter stand-ins, and the `here`
336 // lex-time-constant stand-in. All 5 are V0_1-only
337 // (`primitive_type_with_version` returns `None` for them under V0_0,
338 // same pattern as the bitwise/Unicode-string comment below documents). ----
339 "load-hyphenation-dictionary",
340 "load-unicode-char-database",
341 "set-hyphenation-dictionary",
342 "set-unicode-char-database",
343 "here",
344 // ---- added in 0.1 — bitwise ops, Unicode string ops,
345 // `read-file`, `register-document-information`. All 11
346 // unbound under V0_0 (`base_type_env_with_version`'s
347 // `primitive_type_with_version` filter skips them there, same as the
348 // 8 math-split names just above). `get-initial-text-info` is NOT
349 // listed again here — it's already present above as one
350 // shared name whose *type* forks per version (`prim_types.rs`). ----
351 "<<",
352 ">>",
353 "band",
354 "bor",
355 "bxor",
356 "bnot",
357 "normalize-string-to-nfc",
358 "normalize-string-to-nfd",
359 "split-grapheme-cluster",
360 "read-file",
361 "register-document-information",
362 // ---- added in 0.1 — the graphics-
363 // collection sweep's 2 added prims, unbound under V0_0. The 3 named + 6
364 // hidden retypes (`tabular`, `get-graphics-bbox`, `inline-graphics`,
365 // `inline-graphics-outer`, `inline-frame-outer/-inner/-breakable`,
366 // `block-frame-breakable`) are NOT listed again here — each is one
367 // shared name whose *type* forks per version (`prim_types.rs`),
368 // already present above. ----
369 "unite-graphics",
370 "clip-graphics-by-path",
371 // ---- language-completeness sweep: 0.1 float comparisons
372 // (`primitives.rs`'s `prims!` table comment on ">."/"<."/">=."/"<=.").
373 // Unbound under V0_0 — confirmed genuinely absent from 0.0.6 upstream,
374 // unlike "+."/"-."/"*."/"/." which both generations share.
375 ">.",
376 "<.",
377 ">=.",
378 "<=.",
379];
380
381// `#[allow(dead_code)]`: kept as the back-compat sibling of
382// `base_type_env_with_version` (mirrors `primitives::base_env`'s public
383// wrapper) even though every current internal caller goes straight to
384// `base_type_env_with_version` — `typecheck_verbose`/`typecheck` bypass this
385// fn directly via `typecheck_verbose_with_version`/`typecheck_with_version`.
386#[allow(dead_code)]
387fn base_type_env<'s>(store: &'s SymbolStore) -> TypeEnv<'s> {
388 base_type_env_with_version(store, RustyfiVersion::V0_0)
389}
390
391pub(crate) fn base_type_env_with_version<'s>(
392 store: &'s SymbolStore,
393 version: RustyfiVersion,
394) -> TypeEnv<'s> {
395 let mut env = TypeEnv::default();
396 for name in PRIMITIVE_NAMES {
397 if let Some(poly) = prim_types::primitive_type_with_version(name, version) {
398 // `with_primitive`, not `with`: this is seeding the BASE env,
399 // not a user binding, so it must not mark `name` as
400 // user-shadowed (see `TypeEnv::with_primitive`'s doc comment).
401 env = env.with_primitive(store.intern(name), poly);
402 }
403 }
404 env
405}
406
407/// The `Ast::VersionScope(version, _)` typecheck arm's env
408/// swap. `TypeEnv` is a flat, persistent-clone name -> scheme map (no
409/// separate "local scope stack" the way `compile.rs`'s `Compiler` has), so
410/// this clones `env` and OVERWRITES every `PRIMITIVE_NAMES` entry with
411/// `version`'s primitive type, leaving every other (user/local) binding
412/// untouched — a version-forked primitive used INSIDE the returned env
413/// (e.g. a spliced 0.0.6 dependency building a `page` ADT for
414/// `page-break`) then checks against `version`'s shapes, while a binder
415/// introduced AFTER this call still shadows normally.
416///
417/// A primitive name already shadowed by a USER binding from BEFORE
418/// this `VersionScope` must NOT be re-stomped. Provenance is tracked on
419/// `TypeEnv` directly since it has no lexical-scope stack: every REAL
420/// binding goes through `TypeEnv::with`/`with_all`, which records the name
421/// in `TypeEnv::shadowed`; the primitive-seeding loops use
422/// `TypeEnv::with_primitive`, which does not. So `e.shadowed.contains(name)`
423/// means "a real user binding rebound this primitive anywhere on the path
424/// to here" — skip the overwrite for those.
425fn version_scoped_type_env<'s>(
426 store: &'s SymbolStore,
427 env: &TypeEnv<'s>,
428 version: RustyfiVersion,
429) -> TypeEnv<'s> {
430 let mut e = env.clone();
431 for name in PRIMITIVE_NAMES {
432 let sym = store.intern(name);
433 if e.is_shadowed(sym) {
434 // A user binding shadows this primitive name already — respect
435 // it instead of re-stomping it with `version`'s builtin scheme.
436 continue;
437 }
438 if let Some(poly) = prim_types::primitive_type_with_version(name, version) {
439 e = e.with_primitive(sym, poly);
440 }
441 }
442 e
443}
444
445// ============================================================================
446// The type environment — the same persistent base/overlay split as
447// `elaborate::Scope` (see its doc comment).
448// ============================================================================
449
450/// Overlay size at which a `TypeEnv` folds its recent bindings down into a
451/// fresh shared base (see [`TypeEnv::maybe_promote`]). Bounds the per-`with`
452/// clone cost: every `with` clones only the ≤ this-many-entry overlays plus two
453/// cheap `Rc` bumps of the (large, shared) base, so type inference is O(program
454/// size) instead of the old O(program × env) — the previous flat-`HashMap`-per-
455/// binding clone was ~16M entry-clones on a corpus doc, ~85% of compile time.
456const OVERLAY_CAP: usize = 64;
457
458/// One [`TypeEnv`] slot: a scheme plus the STAGE its binder was read at
459/// (upstream's `Typeenv.add tyenv varnm (pty, evid, pre.stage)` — every
460/// binder upstream registers carries the stage of the expression that
461/// introduced it, `typechecker.ml:129/136/731/1509/1574`, and its `val_stage`
462/// field in 0.1). Held behind one `Rc` per binding.
463struct EnvEntry {
464 poly: PolyType,
465 /// Where a reference to this name is legal from — see
466 /// [`Stage::can_reference`].
467 stage: Stage,
468}
469
470/// A persistent name → scheme environment split into a large SHARED base
471/// (`Rc`, the accumulated prelude/package bindings — cloned by an `Rc` bump)
472/// and a small mutable OVERLAY of the most recent bindings (cloned in full per
473/// `with`, but capped at [`OVERLAY_CAP`]). Lookups check the overlay first,
474/// then the base; later bindings shadow earlier ones, exactly as a flat map
475/// would — but `with`/`with_all` never copy the whole environment.
476#[derive(Clone, Default)]
477pub(crate) struct TypeEnv<'s> {
478 // Schemes are held by `Rc`, not by value: a `PolyType` clone is a DEEP
479 // copy of its whole type tree — measured at 0.6M-1.8M type nodes copied
480 // per corpus document, an order of magnitude more than `instantiate`
481 // and `generalize` together, and the largest single cost left in
482 // typechecking — so an `Rc` makes every `with`'s overlay clone a
483 // refcount bump instead. The stage rides inside that same `Rc` (see
484 // [`EnvEntry`]), costing no extra allocation or word per overlay slot.
485 //
486 // Sharing is sound because a `PolyType`'s structure is immutable once
487 // built; its `TyVarRef`s are `Rc<RefCell<..>>`, so the mutable cells
488 // unification writes through were already common to every copy.
489 base: std::rc::Rc<HashMap<Symbol<'s>, std::rc::Rc<EnvEntry>>>,
490 overlay: HashMap<Symbol<'s>, std::rc::Rc<EnvEntry>>,
491 /// The set of names bound by a REAL program binding
492 /// (`with`/`with_all`, not the `with_primitive` primitive-seeding loops).
493 /// `version_scoped_type_env` consults it (via [`TypeEnv::is_shadowed`]) to
494 /// tell an untouched builtin scheme apart from a user shadow. Same
495 /// base/overlay split as `vars`.
496 base_shadowed: std::rc::Rc<std::collections::HashSet<Symbol<'s>>>,
497 overlay_shadowed: std::collections::HashSet<Symbol<'s>>,
498}
499
500impl<'s> TypeEnv<'s> {
501 /// Fold the overlays into fresh shared bases once the vars overlay reaches
502 /// [`OVERLAY_CAP`], keeping every `with`'s overlay clone bounded. Amortized
503 /// O(1) per binding (each promotion is O(base) but only every `OVERLAY_CAP`
504 /// bindings along a path); inference is tree-shaped, so no single env is a
505 /// hot branch point where a boundary promotion could recur.
506 fn maybe_promote(&mut self) {
507 if self.overlay.len() < OVERLAY_CAP {
508 return;
509 }
510 let mut base = (*self.base).clone();
511 for (k, v) in self.overlay.drain() {
512 base.insert(k, v);
513 }
514 self.base = std::rc::Rc::new(base);
515 if !self.overlay_shadowed.is_empty() {
516 let mut sh = (*self.base_shadowed).clone();
517 for k in self.overlay_shadowed.drain() {
518 sh.insert(k);
519 }
520 self.base_shadowed = std::rc::Rc::new(sh);
521 }
522 }
523
524 /// Bind `name` at `stage` — always the stage the BINDER was read at
525 /// (`Checker::binding_stage`), never a guess. The parameter is explicit
526 /// rather than defaulted precisely because a wrong stage here is silent:
527 /// it does not fail to compile, it just lets a reference through that
528 /// upstream refuses.
529 pub(crate) fn with(&self, name: Symbol<'s>, poly: PolyType, stage: Stage) -> TypeEnv<'s> {
530 let mut e = self.clone();
531 e.overlay_shadowed.insert(name);
532 e.overlay.insert(name, std::rc::Rc::new(EnvEntry { poly, stage }));
533 e.maybe_promote();
534 e
535 }
536
537 /// Install/refresh a BASE PRIMITIVE scheme without recording `name` in
538 /// `shadowed` — used ONLY by the two call sites that seed or refresh a
539 /// `PRIMITIVE_NAMES` entry directly (`base_type_env_with_version`,
540 /// `version_scoped_type_env`), as opposed to a real program binding
541 /// (`Ast::LetIn`/lambda param/pattern bind/`LetRecIn`/top-level decl),
542 /// which always goes through `with`/`with_all` instead. This is exactly
543 /// what lets `version_scoped_type_env` distinguish "still untouched
544 /// builtin" from "user-shadowed" (see its doc comment).
545 /// A primitive is `Persistent0`, so every stage may name it — upstream
546 /// registers the whole primitive table that way (`primitives.cppo.ml:596`,
547 /// `Typeenv.add tyenv varnm (pty, evid, Persistent0)`). Anything else
548 /// would make `\emph` unreachable from a `@stage: 0` library.
549 fn with_primitive(&self, name: Symbol<'s>, poly: PolyType) -> TypeEnv<'s> {
550 let mut e = self.clone();
551 e.overlay.insert(
552 name,
553 std::rc::Rc::new(EnvEntry {
554 poly,
555 stage: Stage::Persistent0,
556 }),
557 );
558 e.maybe_promote();
559 e
560 }
561
562 /// The raw slot. Every OCCURRENCE goes through `Checker::staged` instead,
563 /// which reads this and then enforces the staging matrix — there is
564 /// deliberately no stage-blind `get`, so a new reference site cannot
565 /// silently skip the check.
566 fn entry(&self, name: Symbol<'s>) -> Option<&EnvEntry> {
567 self.overlay
568 .get(&name)
569 .or_else(|| self.base.get(&name))
570 .map(|p| &**p)
571 }
572
573 /// Whether `name` was bound by a real program binding.
574 fn is_shadowed(&self, name: Symbol<'s>) -> bool {
575 self.overlay_shadowed.contains(&name) || self.base_shadowed.contains(&name)
576 }
577
578 /// Extend with each scheme in order (later shadows earlier) — the
579 /// canonical way to commit `infer_binding`'s result.
580 pub(crate) fn with_all(
581 &self,
582 schemes: Vec<(Symbol<'s>, PolyType)>,
583 stage: Stage,
584 ) -> TypeEnv<'s> {
585 let mut e = self.clone();
586 for (name, poly) in schemes {
587 e.overlay_shadowed.insert(name);
588 e.overlay
589 .insert(name, std::rc::Rc::new(EnvEntry { poly, stage }));
590 e.maybe_promote();
591 }
592 e
593 }
594
595 /// Remove a set of bindings: a parent seal revoking a
596 /// nested module's outer-hidden members at the parent's seal point — see
597 /// `v1/module_check.rs`'s `member_revoke_triggers`. V0_1-only. Flattens
598 /// both layers then removes — cold path, so the one-time flatten is fine.
599 #[allow(dead_code)]
600 pub(crate) fn without_all(&self, names: &[Symbol<'s>]) -> TypeEnv<'s> {
601 let mut vars: HashMap<Symbol<'s>, std::rc::Rc<EnvEntry>> = (*self.base).clone();
602 for (k, v) in &self.overlay {
603 vars.insert(*k, v.clone());
604 }
605 let mut sh = (*self.base_shadowed).clone();
606 sh.extend(self.overlay_shadowed.iter().copied());
607 for n in names {
608 vars.remove(n);
609 sh.remove(n);
610 }
611 TypeEnv {
612 base: std::rc::Rc::new(vars),
613 overlay: HashMap::new(),
614 base_shadowed: std::rc::Rc::new(sh),
615 overlay_shadowed: std::collections::HashSet::new(),
616 }
617 }
618}
619
620// ============================================================================
621// Lowering CST `TypeExpr` (a `type` declaration's ctor payload syntax, a
622// synonym's own body, and a `sig .. end`'s `val` annotations — the last
623// parsed but not yet consulted, see `elaborate.rs`'s module doc comment) to
624// `MonoType`. The grammar
625// (`rustyfi_syntax::cst::ast::TypeExpr`/`TypeProd`/`TypeApp`/`TypeAtom`)
626// supports function arrows, parens, type variables, bare names, 2+-way
627// product types (`*`), and a SINGLE-argument postfix type-constructor
628// application (`'a option`, `'a list`) — no record/list-literal/command
629// types or N-ary applied constructors — so this lowering is total (never
630// fails) and needs no arity checking of its own. `list`/`ref` map to this
631// port's dedicated `MonoType::List`/`Ref` formers (mirroring
632// `prim_types::list`/`reff`); every other applied name becomes a
633// one-argument `MonoType::Variant`. A *synonym* reference is left exactly
634// as `name_to_mono` produces it (indistinguishable from an unresolved
635// variant name) — transparently replacing it with the synonym's body is
636// `expand_synonyms`'s job, below.
637// ============================================================================
638
639/// Map a `type` declaration's bare type name to a `MonoType`. Every base
640/// type this port's primitives use is recognized by its surface name;
641/// anything else becomes a nominal, zero-argument `Variant` reference — the
642/// only shape a bare name in this minimal grammar could sensibly mean (no
643/// applied-constructor syntax exists to give it arguments), which is exactly
644/// what makes mutually-recursive user variant types (`type t = .. of t`),
645/// forward references (a later declaration's name used by an earlier one),
646/// and type *synonyms* (a synonym reference is resolved the same nominal
647/// way — see `expand_synonyms`) "just work": the name is resolved nominally,
648/// not by looking anything up at lowering time.
649fn name_to_mono(name: &str, version: RustyfiVersion) -> MonoType {
650 match name {
651 "unit" => t_unit(),
652 "bool" => t_bool(),
653 "int" => t_int(),
654 "float" => t_float(),
655 "length" => t_length(),
656 "string" => t_string(),
657 "inline-text" => t_inline_text(),
658 "block-text" => t_block_text(),
659 // The surface-name fork lives HERE, and only
660 // here. `BaseType::MathText` is reused byte-identically as both
661 // 0.0.6's `math` and V0_1's `math-text` (upstream's own rename);
662 // `math-boxes` is V0_1-only, new. Under V0_1 the word `math` in a
663 // type position is deliberately NOT recognized (0.1 has no `math`
664 // type, `types.cppo.ml:148-155`) — it falls through to the nominal-
665 // `Variant` default below, matching upstream's unbound-type error;
666 // under V0_0, `math-text`/`math-boxes` likewise stay unbound.
667 "math" if !version.math_is_split() => MonoType::Base(BaseType::MathText),
668 "math-text" if version.math_is_split() => MonoType::Base(BaseType::MathText),
669 "math-boxes" if version.math_is_split() => MonoType::Base(BaseType::MathBoxes),
670 "inline-boxes" => t_inline_boxes(),
671 "block-boxes" => t_block_boxes(),
672 "context" => t_context(),
673 "document" => t_document(),
674 "text-info" => MonoType::Base(BaseType::TextInfo),
675 // The graphics- tier base/synonym types upstream's
676 // `types.cppo.ml` base_type_map registers (`pre-path`/`path`/
677 // `graphics`/`image`, plus `deco`/`deco-set`
678 // (`primitives.cppo.ml:275-276`), plus the pragmatic `font`
679 // stand-in — see below).
680 //
681 // `pre-path`/`path`/`graphics`/`image` resolve the SAME WAY under
682 // both versions: upstream maps all four to the same base types in
683 // both generations (0.0.6 `types.cppo.ml:295-298`, dev-0-1-0
684 // `types.cppo.ml:157`, neither cppo-gated), so the port's formers
685 // take no version either.
686 //
687 // The VALUE rep does differ upstream (0.0.6's `BCGraphics` holds one
688 // `GraphicD.element`, dev-0-1-0's a list — `graphics_is_collection`,
689 // and why 0.0.6's `deco` returns `graphics list` where 0.1's returns
690 // one `graphics`), but it's a subset relation, not an
691 // incompatibility: the port spells both with one
692 // `Value::Graphics(GraphicsElem)`, using `GraphicsElem::Group` for
693 // 0.1's collection form. 0.0.6 can only ever produce a leaf
694 // (`unite-graphics`, the sole `Group` constructor, is 0.1-only), and
695 // every consumer (`shift_graphics`, `graphics_bbox`, the PDF/SVG
696 // writers) handles `Group` uniformly, so values cross safely both
697 // ways.
698 //
699 // Do NOT gate these four on `V0_1`: a gate makes `name_to_mono`
700 // disagree across versions, which is exactly what
701 // `forked_type_names()` (below) diffs — all four would report as
702 // forked, and the boundary guard would then reject every 0.1
703 // document importing a 0.0.6 dependency that so much as mentions
704 // `graphics` in export position.
705 //
706 // Deliberately NOT ungated: `deco`/`deco-set` (their EXPANSION really
707 // does fork; a value-level adapter handles it instead), `font` and `paren`
708 // (stand-ins, see below).
709 "pre-path" => t_prepath(),
710 "path" => t_path(),
711 "graphics" => t_graphics(),
712 "image" => t_image(),
713 "deco" if version == RustyfiVersion::V0_1 => t_deco(version),
714 "deco-set" if version == RustyfiVersion::V0_1 => t_decoset(version),
715 // `font` — a REAL base type under V0_1. Do NOT make it `t_string()`
716 // again: that stand-in made `font * float * float` in a 0.1 sig
717 // accidentally coincide with 0.0.6's `string * float * float`,
718 // so the cross-version boundary accepted a coincidence. Upstream
719 // `saphe-split` registers `("font", FontType)`
720 // (`types.cppo.ml`'s `base_type_hash_table:175`, spelled
721 // `tFONTKEY` at `primitives.cppo.ml:45`); its values are opaque
722 // `BCFontKey of FontKey.t` handles a font envelope mints
723 // (`envelopeChecker.ml`'s `check_font_envelope`) — `t_font_key()`/
724 // `Value::Font(FontKey)` are that type/value.
725 //
726 // Under `V0_0` the name keeps falling through to the nominal
727 // `Variant("font", [])` default: upstream 0.0.6 has no `font` type
728 // (no such row in `types.cppo.ml:280-303`, no `type font` in its
729 // bundled `.satyh` packages), so a 0.0.6 program writing `font`
730 // means an unrelated opaque user nominal — why it stays in
731 // `forked_type_names()` and does not cross
732 // (`v1::xver_adapt::forked_note` states the fork).
733 "font" if version == RustyfiVersion::V0_1 => t_font_key(),
734 // math-package completion: upstream's sig writes `val paren-left
735 // : paren` (+18 more) and `val angle-left : length -> paren`.
736 // Structural like `deco`/`deco-set` above — the sealed decl's
737 // declared type unifies with `paren-left`/`-right`'s inferred
738 // `length -> length -> context -> inline-boxes * (length ->
739 // length)` by construction. Under V0_0, `paren` keeps falling to
740 // the nominal `Variant("paren", [])` default (synonym-expansion via
741 // `pervasives.satyh`, unchanged).
742 "paren" if version == RustyfiVersion::V0_1 => t_paren(version),
743 other => MonoType::Variant(other.to_string(), Vec::new()),
744 }
745}
746
747// ============================================================================
748// Forked-name guard: builtin TYPE
749// names that resolve differently — or not at all — between `V0_0` and
750// `V0_1`. Builtin TYPE forks have no dedicated table — they live as inline
751// version guards in `primitive_type_with_version` (`prim_types.rs`) and
752// `name_to_mono` (just above) — so `forked_type_names` derives the set by
753// literally diffing their output per name, rather than filtering a table.
754// ============================================================================
755
756/// Structural (alpha-equivalence) comparison of two [`MonoType`]s, used by
757/// [`forked_type_names`]'s diff below. Plain `MonoType`/`PolyType` derive no
758/// `PartialEq` (a hard constraint keeps `types.rs`/`unify.rs`
759/// untouched), and even if
760/// they did, a naive field compare would be WRONG here: every polymorphic
761/// primitive's scheme mints brand-new [`TyVarRef`](crate::types::TyVarRef)s
762/// from a process-wide counter (`types.rs`'s `FRESH_ID`) on each call, so two
763/// calls to the very SAME (unforked) primitive under two DIFFERENT versions
764/// already carry different fresh-variable identities and would falsely
765/// "differ" under any identity-sensitive comparison. This instead walks both
766/// types in lockstep, building a positional bijection between the two sides'
767/// variables (keyed by `ptr_key()`, `types.rs`'s own union-find identity) —
768/// two types are equal iff they have the same shape up to a CONSISTENT
769/// renaming of variables, exactly "the same type" should mean here.
770fn mono_type_alpha_eq(a: &MonoType, b: &MonoType) -> bool {
771 let mut vmap = HashMap::new();
772 let mut vmap_rev = HashMap::new();
773 let mut rmap = HashMap::new();
774 let mut rmap_rev = HashMap::new();
775 mono_alpha_eq(a, b, &mut vmap, &mut vmap_rev, &mut rmap, &mut rmap_rev)
776}
777
778fn mono_alpha_eq(
779 a: &MonoType,
780 b: &MonoType,
781 vmap: &mut HashMap<usize, usize>,
782 vmap_rev: &mut HashMap<usize, usize>,
783 rmap: &mut HashMap<usize, usize>,
784 rmap_rev: &mut HashMap<usize, usize>,
785) -> bool {
786 match (&*resolve(a), &*resolve(b)) {
787 (MonoType::Var(va), MonoType::Var(vb)) => {
788 bijective_pair(va.ptr_key(), vb.ptr_key(), vmap, vmap_rev)
789 }
790 (MonoType::Base(ba), MonoType::Base(bb)) => ba == bb,
791 (MonoType::Func(ra, da, ca), MonoType::Func(rb, db, cb)) => {
792 row_alpha_eq(&ra, &rb, vmap, vmap_rev, rmap, rmap_rev)
793 && mono_alpha_eq(&da, &db, vmap, vmap_rev, rmap, rmap_rev)
794 && mono_alpha_eq(&ca, &cb, vmap, vmap_rev, rmap, rmap_rev)
795 }
796 (MonoType::Product(ta), MonoType::Product(tb)) => {
797 ta.len() == tb.len()
798 && ta
799 .iter()
800 .zip(tb.iter())
801 .all(|(x, y)| mono_alpha_eq(x, y, vmap, vmap_rev, rmap, rmap_rev))
802 }
803 (MonoType::List(ta), MonoType::List(tb))
804 | (MonoType::Ref(ta), MonoType::Ref(tb))
805 | (MonoType::Code(ta), MonoType::Code(tb)) => {
806 mono_alpha_eq(&ta, &tb, vmap, vmap_rev, rmap, rmap_rev)
807 }
808 (MonoType::Record(ra), MonoType::Record(rb)) => {
809 row_alpha_eq(&ra, &rb, vmap, vmap_rev, rmap, rmap_rev)
810 }
811 (MonoType::Variant(na, aa), MonoType::Variant(nb, ab)) => {
812 na == nb
813 && aa.len() == ab.len()
814 && aa
815 .iter()
816 .zip(ab.iter())
817 .all(|(x, y)| mono_alpha_eq(x, y, vmap, vmap_rev, rmap, rmap_rev))
818 }
819 (MonoType::InlineCmd(ca), MonoType::InlineCmd(cb))
820 | (MonoType::BlockCmd(ca), MonoType::BlockCmd(cb))
821 | (MonoType::MathCmd(ca), MonoType::MathCmd(cb)) => {
822 cmd_args_alpha_eq(&ca, &cb, vmap, vmap_rev, rmap, rmap_rev)
823 }
824 _ => false,
825 }
826}
827
828fn row_alpha_eq(
829 a: &Row,
830 b: &Row,
831 vmap: &mut HashMap<usize, usize>,
832 vmap_rev: &mut HashMap<usize, usize>,
833 rmap: &mut HashMap<usize, usize>,
834 rmap_rev: &mut HashMap<usize, usize>,
835) -> bool {
836 match (&*resolve_row(a), &*resolve_row(b)) {
837 (Row::Empty, Row::Empty) => true,
838 (Row::Var(va), Row::Var(vb)) => bijective_pair(va.ptr_key(), vb.ptr_key(), rmap, rmap_rev),
839 (Row::Cons(la, ta, ra), Row::Cons(lb, tb, rb)) => {
840 la == lb
841 && mono_alpha_eq(&ta, &tb, vmap, vmap_rev, rmap, rmap_rev)
842 && row_alpha_eq(&ra, &rb, vmap, vmap_rev, rmap, rmap_rev)
843 }
844 _ => false,
845 }
846}
847
848fn cmd_args_alpha_eq(
849 a: &[CmdArgType],
850 b: &[CmdArgType],
851 vmap: &mut HashMap<usize, usize>,
852 vmap_rev: &mut HashMap<usize, usize>,
853 rmap: &mut HashMap<usize, usize>,
854 rmap_rev: &mut HashMap<usize, usize>,
855) -> bool {
856 a.len() == b.len()
857 && a.iter().zip(b.iter()).all(|(ca, cb)| {
858 ca.optional == cb.optional
859 && ca.opt_labels.len() == cb.opt_labels.len()
860 && ca
861 .opt_labels
862 .iter()
863 .zip(cb.opt_labels.iter())
864 .all(|((la, tya), (lb, tyb))| {
865 la == lb && mono_alpha_eq(tya, tyb, vmap, vmap_rev, rmap, rmap_rev)
866 })
867 && mono_alpha_eq(&ca.ty, &cb.ty, vmap, vmap_rev, rmap, rmap_rev)
868 })
869}
870
871/// Record (or check) that pointer-key `ka` (side A) and `kb` (side B)
872/// correspond under the bijection being built; `false` if either side is
873/// already mapped to something else (a genuine structural mismatch).
874fn bijective_pair(
875 ka: usize,
876 kb: usize,
877 map: &mut HashMap<usize, usize>,
878 map_rev: &mut HashMap<usize, usize>,
879) -> bool {
880 match (map.get(&ka).copied(), map_rev.get(&kb).copied()) {
881 (Some(mapped_b), Some(mapped_a)) => mapped_b == kb && mapped_a == ka,
882 (None, None) => {
883 map.insert(ka, kb);
884 map_rev.insert(kb, ka);
885 true
886 }
887 _ => false,
888 }
889}
890
891/// Every name that means a DIFFERENT TYPE under `V0_0` than under `V0_1`,
892/// i.e. whose `name_to_mono` lowering differs. Its sole consumer is the
893/// cross-version type-boundary guard (`v1::xver_adapt::reject_type_names`,
894/// via `lib.rs`'s `free.types` check): if a 0.0.6 dependency writes this
895/// name in a type, does the 0.1 consumer read it as the same type?
896///
897/// Do NOT also admit names whose PRIMITIVE scheme forks
898/// (`primitive_type_with_version`). `PRIMITIVE_NAMES` is a list of VALUE
899/// names, none of which is a type, but there is a real collision:
900/// `math-char-class` is both a builtin type (nominal, version-blind) and a
901/// primitive whose scheme mentions the genuinely-forked `math`. Pulling the
902/// TYPE name in anyway makes the guard reject `math.satyh`'s perfectly safe
903/// sig mention (`\math-style : [math-char-class; math] math-cmd`) and,
904/// through it, every 0.1 document reaching the 0.0.6 math package: only
905/// its constructor set forks, so it stays out of this guard's reject set.
906///
907/// A forked primitive VALUE is not this guard's problem: each
908/// 0.0.6 dependency's bindings are wrapped in `Ast::VersionScope(V0_0, _)`, so a forked
909/// primitive referenced inside one resolves against 0.0.6's own `PrimDef`
910/// and runs under `Interp::version = V0_0` — why `lib.rs` has no value half
911/// of the guard. The names below are just the builtin TYPE names, filtered
912/// to those that really do lower differently.
913pub(crate) fn forked_type_names() -> BTreeSet<String> {
914 [
915 "math",
916 "math-text",
917 "math-boxes",
918 "pre-path",
919 "path",
920 "graphics",
921 "image",
922 "deco",
923 "deco-set",
924 "font",
925 "paren",
926 "math-char-class",
927 ]
928 .into_iter()
929 .filter(|&n| {
930 !mono_type_alpha_eq(
931 &name_to_mono(n, RustyfiVersion::V0_0),
932 &name_to_mono(n, RustyfiVersion::V0_1),
933 )
934 })
935 .map(str::to_string)
936 .collect()
937}
938
939fn lower_type_atom(
940 atom: &TypeAtom,
941 tyvars: &HashMap<String, MonoType>,
942 version: RustyfiVersion,
943) -> MonoType {
944 match atom {
945 // `[ty; ty?; ..] inline-cmd`/`block-cmd`/`math-cmd` — the direct
946 // wire-up to the existing `CmdArgType.optional` field:
947 // each bracketed element lowers to one `CmdArgType`,
948 // `optional` set exactly when the element carried a trailing `?`.
949 TypeAtom::Cmd { args, kind, .. } => {
950 let cmd_args: Vec<CmdArgType> = args
951 .iter()
952 .map(|a| {
953 let ty = lower_type_expr(&a.ty, tyvars, version);
954 // SATySFi 0.1 closed command optional-label map:
955 // `?(l:τ,…)` prefixing
956 // this slot's mandatory `ty`. Sorted by label so
957 // `unify_cmd_args`'s zip-equal equal-domain test is
958 // order-insensitive against whatever surface order the
959 // sig was written in (`command_scheme`'s harvest sorts
960 // the same way). Every 0.0.6-reachable item has
961 // `opt_labels == []` (that grammar has no such prefix at
962 // all), so it takes the `optional`/`mandatory` split on
963 // the 0.0.6 positional `?` suffix marker instead.
964 if a.opt_labels.is_empty() {
965 if a.opt.is_some() {
966 optional(ty)
967 } else {
968 mandatory(ty)
969 }
970 } else {
971 let mut labels: Vec<(String, MonoType)> = a
972 .opt_labels
973 .iter()
974 .map(|f| {
975 (
976 f.label.name.clone(),
977 lower_type_expr(&f.ty, tyvars, version),
978 )
979 })
980 .collect();
981 labels.sort_by(|x, y| x.0.cmp(&y.0));
982 labeled(labels, ty)
983 }
984 })
985 .collect();
986 match kind {
987 CmdTypeKind::Inline(_) => MonoType::InlineCmd(cmd_args),
988 CmdTypeKind::Block(_) => MonoType::BlockCmd(cmd_args),
989 CmdTypeKind::Math(_) => MonoType::MathCmd(cmd_args),
990 }
991 }
992 TypeAtom::Paren { inner, .. } => lower_type_expr(inner, tyvars, version),
993 // `(| l1 : ty1; l2 : ty2; … |)` — a CLOSED record type: fold the
994 // fields into a `Row::Cons` chain (in source order) ending in
995 // `Row::Empty`, matching `MonoType::Record`'s row representation
996 // (`types.rs`'s module doc comment) — distinct from `RecordKind`'s
997 // label-only `Kind::Record` bound (`lower_record_kind`, below),
998 // which drops field types entirely; a type-position record keeps
999 // them, since it's a concrete type, not a lower-bound obligation.
1000 TypeAtom::Record { fields, .. } => {
1001 let row = fields.iter().rev().fold(Row::Empty, |rest, f| {
1002 Row::Cons(
1003 f.name.name.clone(),
1004 Box::new(lower_type_expr(&f.ty, tyvars, version)),
1005 Box::new(rest),
1006 )
1007 });
1008 MonoType::Record(row)
1009 }
1010 // `(| l1 : ty1, … | ?'r |)` — a SATySFi 0.1 OPEN record type:
1011 // same fold as the closed form
1012 // above, but the row's TAIL is a fresh `Row::Var` rather than
1013 // `Row::Empty` — reusing the existing generic `Row`/`RowVarRef`/
1014 // `unify_row` machinery (no new type machinery). The row variable's
1015 // *name* (`?'r`) is not itself tracked anywhere past this point (the
1016 // same permissive-fallback philosophy `TypeAtom::Var`'s "should not
1017 // happen" case above already uses for an untracked name) — this
1018 // models one open record type at a time, not
1019 // cross-signature shared-row polymorphism (that needs the `rowquant`
1020 // grammar this track deliberately defers, see `cst.rs`'s
1021 // `TypeAtom::RecordOpen` doc comment). No `V0_0` version gate is
1022 // needed: `TypeAtom::RecordOpen` is unreachable from a `V0_0` token
1023 // stream by construction (see that variant's doc comment).
1024 TypeAtom::RecordOpen { inner, .. } => {
1025 let row = inner
1026 .fields
1027 .iter()
1028 .rev()
1029 .fold(Row::Var(types::new_row_var(0)), |rest, f| {
1030 Row::Cons(
1031 f.name.name.clone(),
1032 Box::new(lower_type_expr(&f.ty, tyvars, version)),
1033 Box::new(rest),
1034 )
1035 });
1036 MonoType::Record(row)
1037 }
1038 TypeAtom::Var(tv) => match tyvars.get(&tv.name) {
1039 Some(v) => v.clone(),
1040 // PERMISSIVE: a type variable not among the declaration's own
1041 // `tyvars` (should not happen for anything the parser accepts,
1042 // since `TypeAtom::Var` can only ever spell one of them here —
1043 // there is no scoping construct in this grammar that could
1044 // introduce any other free type variable) — treat it as its own
1045 // fresh, ungeneralized variable rather than rejecting the whole
1046 // declaration.
1047 None => MonoType::Var(types::new_ty_var(0)),
1048 },
1049 TypeAtom::Name(name) => name_to_mono(&name.name, version),
1050 // `Mod.t` (bare, 0-ary): full module-signature-aware resolution is
1051 // out of scope for this port's parser-level fix (`cst.rs`'s
1052 // `TypeAtom::NameMod` doc comment) — permissively treat it as its
1053 // own nominal type keyed by the qualified spelling, exactly like
1054 // `name_to_mono`'s own fallback for an unregistered unqualified
1055 // name (`MonoType::Variant(name, [])`).
1056 TypeAtom::NameMod(qn) => {
1057 MonoType::Variant(format!("{}.{}", qn.mods.join("."), qn.name), Vec::new())
1058 }
1059 }
1060}
1061
1062/// `txprod`: a [`TypeProd`] is either a single [`TypeApp`] (returned as-is)
1063/// or a genuine `*`-separated product (`MonoType::Product`, always 2+ items
1064/// by construction — see [`prim_types::product`]).
1065fn lower_type_prod(
1066 prod: &TypeProd,
1067 tyvars: &HashMap<String, MonoType>,
1068 version: RustyfiVersion,
1069) -> MonoType {
1070 if prod.rest.is_empty() {
1071 lower_type_app(&prod.first, tyvars, version)
1072 } else {
1073 let mut items = Vec::with_capacity(1 + prod.rest.len());
1074 items.push(lower_type_app(&prod.first, tyvars, version));
1075 for st in &prod.rest {
1076 items.push(lower_type_app(&st.ty, tyvars, version));
1077 }
1078 product(items)
1079 }
1080}
1081
1082/// `txapppre`/`txapp` (restricted to a single argument — see [`TypeApp`]'s
1083/// doc comment): either a bare atom, or one atom applied to a single postfix
1084/// type-constructor name (`'a option`, `('a list) list`).
1085fn lower_type_app(
1086 app: &TypeApp,
1087 tyvars: &HashMap<String, MonoType>,
1088 version: RustyfiVersion,
1089) -> MonoType {
1090 // A bare atom (no postfix constructor).
1091 if app.rest.is_empty() {
1092 return lower_type_atom(&app.head, tyvars, version);
1093 }
1094 // `arg1 … argN ctor`: the last atom is the constructor, the rest are its
1095 // arguments (see `TypeApp`'s doc comment).
1096 let (ctor, args): (&TypeAtom, Vec<MonoType>) = {
1097 let n = app.rest.len();
1098 let arg_tys = std::iter::once(&app.head)
1099 .chain(app.rest[..n - 1].iter())
1100 .map(|a| lower_type_atom(a, tyvars, version))
1101 .collect();
1102 (&app.rest[n - 1], arg_tys)
1103 };
1104 match ctor {
1105 TypeAtom::Name(name) => {
1106 let single = if args.len() == 1 {
1107 Some(args[0].clone())
1108 } else {
1109 None
1110 };
1111 match name.name.as_str() {
1112 "list" if single.is_some() => list(single.unwrap()),
1113 "ref" if single.is_some() => reff(single.unwrap()),
1114 // `code τ` — the staged-value type, spelled in SOURCE only by
1115 // 0.1 (`dev-0-1-0 src/frontend/manualTypeDecoder.ml:31-36`,
1116 // decoded as a one-argument application right beside `list`
1117 // at `:37-42` and `ref` at `:44-49`). 0.0.6's own manual-type
1118 // decoder (`v0.0.6 src/frontend/typeenv.ml:527-530`) knows
1119 // only `list` and `ref`, so under `V0_0` an `int code`
1120 // annotation stays what it has always been here: an unknown
1121 // nominal `Variant("code", [int])`, which unifies with
1122 // nothing and is refused just as upstream's
1123 // `UndefinedTypeName` refuses it.
1124 //
1125 // 0.1 writes types PREFIX (`code int`); `v1/lower.rs` has
1126 // already flipped that into this cst's postfix
1127 // `TypeApp { head, rest }` shape by the time it arrives.
1128 "code" if single.is_some() && version.has_code_type_syntax() => {
1129 MonoType::Code(Box::new(single.unwrap()))
1130 }
1131 // `T implicit` — `satysfi-base`'s typeclass-dictionary marker —
1132 // is transparent: an implicit `T` argument just has type `T`.
1133 "implicit" if single.is_some() => single.unwrap(),
1134 other => MonoType::Variant(other.to_string(), args),
1135 }
1136 }
1137 // `… Mod.t` — permissive nominal-type fallback, qualified-name-keyed
1138 // (see `lower_type_atom`'s `NameMod` arm).
1139 TypeAtom::NameMod(qn) => {
1140 MonoType::Variant(format!("{}.{}", qn.mods.join("."), qn.name), args)
1141 }
1142 // A non-name final atom is not a real constructor (unreachable in valid
1143 // source); lower it alone to keep this total.
1144 _ => lower_type_atom(ctor, tyvars, version),
1145 }
1146}
1147
1148/// `dom -> cod`, with `?->`'s optional-argument prefix (`opts`) folded in as
1149/// leading `option`-wrapped mandatory domains — a stand-in:
1150/// `config ?-> block-text -> document` lowers to `Func(option(config),
1151/// Func(block-text, document))`, exactly the shape the call-site model
1152/// produces (`Some`/`None` applied to a plain,
1153/// `option`-typed domain — see `elaborate.rs`'s `app_arg_to_ast`) — one
1154/// consistent optional-arg model. Not upstream's real
1155/// `option_row`/arity-changing encoding; this only needs the two encodings
1156/// to *unify*, which a plain `option` domain already does. `pub(crate)`:
1157/// `v1/module_check.rs`
1158/// reuses this exact lowering for a sig `val`'s declared type, twice per decl
1159/// (skolemize-by-lowering — a flexible-var map for the committed scheme, a
1160/// rigid-stamp map for the subsumption check).
1161pub(crate) fn lower_type_expr(
1162 ty: &TypeExpr,
1163 tyvars: &HashMap<String, MonoType>,
1164 version: RustyfiVersion,
1165) -> MonoType {
1166 match ty {
1167 TypeExpr::Fun { opts, dom, cod, .. } => {
1168 let result = arrow(
1169 lower_type_prod(dom, tyvars, version),
1170 lower_type_expr(cod, tyvars, version),
1171 );
1172 opts.iter().rev().fold(result, |acc, opt| {
1173 arrow(t_option(lower_type_prod(&opt.ty, tyvars, version)), acc)
1174 })
1175 }
1176 TypeExpr::Atom(prod) => lower_type_prod(prod, tyvars, version),
1177 // `?(l1 : ty1, …) dom -> cod` — a
1178 // CLOSED row (`Row::Cons(l1, ty1, … Row::Empty)`), matching what
1179 // `Ast::LambdaOpt` infers — see this fn's own callers
1180 // (`declare_synonym`/`build_variant_decl`), which reject this node
1181 // under `V0_0` via `check_type_expr_v0_1_only` BEFORE ever
1182 // reaching here, so by the time this arm runs the version is always
1183 // `V0_1` for any input that could legally have parsed this node from
1184 // real 0.1 source (a 0.0.6 source hitting this arm is caught by that
1185 // earlier gate, with a clear version-error message rather than
1186 // silently building a nonsense type here).
1187 TypeExpr::OptRowFun {
1188 opt_dom, dom, cod, ..
1189 } => {
1190 let row = opt_dom.entries.iter().rev().fold(Row::Empty, |acc, e| {
1191 Row::Cons(
1192 e.label.name.clone(),
1193 Box::new(lower_type_expr(&e.ty, tyvars, version)),
1194 Box::new(acc),
1195 )
1196 });
1197 MonoType::Func(
1198 Box::new(row),
1199 Box::new(lower_type_prod(dom, tyvars, version)),
1200 Box::new(lower_type_expr(cod, tyvars, version)),
1201 )
1202 }
1203 }
1204}
1205
1206/// Reject a `?(l : ty) -> ...`
1207/// labeled-optional-argument type domain under `V0_0` with a clear version
1208/// error, mirroring `elaborate.rs`'s `Expr::FunRows`/`AppArg::Bundled`
1209/// value-level gates — the TYPE-level analogue. It lives
1210/// here (not `elaborate.rs`) because a `type`/ctor-payload `TypeExpr` is
1211/// never routed through the elaborator at all: `UserTypeDecl`/
1212/// `UserSynonymDecl` (`elaborate.rs`) carry a raw CST `TypeExpr` fragment
1213/// straight through to [`Checker::declare_variant`]/[`Checker::
1214/// declare_synonym`], the first (and only) place `Checker.version` is in
1215/// scope for it. A cheap existence walk, not a lowering pass —
1216/// [`lower_type_expr`] itself stays total/infallible for every other caller
1217/// (its own doc comment) — this is called BEFORE it, at each dual-version
1218/// entry point.
1219fn check_type_expr_v0_1_only(ty: &TypeExpr, version: RustyfiVersion) -> Result<(), TypeError> {
1220 if version.has_row_polymorphism() {
1221 return Ok(());
1222 }
1223 if let Some(span) = find_opt_row_fun_in_expr(ty) {
1224 return Err(TypeError::simple(
1225 Some(span),
1226 "`?(l : ty) -> ...` labeled-optional-argument type domains are SATySFi \
1227 0.1 syntax — this file is compiled as 0.0.6",
1228 ));
1229 }
1230 Ok(())
1231}
1232
1233fn find_opt_row_fun_in_expr(ty: &TypeExpr) -> Option<Span> {
1234 match ty {
1235 TypeExpr::OptRowFun { opt_dom, .. } => Some(opt_dom.q.0),
1236 TypeExpr::Fun { dom, cod, .. } => {
1237 find_opt_row_fun_in_prod(dom).or_else(|| find_opt_row_fun_in_expr(cod))
1238 }
1239 TypeExpr::Atom(p) => find_opt_row_fun_in_prod(p),
1240 }
1241}
1242
1243fn find_opt_row_fun_in_prod(p: &TypeProd) -> Option<Span> {
1244 find_opt_row_fun_in_app(&p.first)
1245 .or_else(|| p.rest.iter().find_map(|s| find_opt_row_fun_in_app(&s.ty)))
1246}
1247
1248fn find_opt_row_fun_in_app(a: &TypeApp) -> Option<Span> {
1249 std::iter::once(&a.head)
1250 .chain(a.rest.iter())
1251 .find_map(find_opt_row_fun_in_atom)
1252}
1253
1254fn find_opt_row_fun_in_atom(a: &TypeAtom) -> Option<Span> {
1255 match a {
1256 TypeAtom::Cmd { args, .. } => args.iter().find_map(|it| find_opt_row_fun_in_expr(&it.ty)),
1257 TypeAtom::Paren { inner, .. } => find_opt_row_fun_in_expr(inner),
1258 TypeAtom::Record { fields, .. } => {
1259 fields.iter().find_map(|f| find_opt_row_fun_in_expr(&f.ty))
1260 }
1261 TypeAtom::RecordOpen { inner, .. } => inner
1262 .fields
1263 .iter()
1264 .find_map(|f| find_opt_row_fun_in_expr(&f.ty)),
1265 TypeAtom::Var(_) | TypeAtom::Name(_) | TypeAtom::NameMod(_) => None,
1266 }
1267}
1268
1269// ============================================================================
1270// Signature items: the `constraint 'a :: (| l1; l2; … |)`
1271// per-item suffix.
1272// ============================================================================
1273
1274/// Every distinct type-variable name occurring in `ty`, in first-occurrence
1275/// order. Unlike a `type` declaration, a [`SigItem`] has no upfront `tyvars`
1276/// list (`val document : 'a -> …` names `'a` inline, mid-type), so building
1277/// its lowering's `tyvars` map requires walking the type first — every
1278/// occurrence of the same name must resolve to the *same* fresh variable,
1279/// which is also the one a matching `constraint` suffix (naming that same
1280/// `'a`) attaches its `Kind::Record` bound to.
1281///
1282/// `#[allow(dead_code)]`: only exercised by this module's own tests today —
1283/// no sig-enforcement pass calls [`lower_sig_item`] yet.
1284#[allow(dead_code)]
1285fn collect_type_vars(ty: &TypeExpr, out: &mut Vec<String>) {
1286 fn push(name: &str, out: &mut Vec<String>) {
1287 if !out.iter().any(|n| n == name) {
1288 out.push(name.to_string());
1289 }
1290 }
1291 fn walk_atom(atom: &TypeAtom, out: &mut Vec<String>) {
1292 match atom {
1293 TypeAtom::Cmd { args, .. } => {
1294 for a in args {
1295 walk_expr(&a.ty, out);
1296 }
1297 }
1298 TypeAtom::Paren { inner, .. } => walk_expr(inner, out),
1299 TypeAtom::Record { fields, .. } => {
1300 for f in fields {
1301 walk_expr(&f.ty, out);
1302 }
1303 }
1304 TypeAtom::Var(tv) => push(&tv.name, out),
1305 TypeAtom::Name(_) => {}
1306 TypeAtom::NameMod(_) => {}
1307 // An open record's fields carry
1308 // type vars same as a closed record's; its row-variable tail
1309 // (`?'r`) is a ROW var, a different namespace this fn doesn't
1310 // track (it collects `TypeAtom::Var`/`'a`-style tyvars only).
1311 TypeAtom::RecordOpen { inner, .. } => {
1312 for f in &inner.fields {
1313 walk_expr(&f.ty, out);
1314 }
1315 }
1316 }
1317 }
1318 fn walk_app(app: &TypeApp, out: &mut Vec<String>) {
1319 walk_atom(&app.head, out);
1320 for a in &app.rest {
1321 walk_atom(a, out);
1322 }
1323 }
1324 fn walk_prod(prod: &TypeProd, out: &mut Vec<String>) {
1325 walk_app(&prod.first, out);
1326 for st in &prod.rest {
1327 walk_app(&st.ty, out);
1328 }
1329 }
1330 fn walk_expr(ty: &TypeExpr, out: &mut Vec<String>) {
1331 match ty {
1332 TypeExpr::Fun { opts, dom, cod, .. } => {
1333 for opt in opts {
1334 walk_prod(&opt.ty, out);
1335 }
1336 walk_prod(dom, out);
1337 walk_expr(cod, out);
1338 }
1339 TypeExpr::Atom(prod) => walk_prod(prod, out),
1340 // A labeled-optional domain's
1341 // entry types can mention tyvars same as any other domain.
1342 TypeExpr::OptRowFun {
1343 opt_dom, dom, cod, ..
1344 } => {
1345 for e in &opt_dom.entries {
1346 walk_expr(&e.ty, out);
1347 }
1348 walk_prod(dom, out);
1349 walk_expr(cod, out);
1350 }
1351 }
1352 }
1353 walk_expr(ty, out);
1354}
1355
1356/// Lower a [`RecordKind`]'s field list to its label set, dropping field
1357/// *types* — `Kind::Record` (`types.rs`) stores labels only, so
1358/// `constraint 'a :: (| title : inline-text; … |)` checks label
1359/// *presence*, not the field's declared type. A documented
1360/// limitation, not a grammar gap: the
1361/// impl's row still gets its own field types from ordinary usage, so this
1362/// is unlikely to admit a wrong program in practice.
1363#[allow(dead_code)]
1364fn lower_record_kind(rk: &RecordKind) -> BTreeSet<String> {
1365 rk.fields.iter().map(|f| f.name.name.clone()).collect()
1366}
1367
1368/// Lower one value/direct [`SigItem`] to its name and [`MonoType`],
1369/// attaching each `constraint 'a :: (| … |)` suffix as a `Kind::Record`
1370/// bound on `'a`'s freshly-minted variable — "this variable must be a
1371/// record containing at least these labels", built on the existing
1372/// Rémy-style row machinery. Returns `None` for a bare `SigItem::Type`
1373/// item (a type *name* declaration, not a value with a `MonoType` of its
1374/// own).
1375///
1376/// **The obligation check itself rides on existing code, for free.** Once
1377/// this variable is ever unified against a concrete `MonoType::Record`
1378/// (which is exactly what enforcing the signature against a real `struct`
1379/// implementation would do), `unify::bind_var`'s `Kind::Record` branch
1380/// already rejects a row missing any declared label via
1381/// `row_require_label`.
1382#[allow(dead_code)]
1383pub(crate) fn lower_sig_item(
1384 item: &SigItem,
1385 ctx: &mut TypeContext,
1386 version: RustyfiVersion,
1387) -> Option<(String, MonoType)> {
1388 let (name, ty, constraints): (&str, &TypeExpr, &[SigConstraint]) = match item {
1389 SigItem::ValHorzCmd {
1390 name,
1391 ty,
1392 constraints,
1393 ..
1394 } => (&name.name, ty, constraints),
1395 SigItem::ValVertCmd {
1396 name,
1397 ty,
1398 constraints,
1399 ..
1400 } => (&name.name, ty, constraints),
1401 SigItem::Val {
1402 name,
1403 ty,
1404 constraints,
1405 ..
1406 } => (&name.name, ty, constraints),
1407 SigItem::DirectHorzCmd {
1408 name,
1409 ty,
1410 constraints,
1411 ..
1412 } => (&name.name, ty, constraints),
1413 SigItem::DirectVertCmd {
1414 name,
1415 ty,
1416 constraints,
1417 ..
1418 } => (&name.name, ty, constraints),
1419 SigItem::Type { .. } => return None,
1420 };
1421 let mut names = Vec::new();
1422 collect_type_vars(ty, &mut names);
1423 let mut tyvars = HashMap::new();
1424 for n in names {
1425 let found = constraints.iter().find(|c| c.tyvar.name == n);
1426 let v = match found {
1427 Some(c) => ctx.fresh_var_with_kind(Kind::Record(lower_record_kind(&c.kind))),
1428 None => ctx.fresh_var_with_kind(Kind::Universal),
1429 };
1430 tyvars.insert(n, MonoType::Var(v));
1431 }
1432 Some((name.to_string(), lower_type_expr(ty, &tyvars, version)))
1433}
1434
1435/// Lower one [`UserTypeDecl`] (surfaced by `elaborate::elaborate_program`)
1436/// into a [`VariantDecl`], the same shape `prim_types::builtin_variants_with_version`
1437/// produces for `option`/`itemize` — see that struct's doc comment for how
1438/// `param_vars` and `instantiate_ctor` fit together. Each ctor's payload is
1439/// passed through [`expand_synonyms`] so a payload that names a synonym
1440/// (`type wrap = | W of point`) is stored already-transparent — `unify`
1441/// never has to know synonyms exist.
1442fn build_variant_decl(
1443 decl: &UserTypeDecl,
1444 synonyms: &HashMap<String, SynonymDecl>,
1445 version: RustyfiVersion,
1446) -> Result<VariantDecl, TypeError> {
1447 let param_vars: Vec<types::TyVarRef> =
1448 decl.params.iter().map(|_| types::new_ty_var(0)).collect();
1449 let tyvar_map: HashMap<String, MonoType> = decl
1450 .params
1451 .iter()
1452 .cloned()
1453 .zip(param_vars.iter().cloned().map(MonoType::Var))
1454 .collect();
1455 let mut ctors = Vec::with_capacity(decl.ctors.len());
1456 for (name, ty) in &decl.ctors {
1457 let payload = match ty {
1458 None => None,
1459 Some(t) => {
1460 check_type_expr_v0_1_only(t, version)?;
1461 Some(expand_synonyms(
1462 &lower_type_expr(t, &tyvar_map, version),
1463 synonyms,
1464 )?)
1465 }
1466 };
1467 ctors.push((name.clone(), payload));
1468 }
1469 Ok(VariantDecl {
1470 name: decl.name.clone(),
1471 params: decl.params.len(),
1472 ctors,
1473 param_vars,
1474 })
1475}
1476
1477// ============================================================================
1478// Type synonyms (`type point = length * length`): registration, plus the
1479// transparent expansion that keeps a synonym's name from ever reaching
1480// `unify` — mirrors upstream's `SynonymType`/`add_synonym`
1481// (`typechecker.ml`), just against this port's `MonoType`/`substitute`
1482// machinery instead of a substitution-on-the-fly unifier case.
1483// ============================================================================
1484
1485/// A user type-synonym declaration, lowered from [`UserSynonymDecl`] — the
1486/// transparent-expansion counterpart of [`VariantDecl`] (`prim_types.rs`).
1487/// Unlike a variant, a synonym never gets a runtime tag or a
1488/// `Checker::ctors` entry; the only thing that ever looks at one is
1489/// [`expand_synonyms`].
1490struct SynonymDecl {
1491 /// The declaration's own type-parameter placeholders — the same
1492 /// technique as `VariantDecl::param_vars` (matched by pointer identity
1493 /// via `types::substitute`). Realistically always empty: this grammar
1494 /// has no applied-type-constructor syntax to *reference* a synonym with
1495 /// arguments (`TypeAtom`'s doc comment), so a real reference site always
1496 /// supplies zero args — kept general anyway, so a nonzero-param synonym
1497 /// fails with a clear arity error rather than silently misbehaving if
1498 /// that ever changes.
1499 param_vars: Vec<types::TyVarRef>,
1500 /// The synonym's body, lowered exactly once via `lower_type_expr`. Any
1501 /// *other* synonym name it mentions is still an opaque
1502 /// `MonoType::Variant(name, [])` at this point (`lower_type_expr` has no
1503 /// notion of the synonym table) — [`expand_synonyms`] resolves those
1504 /// lazily, on demand, at each reference site.
1505 body: MonoType,
1506}
1507
1508fn build_synonym_decl(decl: &UserSynonymDecl, version: RustyfiVersion) -> SynonymDecl {
1509 let param_vars: Vec<types::TyVarRef> =
1510 decl.params.iter().map(|_| types::new_ty_var(0)).collect();
1511 let tyvar_map: HashMap<String, MonoType> = decl
1512 .params
1513 .iter()
1514 .cloned()
1515 .zip(param_vars.iter().cloned().map(MonoType::Var))
1516 .collect();
1517 SynonymDecl {
1518 param_vars,
1519 body: lower_type_expr(&decl.body, &tyvar_map, version),
1520 }
1521}
1522
1523/// Collect the name of every *synonym* (i.e. present in `synonyms`) directly
1524/// mentioned inside `ty`, ignoring argument count — used only to build the
1525/// "synonym references synonym" graph for [`check_synonym_cycles`], where
1526/// arity is irrelevant (a cycle is a cycle no matter how many arguments each
1527/// step is nominally applied to). Pre-order, duplicates included.
1528///
1529/// **Derived, not hand-written** ([`crate::visit`]). This walk was the live
1530/// instance of this port's recurring bug: its `InlineCmd`/`BlockCmd`/
1531/// `MathCmd` arm recursed into each slot's `ty` and not into that slot's
1532/// `opt_labels`, so a cycle routed through a `?(l : τ)` optional-label map
1533/// was invisible here while `expand_synonyms_cmd_args` expanded straight
1534/// into it — a stack overflow instead of a `cyclic type synonym` error
1535/// (`tests/synonym_cycle_opt_labels.rs`).
1536///
1537/// Semantics are unchanged otherwise: the traversal is structural (it does
1538/// **not** `resolve` through a bound type variable), pre-order, and
1539/// unconditional, exactly as the hand-written pair was.
1540fn synonym_refs(ty: &MonoType, synonyms: &HashMap<String, SynonymDecl>, out: &mut Vec<String>) {
1541 ty.visit(|t: &MonoType| {
1542 if let MonoType::Variant(name, _) = t {
1543 if synonyms.contains_key(name) {
1544 out.push(name.clone());
1545 }
1546 }
1547 });
1548}
1549
1550/// Reject a cyclic synonym (`type a = b` / `type b = a`, or a directly
1551/// self-referential `type a = a * a`) with a clear error instead of letting
1552/// [`expand_synonyms`] recurse forever. Run unconditionally over every
1553/// registered synonym at [`Checker::new`] time, so a cyclic pair is caught
1554/// even if nothing in the program actually references it.
1555fn check_synonym_cycles(synonyms: &HashMap<String, SynonymDecl>) -> Result<(), TypeError> {
1556 for start in synonyms.keys() {
1557 let mut stack = vec![start.clone()];
1558 check_synonym_cycles_from(start, synonyms, &mut stack)?;
1559 }
1560 Ok(())
1561}
1562
1563fn check_synonym_cycles_from(
1564 name: &str,
1565 synonyms: &HashMap<String, SynonymDecl>,
1566 stack: &mut Vec<String>,
1567) -> Result<(), TypeError> {
1568 let mut refs = Vec::new();
1569 synonym_refs(&synonyms[name].body, synonyms, &mut refs);
1570 for r in refs {
1571 if stack.contains(&r) {
1572 let mut cycle = stack.clone();
1573 cycle.push(r);
1574 return Err(TypeError::simple(
1575 None,
1576 format!("cyclic type synonym: {}", cycle.join(" -> ")),
1577 ));
1578 }
1579 stack.push(r.clone());
1580 check_synonym_cycles_from(&r, synonyms, stack)?;
1581 stack.pop();
1582 }
1583 Ok(())
1584}
1585
1586/// Recursively and transparently expand every synonym reference inside
1587/// `ty`, so `unify` never sees a synonym's name — only real base/product/
1588/// function/variant types. `synonyms` was already validated acyclic by
1589/// [`check_synonym_cycles`] (at `Checker::new` time), so the recursion here
1590/// is guaranteed to terminate; arity (a reference's argument count against
1591/// the synonym's own parameter count) is checked here, the one place that
1592/// actually has concrete arguments to check it against.
1593fn expand_synonyms(
1594 ty: &MonoType,
1595 synonyms: &HashMap<String, SynonymDecl>,
1596) -> Result<MonoType, TypeError> {
1597 match ty {
1598 MonoType::Var(_) | MonoType::Base(_) => Ok(ty.clone()),
1599 MonoType::Func(row, dom, cod) => Ok(MonoType::Func(
1600 Box::new(expand_synonyms_row(row, synonyms)?),
1601 Box::new(expand_synonyms(dom, synonyms)?),
1602 Box::new(expand_synonyms(cod, synonyms)?),
1603 )),
1604 MonoType::Product(ts) => Ok(MonoType::Product(
1605 ts.iter()
1606 .map(|t| expand_synonyms(t, synonyms))
1607 .collect::<Result<_, _>>()?,
1608 )),
1609 MonoType::List(t) => Ok(MonoType::List(Box::new(expand_synonyms(t, synonyms)?))),
1610 MonoType::Ref(t) => Ok(MonoType::Ref(Box::new(expand_synonyms(t, synonyms)?))),
1611 MonoType::Code(t) => Ok(MonoType::Code(Box::new(expand_synonyms(t, synonyms)?))),
1612 MonoType::Record(row) => Ok(MonoType::Record(expand_synonyms_row(row, synonyms)?)),
1613 MonoType::Variant(name, args) => {
1614 let args: Vec<MonoType> = args
1615 .iter()
1616 .map(|t| expand_synonyms(t, synonyms))
1617 .collect::<Result<_, _>>()?;
1618 let Some(syn) = synonyms.get(name) else {
1619 return Ok(MonoType::Variant(name.clone(), args));
1620 };
1621 if args.len() != syn.param_vars.len() {
1622 return Err(TypeError::simple(
1623 None,
1624 format!(
1625 "type synonym '{name}' expects {} argument{}, got {}",
1626 syn.param_vars.len(),
1627 if syn.param_vars.len() == 1 { "" } else { "s" },
1628 args.len()
1629 ),
1630 ));
1631 }
1632 let mut var_map: HashMap<usize, MonoType> = HashMap::new();
1633 for (pv, arg) in syn.param_vars.iter().zip(args.iter()) {
1634 var_map.insert(types::ptr_key(pv), arg.clone());
1635 }
1636 let substituted = types::substitute(&syn.body, &var_map, &HashMap::new());
1637 expand_synonyms(&substituted, synonyms)
1638 }
1639 MonoType::InlineCmd(cs) => Ok(MonoType::InlineCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1640 MonoType::BlockCmd(cs) => Ok(MonoType::BlockCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1641 MonoType::MathCmd(cs) => Ok(MonoType::MathCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1642 }
1643}
1644
1645fn expand_synonyms_row(
1646 row: &Row,
1647 synonyms: &HashMap<String, SynonymDecl>,
1648) -> Result<Row, TypeError> {
1649 match row {
1650 Row::Empty => Ok(Row::Empty),
1651 Row::Var(v) => Ok(Row::Var(v.clone())),
1652 Row::Cons(label, t, rest) => Ok(Row::Cons(
1653 label.clone(),
1654 Box::new(expand_synonyms(t, synonyms)?),
1655 Box::new(expand_synonyms_row(rest, synonyms)?),
1656 )),
1657 }
1658}
1659
1660fn expand_synonyms_cmd_args(
1661 cs: &[CmdArgType],
1662 synonyms: &HashMap<String, SynonymDecl>,
1663) -> Result<Vec<CmdArgType>, TypeError> {
1664 cs.iter()
1665 .map(|c| {
1666 Ok(CmdArgType {
1667 optional: c.optional,
1668 opt_labels: c
1669 .opt_labels
1670 .iter()
1671 .map(|(l, t)| Ok((l.clone(), expand_synonyms(t, synonyms)?)))
1672 .collect::<Result<_, TypeError>>()?,
1673 ty: expand_synonyms(&c.ty, synonyms)?,
1674 })
1675 })
1676 .collect()
1677}
1678
1679// ============================================================================
1680// The checker.
1681// ============================================================================
1682
1683pub(crate) struct Checker<'s> {
1684 /// The interner every identifier in the tree being checked came from.
1685 /// Needed both to mint derived lookup keys (`"{modpfx}.{ctor}"`) and to
1686 /// resolve a `Symbol` back to text for an error message — see
1687 /// [`Checker::text`].
1688 store: &'s SymbolStore,
1689 ctx: TypeContext,
1690 /// Constructor name -> the (`Rc`-shared) declaration it belongs to.
1691 /// Later declarations shadow earlier ones of the same ctor name, mirroring
1692 /// ordinary name shadowing elsewhere in this port.
1693 ctors: HashMap<String, Rc<VariantDecl>>,
1694 /// The same declarations, keyed by *type* name instead — needed by the
1695 /// exhaustiveness pass (`exhaustive::check_match`) to enumerate a
1696 /// variant's full constructor set given the scrutinee's resolved
1697 /// `MonoType::Variant(name, _)`.
1698 variants: HashMap<String, Rc<VariantDecl>>,
1699 /// The synonym table. A field rather than a local of `new_with_version`
1700 /// so `declare_variant` can expand ctor payloads through it after
1701 /// construction time (per-binding registration).
1702 synonyms: HashMap<String, SynonymDecl>,
1703 /// Non-fatal diagnostics accumulated by the exhaustiveness/redundancy
1704 /// pass (see `typecheck_verbose`); v0.0.6's `exhchecker.ml` warns and
1705 /// continues rather than rejecting the program.
1706 warnings: Vec<MatchWarning>,
1707 /// The target version this session is checking against — set by
1708 /// [`Checker::install_builtin_variants`] (every real construction path's
1709 /// call; `Checker::empty` alone never does, defaulting to `V0_0`).
1710 /// Threaded into `name_to_mono`'s surface-type-name fork and
1711 /// `Ast::LetMathIn`'s scheme rule (`math_command_scheme` vs
1712 /// `math_command_scheme_v01`).
1713 version: RustyfiVersion,
1714 /// The generation of the code currently being inferred, when that is
1715 /// NOT [`Checker::version`] — set (save/restore) by the
1716 /// `Ast::VersionScope` infer arm, `None` outside every such scope.
1717 /// Deliberately a SEPARATE field rather than a temporary overwrite of
1718 /// `version`, since that one must stay pinned to `V0_1` for a merged
1719 /// whole-program session (`v1::module_check::check_program_inner`). See
1720 /// [`Checker::binding_version`], its only consumer.
1721 scoped_version: Option<RustyfiVersion>,
1722 /// The module path of the member body currently being inferred (pushed by
1723 /// the `Ast::ModuleScope` arm; empty at top level). A BARE constructor
1724 /// reference is looked up under `<path>.Ctor` (innermost prefix first)
1725 /// before the bare fallback — so two modules' same-named constructors
1726 /// (`Term.Paren` vs `Type.Paren`) no longer collide. See [`Ast::ModuleScope`].
1727 ctor_scope: Vec<String>,
1728 /// The stage this expression is being read at. Starts at whatever the
1729 /// file declared (`@stage:`, default [`Stage::Stage1`]) and is shifted by
1730 /// `&`/`~`: a quote reads its body one stage LATER, a splice one stage
1731 /// EARLIER. Only `Next`/`Prev` consult it.
1732 stage: Stage,
1733}
1734
1735/// One elaborated top-level-shaped binding, viewed by reference — the
1736/// typecheck-side mirror of `elaborate::Binding` and of the four Let-shaped
1737/// `Ast` spine variants (`ast.rs`). The module checker constructs these
1738/// directly from its own per-`val` walk; the whole-program path never
1739/// constructs one explicitly (its `infer` arms pass the same references
1740/// through).
1741pub(crate) enum BindingView<'a, 's> {
1742 /// `Ast::LetIn` — plain value OR `\`/`+`-sigiled command binding; the
1743 /// sigil dispatch (`command_scheme`) stays inside the checker.
1744 Let {
1745 name: Symbol<'s>,
1746 value: &'a Ast<'s>,
1747 },
1748 /// `Ast::LetMathIn` — a math-command binding (distinct variant by
1749 /// construction).
1750 LetMath {
1751 name: Symbol<'s>,
1752 value: &'a Ast<'s>,
1753 },
1754 /// `Ast::LetRecIn`'s binding group (all names in scope in all bodies).
1755 LetRec(&'a [(Symbol<'s>, Rc<Ast<'s>>)]),
1756 /// `Ast::LetMutableIn` — value restriction, never generalized.
1757 LetMutable { name: Symbol<'s>, init: &'a Ast<'s> },
1758}
1759
1760impl<'s> Checker<'s> {
1761 // See `base_type_env`'s `#[allow(dead_code)]` note above — same shape,
1762 // same reason.
1763 #[allow(dead_code)]
1764 fn new(program: &Program<'s>) -> Result<Checker<'s>, TypeError> {
1765 Self::new_with_version(program, RustyfiVersion::V0_0)
1766 }
1767
1768 /// Bare session: empty tables, fresh `TypeContext`. Registers NOTHING —
1769 /// not even builtins — so `new_with_version` can compose the exact
1770 /// statement order of the original monolithic constructor.
1771 pub(crate) fn empty(store: &'s SymbolStore) -> Checker<'s> {
1772 Checker {
1773 store,
1774 ctx: TypeContext::new(),
1775 ctors: HashMap::new(),
1776 variants: HashMap::new(),
1777 synonyms: HashMap::new(),
1778 warnings: Vec::new(),
1779 version: RustyfiVersion::V0_0,
1780 scoped_version: None,
1781 ctor_scope: Vec::new(),
1782 // A document is stage 1; a library overrides this from its
1783 // `@stage:` header before checking begins.
1784 stage: Stage::default(),
1785 }
1786 }
1787
1788 /// Set the session's target version with NO other side effect — a pure
1789 /// field write, safe to call before anything else.
1790 /// `new_with_version`/`v1::module_check::check_program`'s session-setup
1791 /// sequences both call this FIRST, ahead of their order-critical
1792 /// `declare_synonym` loop, so a V0_1 synonym body that names
1793 /// `math-text`/`math-boxes` resolves correctly even though
1794 /// `install_builtin_variants` (which also sets this field) does not run
1795 /// until afterward. A no-op for every 0.0.6 path: `empty()` already
1796 /// defaults to `V0_0`.
1797 pub(crate) fn set_version(&mut self, version: RustyfiVersion) {
1798 self.version = version;
1799 }
1800
1801 /// A symbol's source text. Every diagnostic this module formats goes
1802 /// through here: `Symbol`'s own `Debug` is index-only by design, and the
1803 /// golden tests diff the resolved strings.
1804 fn text(&self, sym: Symbol<'s>) -> &'s str {
1805 self.store.resolve(sym)
1806 }
1807
1808 /// Register the builtin variant decls for `version`, and record
1809 /// `version` on `self` — redundant with `set_version` for a path that
1810 /// calls both, kept so this method alone suffices for a caller that
1811 /// skips `set_version` (e.g. the bare-builtins test construction).
1812 pub(crate) fn install_builtin_variants(&mut self, version: RustyfiVersion) {
1813 self.version = version;
1814 self.install_additional_builtin_variants(version);
1815 }
1816
1817 /// Register `version`'s builtin variant/ctor set WITHOUT
1818 /// touching `self.version` (unlike [`Checker::install_builtin_
1819 /// variants`], which also sets the whole-session version tag) — called
1820 /// from the `Ast::VersionScope` `infer` arm below, lazily, the first
1821 /// time a version-scoped subtree is reached, so a `V0_0`-only ADT like
1822 /// `page` (`A0Paper`/…/`A4Paper`/`UserDefinedPaper`, gated on
1823 /// `has_page_adt()`, `prim_types.rs`'s `builtin_variants_with_version`)
1824 /// is constructible/matchable inside a spliced dependency's internal
1825 /// `page-break A4Paper …` call even though the whole-program `Checker`
1826 /// was built under `V0_1` (where `page` isn't registered at all).
1827 /// `self.ctors`/`self.variants` are flat, last-writer-wins, program-
1828 /// global tables already (`hide_ctors`'s doc comment); every
1829 /// `builtin_variants_with_version` entry OTHER than `page` is identical
1830 /// between the two versions (only `page` is gated by `has_page_adt()`,
1831 /// `prim_types.rs:2119`), so this is a safe additive merge, not a
1832 /// replace — idempotent across repeated `VersionScope` nodes of the same
1833 /// version.
1834 pub(crate) fn install_additional_builtin_variants(&mut self, version: RustyfiVersion) {
1835 for decl in builtin_variants_with_version(version) {
1836 let decl = Rc::new(decl);
1837 self.variants.insert(decl.name.clone(), decl.clone());
1838 for (cname, _) in &decl.ctors {
1839 self.ctors.insert(cname.clone(), decl.clone());
1840 }
1841 }
1842 }
1843
1844 /// One synonym registration. Does NOT cycle-check — register all, then
1845 /// call `check_cycles`. Fallible:
1846 /// `check_type_expr_v0_1_only` rejects a `?(l:ty)->` domain in the
1847 /// synonym's body under `V0_0` before it is ever lowered.
1848 pub(crate) fn declare_synonym(&mut self, decl: &UserSynonymDecl) -> Result<(), TypeError> {
1849 check_type_expr_v0_1_only(&decl.body, self.version)?;
1850 self.synonyms
1851 .insert(decl.name.clone(), build_synonym_decl(decl, self.version));
1852 Ok(())
1853 }
1854
1855 /// Cycle-check the accumulated synonym table — a thin wrapper over the
1856 /// existing free fn `check_synonym_cycles`.
1857 pub(crate) fn check_cycles(&self) -> Result<(), TypeError> {
1858 check_synonym_cycles(&self.synonyms)
1859 }
1860
1861 /// One variant-decl registration.
1862 pub(crate) fn declare_variant(&mut self, decl: &UserTypeDecl) -> Result<(), TypeError> {
1863 let decl = Rc::new(build_variant_decl(decl, &self.synonyms, self.version)?);
1864 self.variants.insert(decl.name.clone(), decl.clone());
1865 for (cname, _) in &decl.ctors {
1866 self.ctors.insert(cname.clone(), decl.clone());
1867 }
1868 // If the variant's own type name is module-qualified (`M.t`), also
1869 // register each constructor under a qualified key (`M.Ctor`) so a
1870 // within-module bare reference (via `Checker::lookup_ctor`, driven by
1871 // `Ast::ModuleScope`) resolves to THIS module's ctor even when another
1872 // module declares the same bare ctor name. Builtins have undotted type
1873 // names, so this adds nothing for them.
1874 if let Some((modpfx, _)) = decl.name.rsplit_once('.') {
1875 for (cname, _) in &decl.ctors {
1876 self.ctors.insert(format!("{modpfx}.{cname}"), decl.clone());
1877 }
1878 }
1879 Ok(())
1880 }
1881
1882 /// The original whole-program constructor, re-expressed as the exact
1883 /// same statement sequence through the methods above: synonyms →
1884 /// cycle-check → builtins → user variant decls. This order-preservation
1885 /// is load-bearing for session-incrementality.
1886 fn new_with_version(
1887 program: &Program<'s>,
1888 version: RustyfiVersion,
1889 ) -> Result<Checker<'s>, TypeError> {
1890 // Synonyms are registered (and checked for cycles) before any
1891 // variant decl is lowered, since a variant's ctor payload may name a
1892 // synonym (`build_variant_decl` expands through `synonyms`).
1893 let mut c = Checker::empty(program.store);
1894 c.set_version(version);
1895 for usd in &program.synonym_decls {
1896 c.declare_synonym(usd)?;
1897 }
1898 c.check_cycles()?;
1899 c.install_builtin_variants(version);
1900 for utd in &program.type_decls {
1901 c.declare_variant(utd)?;
1902 }
1903 Ok(c)
1904 }
1905
1906 fn fresh(&mut self) -> MonoType {
1907 MonoType::Var(self.ctx.fresh_var())
1908 }
1909
1910 fn unify_ctx(
1911 &mut self,
1912 expected: &MonoType,
1913 found: &MonoType,
1914 span: Option<Span>,
1915 what: &str,
1916 ) -> Result<(), TypeError> {
1917 unify(expected, found).map_err(|e| TypeError::from_unify(span, what, e))
1918 }
1919
1920 /// Turn a `\`/`+`-named `LetIn` binding's ordinarily-inferred value type
1921 /// `tv` into the genuine command type (`MonoType::InlineCmd`/`BlockCmd`)
1922 /// it gets bound under: a user-defined command is typed as
1923 /// `[τ1; ..; τn] inline-cmd` (resp. `block-cmd`), matching v0.0.6's real
1924 /// `HorzCommandType`/`VertCommandType` (`typechecker.ml`'s
1925 /// `UTLetHorzIn`/`UTLetVertIn` rules), not a plain "context-curried"
1926 /// function.
1927 ///
1928 /// Two shapes reach this function, per [`command_sigil`]'s call site:
1929 ///
1930 /// * a genuine `let-inline`/`let-block` definition, whose value is the
1931 /// `Lambda(ctxvar, Lambda(p1, .., Lambda(pn, body)))` chain
1932 /// `elaborate::elaborate_let_inline` builds — [`peel_func_chain`]
1933 /// recovers that `Func` chain; the leading domain must unify with
1934 /// `context`, the final codomain with `inline-boxes`/`block-boxes`,
1935 /// and the domains between become the command's `CmdArgType` list.
1936 /// * a qualified-name *alias* of an already-command-typed binding (a
1937 /// module's own `M.\cmd` re-export, or an `open` re-binding — both
1938 /// build `LetIn(name, Ast::Var(qualified), body)`): the aliased name
1939 /// was already run through this function at its own definition site,
1940 /// so `tv` here already *is* the command type — this branch passes it
1941 /// through unchanged (re-generalized) instead of peeling a `Func`
1942 /// chain out of something that isn't one.
1943 fn command_scheme(
1944 &mut self,
1945 name: &str,
1946 sigil: char,
1947 tv: MonoType,
1948 span: Option<Span>,
1949 ) -> Result<PolyType, TypeError> {
1950 debug_assert!(sigil == '\\' || sigil == '+');
1951 let is_inline = sigil == '\\';
1952 let (want_result, kind, other_kind) = if is_inline {
1953 (t_inline_boxes(), "inline", "block")
1954 } else {
1955 (t_block_boxes(), "block", "inline")
1956 };
1957
1958 match &*resolve(&tv) {
1959 MonoType::InlineCmd(_) if is_inline => {
1960 return Ok(generalize(self.ctx.level(), &tv));
1961 }
1962 MonoType::BlockCmd(_) if !is_inline => {
1963 return Ok(generalize(self.ctx.level(), &tv));
1964 }
1965 MonoType::InlineCmd(_) | MonoType::BlockCmd(_) => {
1966 return Err(TypeError::simple(
1967 span,
1968 format!(
1969 "'{name}' is bound to a {other_kind} command, but its \
1970 name marks it as {article} {kind} command",
1971 article = if kind == "inline" { "an" } else { "a" },
1972 ),
1973 ));
1974 }
1975 // A qualified-name alias (`M.\cmd` re-export, or an `open`) of a
1976 // GENUINE `let-math` binding: math commands share the `\` sigil
1977 // with inline commands (there is no separate math-command token),
1978 // so an alias site only ever reaches this generic `Ast::LetIn`
1979 // path (never `Ast::LetMathIn`, which is produced only at a math
1980 // command's OWN definition site — top-level `let-math` via
1981 // `walk_bindings`, or the expression-level `let-math .. in ..`
1982 // form, `elaborate.rs`'s `Expr::LetMathIn` arm — never at an
1983 // alias site; see that variant's doc comment). Pass a already-
1984 // `MathCmd`-typed alias through unchanged, exactly like the
1985 // `InlineCmd`/`BlockCmd` arms above do for their own kind.
1986 MonoType::MathCmd(_) if is_inline => {
1987 return Ok(generalize(self.ctx.level(), &tv));
1988 }
1989 _ => {}
1990 }
1991
1992 // V0_1 harvests each param's closed
1993 // `?(l:τ,…)` label map from the `Row` that `Ast::LambdaOpt` leaves on
1994 // that param's own arrow (`peel_func_chain_rows`), instead of the
1995 // 0.0.6 "`_ option` domain ⇒ optional slot" heuristic below (which
1996 // stays untouched, byte-identical, under V0_0 — it never sees a
1997 // non-`Row::Empty` row at all, since V0_0 code never builds
1998 // `Ast::LambdaOpt`).
1999 let params: Vec<CmdArgType> = if self.version.has_row_polymorphism() {
2000 let (mut slots, result) = peel_func_chain_rows(tv);
2001 if slots.is_empty() {
2002 return Err(TypeError::simple(
2003 span,
2004 format!(
2005 "the binding for '{name}' must be a function taking a \
2006 context as its first argument (e.g. via `val inline ctx \
2007 {name} .. = ..`)"
2008 ),
2009 ));
2010 }
2011 let (ctx_row, ctx_ty) = slots.remove(0);
2012 // A labeled bundle can never legally land on the ctx binder
2013 // (`elaborate_let_inline` always wraps it in a plain
2014 // `Ast::Lambda`, which infers `Row::Empty` — `prim_types::arrow`)
2015 // — guard it defensively rather than silently dropping/mis-
2016 // attributing a label.
2017 if !matches!(&*resolve_row(&ctx_row), Row::Empty) {
2018 return Err(TypeError::simple(
2019 span,
2020 format!(
2021 "the context argument of '{name}' cannot carry a labeled \
2022 optional bundle"
2023 ),
2024 ));
2025 }
2026 self.unify_ctx(
2027 &t_context(),
2028 &ctx_ty,
2029 span,
2030 &format!("the context argument of '{name}'"),
2031 )?;
2032 self.unify_ctx(
2033 &want_result,
2034 &result,
2035 span,
2036 &format!("the result of '{name}'"),
2037 )?;
2038 slots
2039 .into_iter()
2040 .map(|(row, dom)| harvest_slot(row, dom))
2041 .collect()
2042 } else {
2043 let (mut doms, result) = peel_func_chain(tv);
2044 if doms.is_empty() {
2045 return Err(TypeError::simple(
2046 span,
2047 format!(
2048 "the binding for '{name}' must be a function taking a \
2049 context as its first argument (e.g. via `let-inline ctx \
2050 {name} .. = ..`)"
2051 ),
2052 ));
2053 }
2054 let ctx_ty = doms.remove(0);
2055 self.unify_ctx(
2056 &t_context(),
2057 &ctx_ty,
2058 span,
2059 &format!("the context argument of '{name}'"),
2060 )?;
2061 self.unify_ctx(
2062 &want_result,
2063 &result,
2064 span,
2065 &format!("the result of '{name}'"),
2066 )?;
2067 // Optional command params, simplified (Sub-area 2): this grammar
2068 // has no def-site `?:param` marker, so a param counts as optional
2069 // exactly when its INFERRED domain resolves to `_ option` — i.e.
2070 // the body actually uses it as an `option` (`match p with Some ..
2071 // | None -> ..`). `CmdArgType.ty` then stores the option's INNER
2072 // type, matching the `[ty?; ..]` signature-lowering shape 1:1
2073 // (`lower_type_atom`'s `TypeAtom::Cmd` arm); `check_cmd_args`
2074 // re-wraps it in `option(..)` per call, since call-site args
2075 // always arrive pre-wrapped as `Some`/`None`
2076 // (`elaborate.rs`'s `app_arg_to_ast`).
2077 doms.into_iter()
2078 .map(|d| match resolve(&d).into_owned() {
2079 MonoType::Variant(vname, mut vargs)
2080 if vname == "option" && vargs.len() == 1 =>
2081 {
2082 optional(vargs.pop().unwrap())
2083 }
2084 _ => mandatory(d),
2085 })
2086 .collect()
2087 };
2088 let cmd_ty = if is_inline {
2089 MonoType::InlineCmd(params)
2090 } else {
2091 MonoType::BlockCmd(params)
2092 };
2093 Ok(generalize(self.ctx.level(), &cmd_ty))
2094 }
2095
2096 /// `Ast::LetMathIn`'s scheme-building rule — the math-command analog of
2097 /// `command_scheme` above, but simpler: a math command has **no**
2098 /// implicit context argument (see `elaborate.rs`'s
2099 /// `elaborate_let_math`), so every domain of `tv`'s function-chain
2100 /// becomes a `CmdArgType` (the same optional-param heuristic as
2101 /// `command_scheme`), and the bare result — not a peeled first argument
2102 /// — must be `math`. A zero-arity binding (`tv` not a `Func` at all,
2103 /// e.g. `let-math \to = rel \`→\``) falls out naturally:
2104 /// `peel_func_chain` returns no domains and `tv` itself as the result.
2105 fn math_command_scheme(
2106 &mut self,
2107 name: &str,
2108 tv: MonoType,
2109 span: Option<Span>,
2110 ) -> Result<PolyType, TypeError> {
2111 let (doms, result) = peel_func_chain(tv);
2112 self.unify_ctx(
2113 &t_math_text(),
2114 &result,
2115 span,
2116 &format!("the result of math command '{name}'"),
2117 )?;
2118 let params: Vec<CmdArgType> = doms
2119 .into_iter()
2120 .map(|d| match resolve(&d).into_owned() {
2121 MonoType::Variant(vname, mut vargs) if vname == "option" && vargs.len() == 1 => {
2122 optional(vargs.pop().unwrap())
2123 }
2124 _ => mandatory(d),
2125 })
2126 .collect();
2127 Ok(generalize(self.ctx.level(), &MonoType::MathCmd(params)))
2128 }
2129
2130 /// `Ast::LetMathIn`'s V0_1 scheme-building rule — the `val math` analog
2131 /// of `math_command_scheme` above. The lowering
2132 /// (`v1/lower.rs::lower_bind_v1`) ALWAYS synthesizes exactly three
2133 /// trailing lambdas around a `val math` body — `fun ctx -> fun sub ->
2134 /// fun sup -> …` — so `tv`'s function chain always has at least 3
2135 /// domains; the LAST three are peeled off as `(d_ctx, d_sub, d_sup)`
2136 /// (`context`, `option math-text`, `option math-text`), the bare result
2137 /// must be `math-boxes`, and the REMAINING leading domains become the
2138 /// command's ordinary `CmdArgType` params.
2139 ///
2140 /// Like `command_scheme`'s V0_1 branch,
2141 /// a leading user parameter may be a `?(l = x, …)` bundle, so this uses
2142 /// the row-carrying `peel_func_chain_rows` + `harvest_slot` instead of
2143 /// the plain `_ option` heuristic. The synthesized ctx/sub/sup trailing
2144 /// trio can never legally carry a bundle (`lower_value_math` always
2145 /// wraps them in plain `fun`s, inferring `Row::Empty`) — guarded
2146 /// defensively, since an off-by-one here would silently turn `sub`/`sup`
2147 /// into a labeled slot or eat the last user param (the trio is at the
2148 /// TAIL of the domain chain, opposite inline/block where ctx is FIRST).
2149 fn math_command_scheme_v01(
2150 &mut self,
2151 name: &str,
2152 tv: MonoType,
2153 span: Option<Span>,
2154 ) -> Result<PolyType, TypeError> {
2155 let (mut slots, result) = peel_func_chain_rows(tv);
2156 if slots.len() < 3 {
2157 return Err(TypeError::simple(
2158 span,
2159 format!(
2160 "'val math' command '{name}' must take a context and (via the \
2161 synthesized `with sub sup`/`%math-attach-scripts` wrapper) two \
2162 optional scripts as its trailing arguments — see the math-split spec"
2163 ),
2164 ));
2165 }
2166 let (row_sup, d_sup) = slots.pop().unwrap();
2167 let (row_sub, d_sub) = slots.pop().unwrap();
2168 let (row_ctx, d_ctx) = slots.pop().unwrap();
2169 for (which, row) in [
2170 ("context", &row_ctx),
2171 ("'sub'", &row_sub),
2172 ("'sup'", &row_sup),
2173 ] {
2174 if !matches!(&*resolve_row(row), Row::Empty) {
2175 return Err(TypeError::simple(
2176 span,
2177 format!(
2178 "the {which} argument of 'val math' command '{name}' cannot \
2179 carry a labeled optional bundle"
2180 ),
2181 ));
2182 }
2183 }
2184 self.unify_ctx(
2185 &t_context(),
2186 &d_ctx,
2187 span,
2188 &format!("the context argument of 'val math' command '{name}'"),
2189 )?;
2190 self.unify_ctx(
2191 &t_option(t_math_text()),
2192 &d_sub,
2193 span,
2194 &format!("the 'sub' argument of 'val math' command '{name}'"),
2195 )?;
2196 self.unify_ctx(
2197 &t_option(t_math_text()),
2198 &d_sup,
2199 span,
2200 &format!("the 'sup' argument of 'val math' command '{name}'"),
2201 )?;
2202 self.unify_ctx(
2203 &t_math_boxes(),
2204 &result,
2205 span,
2206 &format!(
2207 "the result of 0.1 math command '{name}' — a `math-boxes`, \
2208 usually via `read-math`"
2209 ),
2210 )?;
2211 let params: Vec<CmdArgType> = slots
2212 .into_iter()
2213 .map(|(row, dom)| harvest_slot(row, dom))
2214 .collect();
2215 Ok(generalize(self.ctx.level(), &MonoType::MathCmd(params)))
2216 }
2217
2218 /// Shared by `check_itext`'s `IText::Cmd`, `check_btext`'s `BText::Cmd`,
2219 /// and `check_math_elem`'s `MathElem::Cmd`: check a command application's
2220 /// argument count (exact — every optional slot is either explicitly
2221 /// marked at the call site or `None`-padded by elaboration, so it is
2222 /// never actually *absent* from `args`) and each argument's type against
2223 /// `params`. An `optional` param's `args[i].arg` is always a
2224 /// `Some(..)`/`None` value (`app_arg_to_ast`'s desugaring), so it's
2225 /// checked against `option(param.ty)`, not `param.ty` directly.
2226 ///
2227 /// Each `args[i]` additionally carries
2228 /// a (possibly empty) supplied `?(l = e, …)` bundle (`args[i].opts`) —
2229 /// every label must be declared in that slot's own closed
2230 /// `param.opt_labels` map (upstream's `UnexpectedOptionalLabel`,
2231 /// `typechecker.ml:900-901`); a declared label this call omits simply
2232 /// defaults to `None` at runtime, nothing to check here for it. `opts`
2233 /// is `[]` for every 0.0.6-reachable call, so this loop is a no-op
2234 /// there.
2235 fn check_cmd_args(
2236 &mut self,
2237 env: &TypeEnv<'s>,
2238 name: &str,
2239 span: Span,
2240 params: &[CmdArgType],
2241 args: &[CmdArg<'s>],
2242 ) -> Result<(), TypeError> {
2243 if params.len() != args.len() {
2244 return Err(TypeError::simple(
2245 Some(span),
2246 format!(
2247 "command '{name}' expects {} argument{}, got {}",
2248 params.len(),
2249 if params.len() == 1 { "" } else { "s" },
2250 args.len()
2251 ),
2252 ));
2253 }
2254 for (i, (param, arg)) in params.iter().zip(args.iter()).enumerate() {
2255 for (label, val) in &arg.opts {
2256 match param.opt_labels.iter().find(|(l, _)| l == label) {
2257 Some((_, lty)) => {
2258 let tval = self.infer(env, val)?;
2259 self.unify_ctx(
2260 lty,
2261 &tval,
2262 ast_span(val).or(Some(span)),
2263 &format!("optional argument `{label}` of '{name}'"),
2264 )?;
2265 }
2266 None => {
2267 return Err(TypeError::simple(
2268 ast_span(val).or(Some(span)),
2269 format!(
2270 "command '{name}' has no optional label `{label}` \
2271 on argument {}",
2272 i + 1
2273 ),
2274 ));
2275 }
2276 }
2277 }
2278 let targ = self.infer(env, &arg.arg)?;
2279 let expected = if param.optional {
2280 t_option(param.ty.clone())
2281 } else {
2282 param.ty.clone()
2283 };
2284 self.unify_ctx(
2285 &expected,
2286 &targ,
2287 ast_span(&arg.arg).or(Some(span)),
2288 &format!("argument {} of '{name}'", i + 1),
2289 )?;
2290 }
2291 Ok(())
2292 }
2293
2294 // ---- expressions -------------------------------------------------------
2295
2296 /// Infer the scheme(s) of ONE binding against a static `env`,
2297 /// WITHOUT extending anything. Returns the
2298 /// `(name, PolyType)` pairs in binding order (singleton for
2299 /// Let/LetMath/LetMutable; group order for LetRec). The caller decides
2300 /// what to put in the environment — the whole-program path commits them
2301 /// verbatim (`env.with_all`).
2302 ///
2303 /// Level discipline, sigil dispatch, generalization, and the
2304 /// value-restriction asymmetry all live INSIDE this method, so callers
2305 /// cannot get them wrong.
2306 pub(crate) fn infer_binding(
2307 &mut self,
2308 env: &TypeEnv<'s>,
2309 binding: BindingView<'_, 's>,
2310 ) -> Result<Vec<(Symbol<'s>, PolyType)>, TypeError> {
2311 match binding {
2312 BindingView::Let { name, value } => {
2313 self.ctx.enter_level();
2314 let tv = self.infer(env, value)?;
2315 self.ctx.leave_level();
2316 let scheme = match command_sigil(self.text(name)) {
2317 // A `\`/`+`-named binding: either a genuine `let-inline`/
2318 // `let-block` definition (`value` is the
2319 // `Lambda(ctxvar, Lambda(p1, .., body))` chain
2320 // `elaborate_let_inline` builds) or a qualified-name
2321 // alias of one (`value` is a bare `Ast::Var`, from a
2322 // module's own `M.\cmd` re-export or an `open`) — see
2323 // `command_scheme`.
2324 Some(sigil) => {
2325 self.command_scheme(self.text(name), sigil, tv, ast_span(value))?
2326 }
2327 None => generalize(self.ctx.level(), &tv),
2328 };
2329 Ok(vec![(name, scheme)])
2330 }
2331
2332 // `let-math \cmd param* = expr in body` — structurally
2333 // identical to the `Let` command- binding rule above, but for a
2334 // binding that is ALREADY known (by construction, via the
2335 // dedicated Ast variant) to be a math command, so there is no
2336 // sigil to dispatch on and no "which kind of `\`-binding is
2337 // this" ambiguity to resolve.
2338 BindingView::LetMath { name, value } => {
2339 self.ctx.enter_level();
2340 let tv = self.infer(env, value)?;
2341 self.ctx.leave_level();
2342 // V0_0's `let-math` and V0_1's `val math` both lower to the
2343 // SAME `Ast::LetMathIn` — only the SCHEME RULE forks, since
2344 // a `val math` binding's lowering always synthesizes exactly
2345 // three trailing ctx/sub/sup lambdas that
2346 // `math_command_scheme`'s v0.0.6 rule knows nothing about.
2347 //
2348 // It forks on the BINDING's generation, not the session's
2349 // (`binding_version`, not `self.version`): in a merged
2350 // cross-version program the session is always `V0_1` while a
2351 // spliced 0.0.6 package's `let-math` RHS carries its own
2352 // `Ast::VersionScope(V0_0, _)`. On every single-version
2353 // program the two agree by construction (no `VersionScope`
2354 // node exists there at all).
2355 let scheme = if self.binding_version(value).math_is_split() {
2356 self.math_command_scheme_v01(self.text(name), tv, ast_span(value))?
2357 } else {
2358 self.math_command_scheme(self.text(name), tv, ast_span(value))?
2359 };
2360 Ok(vec![(name, scheme)])
2361 }
2362
2363 BindingView::LetRec(bindings) => {
2364 self.ctx.enter_level();
2365 // The group's own stage, so a `@stage: 0` library's mutually
2366 // recursive clauses can still see each other (they are read at
2367 // stage 0, and a stage-0 read of a stage-1 binder is refused).
2368 let group_stage = self.binding_stage_rec(bindings);
2369 let mut rec_env = env.clone();
2370 let mut vars = Vec::with_capacity(bindings.len());
2371 for (name, _) in bindings {
2372 let v = self.fresh();
2373 vars.push(v.clone());
2374 rec_env = rec_env.with(*name, PolyType::mono(v), group_stage);
2375 }
2376 for ((name, val), v) in bindings.iter().zip(vars.iter()) {
2377 let tv = self.infer(&rec_env, val)?;
2378 self.unify_ctx(
2379 v,
2380 &tv,
2381 ast_span(val),
2382 &format!("let-rec binding '{}'", self.text(*name)),
2383 )?;
2384 }
2385 self.ctx.leave_level();
2386 let mut schemes = Vec::with_capacity(bindings.len());
2387 for ((name, _), v) in bindings.iter().zip(vars.iter()) {
2388 let scheme = generalize(self.ctx.level(), v);
2389 schemes.push((*name, scheme));
2390 }
2391 Ok(schemes)
2392 }
2393
2394 BindingView::LetMutable { name, init } => {
2395 // NO generalization: `let-mutable`'s binding is the
2396 // classic ML "value restriction" case — a mutable reference
2397 // must stay monomorphic, or `let-mutable r <- [] in ((r <-
2398 // 1 :: !r); (r <- true :: !r); !r)`-style code could smuggle
2399 // an `int` and a `bool` through the very same cell. Binding
2400 // it via `PolyType::mono` (not `generalize`) enforces this
2401 // directly: every use of `name` in `body` shares the exact
2402 // same `Ref` type, not a fresh instantiation.
2403 let tinit = self.infer(env, init)?;
2404 Ok(vec![(name, PolyType::mono(reff(tinit)))])
2405 }
2406 }
2407 }
2408
2409 /// Infer one expression against a static env — a `pub(crate)` wrapper
2410 /// over the private `infer` below, which stays private so the ~40
2411 /// internal `self.infer(` call sites need not change. `v1::module_check`
2412 /// uses this for the document body and any non-binding expression.
2413 pub(crate) fn infer_expr(
2414 &mut self,
2415 env: &TypeEnv<'s>,
2416 ast: &Ast<'s>,
2417 ) -> Result<MonoType, TypeError> {
2418 self.infer(env, ast)
2419 }
2420
2421 /// Drain accumulated non-fatal match warnings (exhaustiveness /
2422 /// redundancy) — for per-binding callers; the whole-program path reads
2423 /// the `warnings` field directly.
2424 pub(crate) fn take_warnings(&mut self) -> Vec<MatchWarning> {
2425 std::mem::take(&mut self.warnings)
2426 }
2427
2428 /// Mutable access to the inference context — sig lowering needs it
2429 /// (`lower_sig_item(item, &mut ctx)`), and its subsumption check mints
2430 /// fresh vars through it.
2431 pub(crate) fn ctx_mut(&mut self) -> &mut TypeContext {
2432 &mut self.ctx
2433 }
2434
2435 /// Expand every synonym reference inside `ty` against this session's
2436 /// synonym table — a `pub(crate)` accessor over the private free fn
2437 /// [`expand_synonyms`], added for `v1/module_check.rs`:
2438 /// a sig `val`'s declared type may mention the
2439 /// module's own `type t = ..` synonym (by its pre-qualified `"M.t"`
2440 /// name, `v1/lower.rs`'s `TypeNameEnv`), and must expand through the
2441 /// SAME table the impl side's `build_variant_decl`/ordinary inference
2442 /// already does, so e.g. `val f : t -> t` over `type t = int` checks
2443 /// against the impl's expanded `int -> int`.
2444 pub(crate) fn expand_synonyms_in(&self, ty: &MonoType) -> Result<MonoType, TypeError> {
2445 expand_synonyms(ty, &self.synonyms)
2446 }
2447
2448 /// Deregister constructors hidden by a signature seal (
2449 /// `v1/module_check.rs`'s ctor-hide trigger — see that module's doc
2450 /// comment). Each entry is removed only if the currently-registered
2451 /// decl's type name matches, guarding against bare-name ctor collisions
2452 /// (`Checker.ctors` is last-writer-wins program-globally, 0.0.6-
2453 /// inherited): if a LATER unsealed variant re-registered the same ctor
2454 /// name after this one, its entry survives the hide untouched.
2455 pub(crate) fn hide_ctors(&mut self, entries: &[(String, String)]) {
2456 for (ctor, tyname) in entries {
2457 if self.ctors.get(ctor).is_some_and(|d| &d.name == tyname) {
2458 self.ctors.remove(ctor);
2459 }
2460 // Also drop the module-qualified key registered by
2461 // `declare_variant` (guarded by the same decl-identity check).
2462 if let Some((modpfx, _)) = tyname.rsplit_once('.') {
2463 let q = format!("{modpfx}.{ctor}");
2464 if self.ctors.get(&q).is_some_and(|d| &d.name == tyname) {
2465 self.ctors.remove(&q);
2466 }
2467 }
2468 }
2469 }
2470
2471 /// `f ?(l = e, …) arg` — SATySFi 0.1 labeled-optional application
2472 /// inference. Kept OUT of the hot [`Checker::infer`] match (via
2473 /// `#[inline(never)]`) so its labeled-optional locals (`Vec`, `Row`) do
2474 /// not enlarge the deeply-recursed `infer` stack frame — see
2475 /// `infer_lambda_opt`.
2476 #[inline(never)]
2477 fn infer_apply_opt(
2478 &mut self,
2479 env: &TypeEnv<'s>,
2480 func: &Ast<'s>,
2481 opts: &[(String, Ast<'s>)],
2482 arg: &Ast<'s>,
2483 ) -> Result<MonoType, TypeError> {
2484 let tf = self.infer(env, func)?;
2485 let ta = self.infer(env, arg)?;
2486 let tr = self.fresh();
2487 let mut opt_tys = Vec::with_capacity(opts.len());
2488 for (label, e) in opts {
2489 opt_tys.push((label.clone(), self.infer(env, e)?));
2490 }
2491 let mut row = Row::Var(self.ctx.fresh_row_var());
2492 for (label, ty) in opt_tys.into_iter().rev() {
2493 row = Row::Cons(label, Box::new(ty), Box::new(row));
2494 }
2495 self.unify_ctx(
2496 &tf,
2497 &MonoType::Func(Box::new(row), Box::new(ta), Box::new(tr.clone())),
2498 ast_span(func),
2499 "function application",
2500 )?;
2501 Ok(tr)
2502 }
2503
2504 /// `fun ?(l = x, …) p -> body` — SATySFi 0.1 labeled-optional lambda
2505 /// inference. Kept OUT of the hot [`Checker::infer`] match
2506 /// (`#[inline(never)]`): `infer` recurses to the AST depth of the program
2507 /// under check, and Rust sizes a function's stack frame to its LARGEST
2508 /// match arm's locals; inlining this arm's `env.clone()` + `Vec`/`Row`
2509 /// locals into every `infer` frame measurably enlarged the deep recursion
2510 /// (enough to overflow the test harness's small default thread stack on a
2511 /// big merged program). Extracting it restores `infer`'s frame to its
2512 /// pre-optional-arg size.
2513 #[inline(never)]
2514 fn infer_lambda_opt(
2515 &mut self,
2516 env: &TypeEnv<'s>,
2517 opts: &[(String, Symbol<'s>)],
2518 param: Symbol<'s>,
2519 body: &Ast<'s>,
2520 ) -> Result<MonoType, TypeError> {
2521 let mut inner = env.clone();
2522 let mut opt_tys = Vec::with_capacity(opts.len());
2523 for (label, binder) in opts {
2524 let tl = self.fresh();
2525 inner = inner.with(*binder, PolyType::mono(t_option(tl.clone())), self.stage);
2526 opt_tys.push((label.clone(), tl));
2527 }
2528 let tp = self.fresh();
2529 inner = inner.with(param, PolyType::mono(tp.clone()), self.stage);
2530 let tb = self.infer(&inner, body)?;
2531 let mut row = Row::Empty;
2532 for (label, tl) in opt_tys.into_iter().rev() {
2533 row = Row::Cons(label, Box::new(tl), Box::new(row));
2534 }
2535 Ok(MonoType::Func(Box::new(row), Box::new(tp), Box::new(tb)))
2536 }
2537
2538 /// The stage a binding whose right-hand side is `value` is introduced at
2539 /// — upstream's `pre.stage` at the point of `Typeenv.add`.
2540 ///
2541 /// Upstream reads a whole FILE at one stage, so `pre.stage` is simply the
2542 /// ambient stage there. This port flattens every library into one
2543 /// `let`-chain and instead marks each spliced binding's RHS with
2544 /// [`Ast::StageScope`] (`elaborate.rs`'s per-item wrap), so the ambient
2545 /// stage is only right for a binding the current file wrote itself. The
2546 /// peeling through `ModuleScope`/`VersionScope` is because those wrappers
2547 /// are applied INSIDE `push_named_binding`/the `LetRec` arm, i.e. after
2548 /// the stage wrap, for a module member.
2549 ///
2550 /// Only the `ModuleScope` half of that peel actually fires: `elaborate.rs`
2551 /// applies `maybe_v006_scope` to a binding's RHS and `stage_wrap_item`
2552 /// outside it, so a cross-version staged binding is always
2553 /// `StageScope(_, VersionScope(_, ..))`. The `VersionScope` arm is kept
2554 /// anyway, because `elaborate::already_staged` peels the same two and the
2555 /// two must not disagree. Deleting the `ModuleScope` arm breaks
2556 /// `xver_staging.rs`'s
2557 /// `the_file_stage_and_the_version_scope_compose_on_a_module_member`.
2558 pub(crate) fn binding_stage(&self, value: &Ast<'s>) -> Stage {
2559 fn declared<'s>(a: &Ast<'s>) -> Option<Stage> {
2560 match a {
2561 Ast::StageScope(st, _) => Some(*st),
2562 Ast::ModuleScope(_, b) | Ast::VersionScope(_, b) => declared(b),
2563 _ => None,
2564 }
2565 }
2566 declared(value).unwrap_or(self.stage)
2567 }
2568
2569 /// The GENERATION one binding was authored in — the version analogue of
2570 /// [`Checker::binding_stage`], and needed for exactly the same reason: a
2571 /// merged cross-version program has ONE `Checker::version`, hard-coded
2572 /// to `V0_1` (`v1::module_check::check_program_inner`), while each
2573 /// spliced 0.0.6 dependency's bindings carry their own
2574 /// `Ast::VersionScope(V0_0, _)` on the RHS (`elaborate::
2575 /// maybe_v006_scope`). Any scheme rule that FORKS on the version must
2576 /// ask the binding, not the session, or a 0.0.6 package's binding is
2577 /// read under 0.1's rule.
2578 ///
2579 /// Concretely: `let-math`. `math_command_scheme` (0.0.6) and
2580 /// `math_command_scheme_v01` are two different rules for the same
2581 /// `Ast::LetMathIn`, and 0.1's demands three synthesized trailing
2582 /// `ctx`/`sub`/`sup` lambdas a 0.0.6 `let-math \frac = math-frac` does
2583 /// not have — dispatching on `self.version` refused EVERY `let-math` in
2584 /// a crossed 0.0.6 package, including the bundled `math.satyh` that
2585 /// `@require:` reaches transitively (`texlogo`, `latexcmds`, `siunitx`,
2586 /// …).
2587 ///
2588 /// The wrapper peel is `binding_stage`'s, minus its terminating arm:
2589 /// `elaborate::walk_bindings` puts `VersionScope` INSIDE `StageScope`
2590 /// (`already_staged`'s doc comment), so a staged spliced binding is
2591 /// `StageScope(_, VersionScope(_, ..))` and this must look through
2592 /// `StageScope` too.
2593 ///
2594 /// The peel alone is not enough, hence the `scoped_version` fallback:
2595 /// `maybe_v006_scope` wraps a TOP-LEVEL binding's RHS, but an
2596 /// EXPRESSION-level `let-math \c = e in body` (`elaborate.rs`'s
2597 /// `Expr::LetMathIn` arm, e.g. `siunitx`'s `let-math \C = ord \`C\` in
2598 /// ${\math-sup{}{\circ}\C}`) is a node inside another binding's
2599 /// already-wrapped RHS and carries no wrapper of its own — so the
2600 /// ambient generation, recorded by the `Ast::VersionScope` infer arm as
2601 /// it descends, answers for it instead. Outside every scope this is
2602 /// `None` and the session's own version answers.
2603 pub(crate) fn binding_version(&self, value: &Ast<'s>) -> RustyfiVersion {
2604 fn declared<'s>(a: &Ast<'s>) -> Option<RustyfiVersion> {
2605 match a {
2606 Ast::VersionScope(v, _) => Some(*v),
2607 Ast::StageScope(_, b) | Ast::ModuleScope(_, b) => declared(b),
2608 _ => None,
2609 }
2610 }
2611 declared(value)
2612 .or(self.scoped_version)
2613 .unwrap_or(self.version)
2614 }
2615
2616 /// [`Checker::binding_stage`] for a `let-rec` GROUP. Every clause of one
2617 /// group comes from one item of one file, so they share a stage; the
2618 /// elaborator wraps them identically and the first is representative.
2619 pub(crate) fn binding_stage_rec(&self, bindings: &[(Symbol<'s>, Rc<Ast<'s>>)]) -> Stage {
2620 match bindings.first() {
2621 Some((_, v)) => self.binding_stage(v),
2622 None => self.stage,
2623 }
2624 }
2625
2626 /// Look `name` up for an OCCURRENCE, enforcing the staging matrix
2627 /// ([`Stage::can_reference`]) — upstream's `UTContentOf` arm, which is
2628 /// where a stage-0 name used from stage 1 (or the reverse) is refused.
2629 ///
2630 /// `Ok(None)` means unbound, left to each caller because each has its own
2631 /// "should not happen post-elaboration" wording. `what` names the kind of
2632 /// occurrence for the diagnostic (`"variable"`, `"inline command"`, …).
2633 fn staged<'e>(
2634 &self,
2635 env: &'e TypeEnv<'s>,
2636 name: Symbol<'s>,
2637 span: Option<Span>,
2638 what: &str,
2639 ) -> Result<Option<&'e PolyType>, TypeError> {
2640 let Some(entry) = env.entry(name) else {
2641 return Ok(None);
2642 };
2643 if !self.stage.can_reference(entry.stage) {
2644 return Err(TypeError::simple(
2645 span,
2646 format!(
2647 "invalid occurrence of {what} '{}' as to stage: it is bound at {}, \
2648 but this is {}",
2649 self.text(name),
2650 entry.stage.as_str(),
2651 self.stage.as_str()
2652 ),
2653 ));
2654 }
2655 Ok(Some(&entry.poly))
2656 }
2657
2658 /// `infer`, reading `ast` at a different stage and restoring afterwards --
2659 /// the type-side twin of the `ctor_scope` push/pop below it.
2660 fn infer_at(
2661 &mut self,
2662 stage: Stage,
2663 env: &TypeEnv<'s>,
2664 ast: &Ast<'s>,
2665 ) -> Result<MonoType, TypeError> {
2666 let saved = std::mem::replace(&mut self.stage, stage);
2667 let result = self.infer(env, ast);
2668 self.stage = saved;
2669 result
2670 }
2671
2672 fn infer(&mut self, env: &TypeEnv<'s>, ast: &Ast<'s>) -> Result<MonoType, TypeError> {
2673 match ast {
2674 // A binding spliced in from a file whose `@stage:` was not the
2675 // default: read it at that stage, so its quotes are legal.
2676 Ast::StageScope(stage, body) => self.infer_at(*stage, env, body),
2677 // `&e` — quote. Legal only at stage 0; its body is read one
2678 // stage later, and the result is that body's type wrapped in
2679 // `code` (upstream `typechecker.ml`'s `UTNext` arm).
2680 Ast::Next(inner) => {
2681 if self.stage != Stage::Stage0 {
2682 return Err(TypeError::simple(
2683 None,
2684 format!(
2685 "`&` (next-stage quote) is only valid at stage 0, but this is {}",
2686 self.stage.as_str()
2687 ),
2688 ));
2689 }
2690 let ty = self.infer_at(Stage::Stage1, env, inner)?;
2691 Ok(MonoType::Code(Box::new(ty)))
2692 }
2693 // `~e` — splice. Legal only at stage 1; its body is read one stage
2694 // earlier and must produce `code b`, which this expression then
2695 // stands for (upstream's `UTPrev` arm).
2696 Ast::Prev(inner) => {
2697 if self.stage != Stage::Stage1 {
2698 return Err(TypeError::simple(
2699 None,
2700 format!(
2701 "`~` (previous-stage splice) is only valid at stage 1, but this is {}",
2702 self.stage.as_str()
2703 ),
2704 ));
2705 }
2706 let ty = self.infer_at(Stage::Stage0, env, inner)?;
2707 let beta = MonoType::Var(self.ctx.fresh_var());
2708 unify(&ty, &MonoType::Code(Box::new(beta.clone())))
2709 .map_err(|e| TypeError::from_unify(None, "a `~` splice", e))?;
2710 Ok(beta)
2711 }
2712 Ast::Unit => Ok(t_unit()),
2713 Ast::Bool(_) => Ok(t_bool()),
2714 Ast::Int(_) => Ok(t_int()),
2715 Ast::Float(_) => Ok(t_float()),
2716 Ast::Length(_) => Ok(t_length()),
2717 Ast::Str(_) => Ok(t_string()),
2718
2719 Ast::Var(name, span) => match self.staged(env, *name, Some(*span), "variable")? {
2720 Some(poly) => Ok(instantiate(poly, self.ctx.level())),
2721 // Should not happen post-elaboration: `elaborate.rs`'s
2722 // `scoped_var` already rejects any unbound name before this
2723 // ever runs. Surfaced as a (spanned) error rather than a
2724 // panic anyway, since "should not happen" isn't "cannot".
2725 None => Err(TypeError::simple(
2726 Some(*span),
2727 format!(
2728 "internal error: unbound variable '{}' reached the typechecker",
2729 self.text(*name)
2730 ),
2731 )),
2732 },
2733
2734 Ast::Apply(f, a) => {
2735 let tf = self.infer(env, f)?;
2736 let ta = self.infer(env, a)?;
2737 let tr = self.fresh();
2738 // Version-split the optional-argument row: 0.0.6 functions
2739 // provably carry no labeled optionals (`Row::Empty`, matching
2740 // `arrow()` and every prim). Under 0.1 a fresh open row var
2741 // absorbs the callee's
2742 // declared optional row, letting a *plain* call of an
2743 // opt-taking function typecheck (defaulting every optional to
2744 // `None` at run time) and making higher-order code
2745 // row-polymorphic after generalization.
2746 let opts_row = if self.version.has_row_polymorphism() {
2747 Row::Var(self.ctx.fresh_row_var())
2748 } else {
2749 Row::Empty
2750 };
2751 self.unify_ctx(
2752 &tf,
2753 &MonoType::Func(Box::new(opts_row), Box::new(ta), Box::new(tr.clone())),
2754 ast_span(f),
2755 "function application",
2756 )?;
2757 Ok(tr)
2758 }
2759
2760 Ast::Lambda(param, body) => {
2761 let tp = self.fresh();
2762 let inner = env.with(*param, PolyType::mono(tp.clone()), self.stage);
2763 let tb = self.infer(&inner, body)?;
2764 Ok(arrow(tp, tb))
2765 }
2766
2767 // `f ?(l = e, …) arg` (SATySFi 0.1; upstream typechecker.ml's
2768 // `Apply(labmap, …)`). The callee must unify with a function
2769 // whose optional row carries at least each supplied `l : τ_l`,
2770 // with an open tail (a fresh row var) for any further optionals
2771 // the callee declares that this call omits.
2772 Ast::ApplyOpt { func, opts, arg } => self.infer_apply_opt(env, func, opts, arg),
2773
2774 // `fun ?(l = x, …) p -> body` (SATySFi 0.1; upstream
2775 // `Function(evid_labmap, …)`). Each labeled optional binder `x`
2776 // is bound at `option τ_l` inside the body; the resulting
2777 // function type carries a CLOSED row `?(l : τ_l, …)` — the very
2778 // same fresh `τ_l` shared between the binder's `option τ_l` and
2779 // the row's `Cons(l, τ_l)`.
2780 Ast::LambdaOpt { opts, param, body } => self.infer_lambda_opt(env, opts, *param, body),
2781
2782 Ast::LetIn(name, value, body) => {
2783 let schemes = self.infer_binding(env, BindingView::Let { name: *name, value })?;
2784 let inner = env.with_all(schemes, self.binding_stage(value));
2785 self.infer(&inner, body)
2786 }
2787
2788 // `let-math \cmd param* = expr in body` — see
2789 // `infer_binding`'s `BindingView::LetMath` arm.
2790 Ast::LetMathIn(name, value, body) => {
2791 let schemes =
2792 self.infer_binding(env, BindingView::LetMath { name: *name, value })?;
2793 let inner = env.with_all(schemes, self.binding_stage(value));
2794 self.infer(&inner, body)
2795 }
2796
2797 Ast::LetRecIn(bindings, body) => {
2798 let schemes = self.infer_binding(env, BindingView::LetRec(bindings))?;
2799 let inner = env.with_all(schemes, self.binding_stage_rec(bindings));
2800 self.infer(&inner, body)
2801 }
2802
2803 Ast::IfThenElse(cond, then_b, else_b) => {
2804 let tc = self.infer(env, cond)?;
2805 self.unify_ctx(&t_bool(), &tc, ast_span(cond), "the condition of 'if'")?;
2806 let tt = self.infer(env, then_b)?;
2807 let te = self.infer(env, else_b)?;
2808 self.unify_ctx(&tt, &te, ast_span(else_b), "the branches of 'if'")?;
2809 Ok(tt)
2810 }
2811
2812 Ast::Match(scrutinee, arms) => {
2813 let tscrut = self.infer(env, scrutinee)?;
2814 let mut result: Option<MonoType> = None;
2815 for arm in arms {
2816 let arm_env = self.bind_pattern(env.clone(), &arm.pat, &tscrut)?;
2817 if let Some(guard) = &arm.guard {
2818 let tg = self.infer(&arm_env, guard)?;
2819 self.unify_ctx(&t_bool(), &tg, ast_span(guard), "a match guard")?;
2820 }
2821 let tbody = self.infer(&arm_env, &arm.body)?;
2822 match &result {
2823 None => result = Some(tbody),
2824 Some(r) => {
2825 self.unify_ctx(r, &tbody, ast_span(&arm.body), "the arms of 'match'")?
2826 }
2827 }
2828 }
2829 // Exhaustiveness/redundancy: non-fatal, so it runs
2830 // only after every arm has
2831 // typechecked, against `tscrut` as resolved as inference will
2832 // ever make it. See `exhaustive::check_match`'s doc comment.
2833 let resolved_scrut = resolve(&tscrut);
2834 let new_warnings = crate::exhaustive::check_match(
2835 self.store,
2836 &resolved_scrut,
2837 ast_span(scrutinee),
2838 arms,
2839 &self.variants,
2840 );
2841 self.warnings.extend(new_warnings);
2842 // `Match`'s `arms` is always non-empty (`c::Expr::Match`
2843 // requires a `first` arm plus zero or more `rest`), so
2844 // `result` is always `Some` in practice; the fallback fresh
2845 // variable is defensive only.
2846 Ok(result.unwrap_or_else(|| self.fresh()))
2847 }
2848
2849 Ast::Tuple(items) => {
2850 let tys = items
2851 .iter()
2852 .map(|it| self.infer(env, it))
2853 .collect::<Result<Vec<_>, _>>()?;
2854 Ok(product(tys))
2855 }
2856
2857 Ast::Ctor(name, payload) => self.infer_ctor(env, name, payload.as_deref(), None),
2858
2859 Ast::Record(fields) => {
2860 let mut typed = Vec::with_capacity(fields.len());
2861 for (label, e) in fields {
2862 typed.push((label.clone(), self.infer(env, e)?));
2863 }
2864 let mut row = Row::Empty;
2865 for (label, ty) in typed.into_iter().rev() {
2866 row = Row::Cons(label, Box::new(ty), Box::new(row));
2867 }
2868 Ok(MonoType::Record(row))
2869 }
2870
2871 Ast::List(items) => {
2872 let elem = self.fresh();
2873 for it in items {
2874 let t = self.infer(env, it)?;
2875 self.unify_ctx(&elem, &t, ast_span(it), "a list element")?;
2876 }
2877 Ok(list(elem))
2878 }
2879
2880 Ast::InlineText(elems) => {
2881 for e in elems.iter() {
2882 self.check_itext(env, e)?;
2883 }
2884 Ok(t_inline_text())
2885 }
2886
2887 Ast::BlockText(elems) => {
2888 for e in elems.iter() {
2889 self.check_btext(env, e)?;
2890 }
2891 Ok(t_block_text())
2892 }
2893
2894 Ast::MathText(elems) => {
2895 for e in elems.iter() {
2896 self.check_math_elem(env, e)?;
2897 }
2898 Ok(MonoType::Base(BaseType::MathText))
2899 }
2900
2901 Ast::LetMutableIn(name, init, body) => {
2902 // NO generalization: `let-mutable`'s binding is the
2903 // classic ML "value restriction" case — see
2904 // `infer_binding`'s `BindingView::LetMutable` arm.
2905 let schemes =
2906 self.infer_binding(env, BindingView::LetMutable { name: *name, init })?;
2907 let inner = env.with_all(schemes, self.binding_stage(init));
2908 self.infer(&inner, body)
2909 }
2910
2911 Ast::Overwrite(name, span, value) => {
2912 let t_ref = match self.staged(env, *name, Some(*span), "mutable variable")? {
2913 Some(poly) => instantiate(poly, self.ctx.level()),
2914 None => {
2915 return Err(TypeError::simple(
2916 Some(*span),
2917 format!(
2918 "internal error: unbound mutable variable '{}' reached the typechecker",
2919 self.text(*name)
2920 ),
2921 ))
2922 }
2923 };
2924 let inner = self.fresh();
2925 self.unify_ctx(
2926 &t_ref,
2927 &reff(inner.clone()),
2928 Some(*span),
2929 &format!("the overwrite target '{}'", self.text(*name)),
2930 )?;
2931 let tvalue = self.infer(env, value)?;
2932 // Prefer the overwrite's own (always-present) span over
2933 // `ast_span(value)`, which is `None` for most value shapes
2934 // (literals carry no span at all — see `ast.rs`'s module
2935 // doc comment) and would otherwise leave this common error
2936 // unlocated.
2937 self.unify_ctx(
2938 &inner,
2939 &tvalue,
2940 ast_span(value).or(Some(*span)),
2941 &format!("the overwrite value for '{}'", self.text(*name)),
2942 )?;
2943 Ok(t_unit())
2944 }
2945
2946 Ast::WhileDo(cond, body) => {
2947 let tc = self.infer(env, cond)?;
2948 self.unify_ctx(&t_bool(), &tc, ast_span(cond), "the condition of 'while'")?;
2949 let tb = self.infer(env, body)?;
2950 self.unify_ctx(&t_unit(), &tb, ast_span(body), "the body of 'while'")?;
2951 Ok(t_unit())
2952 }
2953
2954 Ast::Sequential(a, b) => {
2955 let ta = self.infer(env, a)?;
2956 // v0.0.6 requires the left-hand side of `before`/`;` to be
2957 // `unit` (`typechecker.ml`'s `UTSequential` case): not just
2958 // "evaluated and discarded" but type-checked as `unit`
2959 // specifically, so e.g. a stray non-unit expression used
2960 // only for effect (but returning, say, an `int`) is rejected
2961 // rather than silently ignored.
2962 self.unify_ctx(
2963 &t_unit(),
2964 &ta,
2965 ast_span(a),
2966 "the left-hand side of 'before'",
2967 )?;
2968 self.infer(env, b)
2969 }
2970
2971 Ast::AccessField(e, label, span) => {
2972 let te = self.infer(env, e)?;
2973 let field = self.fresh();
2974 let rv = self.ctx.fresh_row_var();
2975 let open_row = MonoType::Record(Row::Cons(
2976 label.clone(),
2977 Box::new(field.clone()),
2978 Box::new(Row::Var(rv)),
2979 ));
2980 self.unify_ctx(
2981 &open_row,
2982 &te,
2983 Some(*span),
2984 &format!("the field access '#{label}'"),
2985 )?;
2986 Ok(field)
2987 }
2988
2989 Ast::UpdateField(base, label, value) => {
2990 let tbase = self.infer(env, base)?;
2991 let tvalue = self.infer(env, value)?;
2992 let rv = self.ctx.fresh_row_var();
2993 let open_row = MonoType::Record(Row::Cons(
2994 label.clone(),
2995 Box::new(tvalue),
2996 Box::new(Row::Var(rv)),
2997 ));
2998 self.unify_ctx(
2999 &open_row,
3000 &tbase,
3001 ast_span(base),
3002 &format!("the record update of '{label}'"),
3003 )?;
3004 Ok(tbase)
3005 }
3006
3007 // Swap the active primitive-type env to `version`'s
3008 // for `body` — see `version_scoped_type_env`'s doc comment —
3009 // and make sure `version`'s builtin ADTs (e.g. `page`,
3010 // `V0_0`-only) are registered in the (otherwise
3011 // whole-program-tagged) ctor table — see
3012 // `install_additional_builtin_variants`'s doc comment. Never
3013 // reached on a pure single-version program (no
3014 // `Ast::VersionScope` node is ever produced there).
3015 Ast::VersionScope(version, body) => {
3016 self.install_additional_builtin_variants(*version);
3017 let scoped = version_scoped_type_env(self.store, env, *version);
3018 // Also make the generation available to any version-forking
3019 // rule reached INSIDE the body — an expression-level
3020 // `let-math .. in ..` is the one that needs it, since its
3021 // own `Ast::LetMathIn` carries no wrapper of its own. See
3022 // `Checker::binding_version`.
3023 let saved = self.scoped_version.replace(*version);
3024 let r = self.infer(&scoped, body);
3025 self.scoped_version = saved;
3026 r
3027 }
3028 // A module member's body: resolve its bare constructor references
3029 // against `path`'s constructors first. `path` is the full absolute
3030 // module path (nested modules wrap with `["M","N"]`), so replace
3031 // rather than push.
3032 Ast::ModuleScope(path, body) => {
3033 let saved = std::mem::replace(&mut self.ctor_scope, path.clone());
3034 let r = self.infer(env, body);
3035 self.ctor_scope = saved;
3036 r
3037 }
3038 }
3039 }
3040
3041 /// Look up a constructor honoring the current [`Checker::ctor_scope`]: try
3042 /// the innermost-out module-qualified keys (`M.N.Ctor`, `M.Ctor`) before
3043 /// the bare fallback (`Ctor`). Keeps the returned decl's ctor NAME strings
3044 /// bare — only the table KEY is qualified — so eval/exhaustiveness/error
3045 /// text are untouched.
3046 fn lookup_ctor(&self, name: &str) -> Option<Rc<VariantDecl>> {
3047 for k in (1..=self.ctor_scope.len()).rev() {
3048 let key = format!("{}.{}", self.ctor_scope[..k].join("."), name);
3049 if let Some(d) = self.ctors.get(&key) {
3050 return Some(d.clone());
3051 }
3052 }
3053 self.ctors.get(name).cloned()
3054 }
3055
3056 /// Shared by `Ast::Ctor` and pattern-matching's `Pattern::Ctor`: look up
3057 /// `name`'s declaration, mint fresh type arguments for its (possibly
3058 /// zero) parameters, and check the payload — either an already-inferred
3059 /// expression type to unify against (`Ast::Ctor`'s case, via `infer`
3060 /// directly) or nothing (patterns bind their own payload separately, in
3061 /// `bind_pattern`). `expected_result`, if given, is unified against the
3062 /// application's result type — used by nothing yet in this port's
3063 /// rules but kept general for symmetry; always `None` from `infer`,
3064 /// which just returns the result type instead.
3065 fn infer_ctor(
3066 &mut self,
3067 env: &TypeEnv<'s>,
3068 name: &str,
3069 payload: Option<&Ast<'s>>,
3070 expected_result: Option<&MonoType>,
3071 ) -> Result<MonoType, TypeError> {
3072 let decl = self
3073 .lookup_ctor(name)
3074 .ok_or_else(|| TypeError::simple(None, format!("unknown constructor '{name}'")))?;
3075 let args: Vec<MonoType> = (0..decl.params).map(|_| self.fresh()).collect();
3076 let (payload_ty, result_ty) = decl.instantiate_ctor(name, &args).ok_or_else(|| {
3077 TypeError::simple(
3078 None,
3079 format!("constructor '{name}' applied with the wrong number of type arguments"),
3080 )
3081 })?;
3082 if let Some(expected) = expected_result {
3083 self.unify_ctx(expected, &result_ty, None, &format!("constructor '{name}'"))?;
3084 }
3085 match (payload_ty, payload) {
3086 (Some(expected), Some(actual)) => {
3087 let actual_ty = self.infer(env, actual)?;
3088 self.unify_ctx(
3089 &expected,
3090 &actual_ty,
3091 ast_span(actual),
3092 &format!("the payload of constructor '{name}'"),
3093 )?;
3094 }
3095 (None, None) => {}
3096 (Some(_), None) => {
3097 return Err(TypeError::simple(
3098 None,
3099 format!("constructor '{name}' expects a payload but none was given"),
3100 ))
3101 }
3102 (None, Some(_)) => {
3103 return Err(TypeError::simple(
3104 None,
3105 format!("constructor '{name}' takes no payload but one was given"),
3106 ))
3107 }
3108 }
3109 Ok(result_ty)
3110 }
3111
3112 // ---- patterns ------------------------------------------------------
3113
3114 /// Type-check `pat` against `ty`, extending (a clone of) `env` with
3115 /// every name it binds. Mirrors `typechecker.ml`'s `typecheck_pattern`.
3116 fn bind_pattern(
3117 &mut self,
3118 env: TypeEnv<'s>,
3119 pat: &Pattern<'s>,
3120 ty: &MonoType,
3121 ) -> Result<TypeEnv<'s>, TypeError> {
3122 match pat {
3123 Pattern::Wild => Ok(env),
3124 Pattern::Var(name) => Ok(env.with(*name, PolyType::mono(ty.clone()), self.stage)),
3125 Pattern::Unit => {
3126 self.unify_ctx(&t_unit(), ty, None, "a unit pattern")?;
3127 Ok(env)
3128 }
3129 Pattern::Bool(_) => {
3130 self.unify_ctx(&t_bool(), ty, None, "a boolean pattern")?;
3131 Ok(env)
3132 }
3133 Pattern::Int(_) => {
3134 self.unify_ctx(&t_int(), ty, None, "an integer pattern")?;
3135 Ok(env)
3136 }
3137 Pattern::Str(_) => {
3138 self.unify_ctx(&t_string(), ty, None, "a string pattern")?;
3139 Ok(env)
3140 }
3141 Pattern::Tuple(pats) => {
3142 let elem_tys: Vec<MonoType> = pats.iter().map(|_| self.fresh()).collect();
3143 self.unify_ctx(&product(elem_tys.clone()), ty, None, "a tuple pattern")?;
3144 let mut env = env;
3145 for (p, t) in pats.iter().zip(elem_tys.iter()) {
3146 env = self.bind_pattern(env, p, t)?;
3147 }
3148 Ok(env)
3149 }
3150 Pattern::EmptyList => {
3151 let elem = self.fresh();
3152 self.unify_ctx(&list(elem), ty, None, "an empty-list pattern")?;
3153 Ok(env)
3154 }
3155 Pattern::Cons(head, tail) => {
3156 let elem = self.fresh();
3157 self.unify_ctx(&list(elem.clone()), ty, None, "a cons pattern")?;
3158 let env = self.bind_pattern(env, head, &elem)?;
3159 self.bind_pattern(env, tail, &list(elem))
3160 }
3161 Pattern::Ctor(name, payload) => {
3162 let decl = self.lookup_ctor(name).ok_or_else(|| {
3163 TypeError::simple(None, format!("unknown constructor '{name}' in a pattern"))
3164 })?;
3165 let args: Vec<MonoType> = (0..decl.params).map(|_| self.fresh()).collect();
3166 let (payload_ty, result_ty) = decl.instantiate_ctor(name, &args).ok_or_else(|| {
3167 TypeError::simple(
3168 None,
3169 format!(
3170 "constructor '{name}' applied with the wrong number of type arguments in a pattern"
3171 ),
3172 )
3173 })?;
3174 self.unify_ctx(
3175 &result_ty,
3176 ty,
3177 None,
3178 &format!("the constructor pattern '{name}'"),
3179 )?;
3180 match (payload_ty, payload) {
3181 (Some(expected), Some(p)) => self.bind_pattern(env, p, &expected),
3182 (None, None) => Ok(env),
3183 (Some(_), None) => Err(TypeError::simple(
3184 None,
3185 format!(
3186 "constructor pattern '{name}' expects a payload but none was given"
3187 ),
3188 )),
3189 (None, Some(_)) => Err(TypeError::simple(
3190 None,
3191 format!("constructor pattern '{name}' takes no payload but one was given"),
3192 )),
3193 }
3194 }
3195 Pattern::As(inner, name) => {
3196 let env = self.bind_pattern(env, inner, ty)?;
3197 Ok(env.with(*name, PolyType::mono(ty.clone()), self.stage))
3198 }
3199 }
3200 }
3201
3202 // ---- inline / block / math text -------------------------------------
3203
3204 /// Check one inline-text element. A command's own type is a genuine
3205 /// `MonoType::InlineCmd(params)` (`[...] inline-cmd`, mirroring v0.0.6's
3206 /// `HorzCommandType`) — bound either by `Ast::LetIn`'s command-binding
3207 /// rule (`Checker::command_scheme`) or, for the port's built-in
3208 /// commands, directly by `prim_types::primitive_type`'s `\emph` entry.
3209 /// Checking an application here is exact-arity plus one unification per
3210 /// argument against `params`, via `check_cmd_args` — there is no
3211 /// `context -> arg1 -> .. -> inline-boxes` function shape to unify the
3212 /// whole command type against.
3213 fn check_itext(&mut self, env: &TypeEnv<'s>, it: &IText<'s>) -> Result<(), TypeError> {
3214 match it {
3215 IText::Text(_) | IText::CodeText(_) => Ok(()),
3216 IText::Cmd { name, span, args } => {
3217 let tcmd = match self.staged(env, *name, Some(*span), "inline command")? {
3218 Some(poly) => instantiate(poly, self.ctx.level()),
3219 None => {
3220 return Err(TypeError::simple(
3221 Some(*span),
3222 format!(
3223 "internal error: unbound inline command '{}' reached the typechecker",
3224 self.text(*name)
3225 ),
3226 ))
3227 }
3228 };
3229 match &*resolve(&tcmd) {
3230 MonoType::InlineCmd(params) => {
3231 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3232 }
3233 other => Err(TypeError::simple(
3234 Some(*span),
3235 format!(
3236 "internal error: inline command '{}' does not have an \
3237 inline-cmd type (found `{other}`)",
3238 self.text(*name)
3239 ),
3240 )),
3241 }
3242 }
3243 IText::Embed { expr, span } => {
3244 let te = self.infer(env, expr)?;
3245 self.unify_ctx(
3246 &t_inline_text(),
3247 &te,
3248 Some(*span),
3249 "an inline-text '#…;' embed",
3250 )?;
3251 Ok(())
3252 }
3253 IText::EmbedMath { elems, span: _ } => {
3254 // PERMISSIVE: there is no real type to check a quoted-math
3255 // embed's expressions against. Type each against its own
3256 // fresh variable, purely so unbound-name mistakes inside it
3257 // are still caught, without asserting anything about the
3258 // result.
3259 for me in elems.iter() {
3260 self.check_math_elem(env, me)?;
3261 }
3262 Ok(())
3263 }
3264 }
3265 }
3266
3267 /// Block-text analogue of `check_itext`'s `IText::Cmd` case — see its
3268 /// doc comment; a `BText::Cmd`'s type is `MonoType::BlockCmd(params)`.
3269 fn check_btext(&mut self, env: &TypeEnv<'s>, bt: &BText<'s>) -> Result<(), TypeError> {
3270 match bt {
3271 BText::Cmd { name, span, args } => {
3272 let tcmd = match self.staged(env, *name, Some(*span), "block command")? {
3273 Some(poly) => instantiate(poly, self.ctx.level()),
3274 None => {
3275 return Err(TypeError::simple(
3276 Some(*span),
3277 format!(
3278 "internal error: unbound block command '{}' reached the typechecker",
3279 self.text(*name)
3280 ),
3281 ))
3282 }
3283 };
3284 match &*resolve(&tcmd) {
3285 MonoType::BlockCmd(params) => {
3286 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3287 }
3288 other => Err(TypeError::simple(
3289 Some(*span),
3290 format!(
3291 "internal error: block command '{}' does not have a \
3292 block-cmd type (found `{other}`)",
3293 self.text(*name)
3294 ),
3295 )),
3296 }
3297 }
3298 BText::Embed { expr, span } => {
3299 let te = self.infer(env, expr)?;
3300 self.unify_ctx(
3301 &t_block_text(),
3302 &te,
3303 Some(*span),
3304 "a block-text '#…;' embed",
3305 )?;
3306 Ok(())
3307 }
3308 }
3309 }
3310
3311 /// Walk one quoted math element. `Chars`/`Group`/`Sub`/`Sup`/`Primes`
3312 /// carry no program-mode content of their own (nothing to check beyond
3313 /// recursing). `Cmd`/`Embed` are where math meets the ordinary
3314 /// expression language: a `Cmd`'s `name` must resolve to a genuine
3315 /// `MathCmd` type (checked exactly like `check_itext`'s `IText::Cmd`,
3316 /// via `check_cmd_args` — a math command's optional `?:`/`?*`-marked
3317 /// or marker-less-padded arguments are handled by `check_cmd_args` the
3318 /// same generic way), and an `Embed`'s (`#expr`) type must unify with
3319 /// `math` (a math command parameter, or another program-mode value
3320 /// that itself produces math — `Value::Math`/ `Value::MathText` are
3321 /// the two runtime shapes this unifies against, see `value.rs`).
3322 fn check_math_elem(&mut self, env: &TypeEnv<'s>, m: &MathElem<'s>) -> Result<(), TypeError> {
3323 match m {
3324 MathElem::Chars(_) => Ok(()),
3325 MathElem::Group(elems) => {
3326 for e in elems {
3327 self.check_math_elem(env, e)?;
3328 }
3329 Ok(())
3330 }
3331 MathElem::Sub(base, script) | MathElem::Sup(base, script) => {
3332 self.check_math_elem(env, base)?;
3333 for e in script {
3334 self.check_math_elem(env, e)?;
3335 }
3336 Ok(())
3337 }
3338 MathElem::Primes(base, _) => self.check_math_elem(env, base),
3339 MathElem::Cmd { name, span, args } => {
3340 let tcmd = match self.staged(env, *name, Some(*span), "math command")? {
3341 Some(poly) => instantiate(poly, self.ctx.level()),
3342 None => {
3343 return Err(TypeError::simple(
3344 Some(*span),
3345 format!(
3346 "internal error: unbound math command '{}' reached the typechecker",
3347 self.text(*name)
3348 ),
3349 ))
3350 }
3351 };
3352 match &*resolve(&tcmd) {
3353 MonoType::MathCmd(params) => {
3354 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3355 }
3356 other => Err(TypeError::simple(
3357 Some(*span),
3358 format!(
3359 "internal error: math command '{}' does not have a \
3360 math-cmd type (found `{other}`)",
3361 self.text(*name)
3362 ),
3363 )),
3364 }
3365 }
3366 MathElem::Embed { expr, span } => {
3367 let te = self.infer(env, expr)?;
3368 self.unify_ctx(&t_math_text(), &te, Some(*span), "a math '#…' embed")?;
3369 Ok(())
3370 }
3371 }
3372 }
3373}
3374
3375/// If `name` (an `Ast::LetIn` binding's name) is command-shaped, the sigil
3376/// that says which kind — `'\\'` for an inline command, `'+'` for a block
3377/// command — else `None` for an ordinary variable binding.
3378///
3379/// Looks only at the *local* segment (after the last `.`): a
3380/// module-qualified command is spelled e.g. `"M.\cmd"`, sigil on the local
3381/// part only (module names can never start with `\`/`+` — `qualify_key`'s
3382/// doc comment). A bare name has no `.` at all, so `rsplit('.').next()`
3383/// degrades to the whole string.
3384///
3385/// **Must also check the second character.** A genuine command sigil is
3386/// always immediately followed by an identifier, but a parenthesized
3387/// operator NAME (`cst.rs`'s `BindName`) can merely *start* with the same
3388/// character, e.g. `let (+++>) = ..` (`itemize.satyh`) or `let (+.) = ..`.
3389/// Requiring an alphabetic second character mirrors the lexer's own split
3390/// and keeps such an operator name an ordinary variable binding rather than
3391/// a false-positive command.
3392fn command_sigil(name: &str) -> Option<char> {
3393 let local = name.rsplit('.').next().unwrap_or(name);
3394 let mut chars = local.chars();
3395 match chars.next() {
3396 Some(c @ ('\\' | '+')) if chars.next().is_some_and(|c2| c2.is_ascii_alphabetic()) => {
3397 Some(c)
3398 }
3399 _ => None,
3400 }
3401}
3402
3403/// Greedily unwrap a (resolved) `Func` chain into its list of domains and
3404/// final codomain: `dom1 -> dom2 -> .. -> domN -> result` becomes
3405/// `(vec![dom1, .., domN], result)`. Only ever follows the *codomain* at
3406/// each step (never recurses into a domain, even one that is itself a
3407/// `Func`) — used by `Checker::command_scheme` to recover a `let-inline`/
3408/// `let-block` binding's `context -> arg1 -> .. -> argN -> result` shape
3409/// from its ordinarily-inferred function type.
3410fn peel_func_chain(ty: MonoType) -> (Vec<MonoType>, MonoType) {
3411 let mut doms = Vec::new();
3412 let mut cur = ty;
3413 loop {
3414 // Owned: this walk moves each arrow's domain/codomain out.
3415 match resolve(&cur).into_owned() {
3416 MonoType::Func(_row, dom, cod) => {
3417 doms.push(*dom);
3418 cur = *cod;
3419 }
3420 other => return (doms, other),
3421 }
3422 }
3423}
3424
3425/// [`peel_func_chain`]'s row-carrying twin:
3426/// same greedy unwrap, but keeps each arrow's own (resolved) optional-
3427/// argument [`Row`] alongside its domain, since a V0_1 command's `LambdaOpt`-
3428/// produced arrows carry each parameter's `?(l:τ,…)` bundle on that
3429/// PARAMETER's own arrow (the arrow whose *domain* is the labeled argument —
3430/// see `Checker::command_scheme`'s V0_1 harvest). Used only by
3431/// `command_scheme`; `check_cmd_args`/`math_command_scheme*` still use the
3432/// row-blind `peel_func_chain` (they never harvest labels).
3433fn peel_func_chain_rows(ty: MonoType) -> (Vec<(Row, MonoType)>, MonoType) {
3434 let mut slots = Vec::new();
3435 let mut cur = ty;
3436 loop {
3437 // Owned, as in `peel_func_chain`.
3438 match resolve(&cur).into_owned() {
3439 MonoType::Func(row, dom, cod) => {
3440 slots.push((*row, *dom));
3441 cur = *cod;
3442 }
3443 other => return (slots, other),
3444 }
3445 }
3446}
3447
3448/// Turn one V0_1 command parameter's [`Row`] (the row `Ast::LambdaOpt`'s
3449/// inference leaves on the `Func` arrow whose *domain* is that parameter —
3450/// see [`peel_func_chain_rows`]) into a closed-label-map [`CmdArgType`]: walk
3451/// the (resolved) row's `Cons` chain into a `Vec<(String, MonoType)>`, sorted
3452/// by label so `unify_cmd_args`'s equal-domain zip is order-insensitive.
3453/// A leftover `Row::Var` (an
3454/// under-constrained/free row — the ordinary case for a slot with no `?(…)`
3455/// bundle at all) defaults to no labels, same as `Row::Empty`. Shared by
3456/// `Checker::command_scheme`'s V0_1 branch and
3457/// `Checker::math_command_scheme_v01` so both harvest
3458/// identically.
3459fn harvest_slot(row: Row, dom: MonoType) -> CmdArgType {
3460 let mut opt_labels: Vec<(String, MonoType)> = Vec::new();
3461 let mut cur = resolve_row(&row).into_owned();
3462 loop {
3463 match cur {
3464 Row::Empty => break,
3465 Row::Var(_) => break,
3466 Row::Cons(label, lty, rest) => {
3467 opt_labels.push((label, *lty));
3468 cur = resolve_row(&rest).into_owned();
3469 }
3470 }
3471 }
3472 opt_labels.sort_by(|a, b| a.0.cmp(&b.0));
3473 labeled(opt_labels, dom)
3474}
3475
3476/// A best-effort span for an `Ast` node: only `Var`/`Overwrite`/
3477/// `AccessField` carry one directly (see `ast.rs`'s module doc comment);
3478/// everything else falls back to `None`; the resulting `TypeError` then just
3479/// prints without a location prefix.
3480pub(crate) fn ast_span<'s>(ast: &Ast<'s>) -> Option<Span> {
3481 match ast {
3482 Ast::Var(_, span) => Some(*span),
3483 Ast::Overwrite(_, span, _) => Some(*span),
3484 Ast::AccessField(_, _, span) => Some(*span),
3485 Ast::VersionScope(_, inner) => ast_span(inner),
3486 Ast::ModuleScope(_, inner) => ast_span(inner),
3487 _ => None,
3488 }
3489}
3490
3491/// Type-check a whole elaborated [`Program`], additionally returning every
3492/// non-fatal [`MatchWarning`] the exhaustiveness/redundancy pass collected
3493/// — v0.0.6's `exhchecker.ml` warns
3494/// on a non-exhaustive or redundant `match` rather than rejecting the
3495/// program, so these never turn a would-have-passed program into a
3496/// `TypeError`.
3497pub fn typecheck_verbose<'s>(program: &Program<'s>) -> Result<Vec<MatchWarning>, TypeError> {
3498 typecheck_verbose_with_version(program, RustyfiVersion::V0_0)
3499}
3500
3501/// Same as [`typecheck_verbose`], for a given target `version` — threads
3502/// through to `Checker::new_with_version`/`base_type_env_with_version` so a
3503/// `V0_1` program's `page-break` resolves against the `length * length`
3504/// tuple type (`prim_types::t_page_or_geometry`) and never sees `page`'s
3505/// `VariantDecl` (gated out of `builtin_variants_with_version(V0_1)`) — a
3506/// `V0_1` program that writes `A4Paper` gets the SAME "unbound constructor"
3507/// error upstream's own 0.1 compiler would give it, which is the faithful
3508/// behavior (the ADT is genuinely gone, not merely discouraged).
3509pub fn typecheck_verbose_with_version<'s>(
3510 program: &Program<'s>,
3511 version: RustyfiVersion,
3512) -> Result<Vec<MatchWarning>, TypeError> {
3513 let mut checker = Checker::new_with_version(program, version)?;
3514 let env = base_type_env_with_version(checker.store, version);
3515 checker.infer(&env, &program.body)?;
3516 Ok(checker.warnings)
3517}
3518
3519/// Type-check a whole elaborated [`Program`]. Validation only: on success the
3520/// caller proceeds to evaluate `program.body`; the evaluator is untouched by
3521/// this phase. A thin wrapper over [`typecheck_verbose`] that discards its
3522/// warnings.
3523pub fn typecheck<'s>(program: &Program<'s>) -> Result<(), TypeError> {
3524 typecheck_with_version(program, RustyfiVersion::V0_0)
3525}
3526
3527/// Same as [`typecheck`], for a given target `version`. See
3528/// `typecheck_verbose_with_version`'s doc comment.
3529pub fn typecheck_with_version<'s>(
3530 program: &Program<'s>,
3531 version: RustyfiVersion,
3532) -> Result<(), TypeError> {
3533 typecheck_verbose_with_version(program, version).map(|_warnings| ())
3534}
3535
3536// ============================================================================
3537// Per-binding ≡ whole-program equivalence, and session-incrementality.
3538// A `#[cfg(test)]` unit module since `BindingView`/`Checker`/`infer_binding`
3539// etc. are `pub(crate)` — an integration test can't reach them.
3540// ============================================================================
3541#[cfg(test)]
3542mod l3_per_binding_tests {
3543 use super::*;
3544 use crate::{elaborate, primitives};
3545
3546 fn elaborate_src<'s>(store: &'s SymbolStore, src: &str) -> Program<'s> {
3547 let file = rustyfi_syntax::parse_file(src).expect("parse failed");
3548 let env = primitives::base_env();
3549 let scope = elaborate::Scope::new(store, env.names());
3550 elaborate::elaborate_program(&file, &scope).expect("elaborate failed")
3551 }
3552
3553 /// Manually drive the checker per binding: walk `program.body`'s Let
3554 /// chain constructing `BindingView`s by hand, `infer_binding` +
3555 /// `with_all` at each step, `infer_expr` on the non-Let tail. This is
3556 /// exactly what `infer`'s own recursion does internally — driven here
3557 /// from outside the engine through the `pub(crate)` per-binding API, the
3558 /// same way `v1/module_check.rs` does.
3559 fn drive_manually<'s>(
3560 program: &Program<'s>,
3561 version: RustyfiVersion,
3562 ) -> Result<Vec<MatchWarning>, TypeError> {
3563 let mut checker = Checker::new_with_version(program, version)?;
3564 let mut env = base_type_env_with_version(program.store, version);
3565 let mut ast: &Ast<'s> = &program.body;
3566 loop {
3567 ast = match ast {
3568 Ast::LetIn(name, value, body) => {
3569 let schemes =
3570 checker.infer_binding(&env, BindingView::Let { name: *name, value })?;
3571 env = env.with_all(schemes, checker.binding_stage(value));
3572 body
3573 }
3574 Ast::LetMathIn(name, value, body) => {
3575 let schemes =
3576 checker.infer_binding(&env, BindingView::LetMath { name: *name, value })?;
3577 env = env.with_all(schemes, checker.binding_stage(value));
3578 body
3579 }
3580 Ast::LetRecIn(bindings, body) => {
3581 let schemes = checker.infer_binding(&env, BindingView::LetRec(bindings))?;
3582 env = env.with_all(schemes, checker.binding_stage_rec(bindings));
3583 body
3584 }
3585 Ast::LetMutableIn(name, init, body) => {
3586 let schemes = checker
3587 .infer_binding(&env, BindingView::LetMutable { name: *name, init })?;
3588 env = env.with_all(schemes, checker.binding_stage(init));
3589 body
3590 }
3591 other => {
3592 checker.infer_expr(&env, other)?;
3593 break;
3594 }
3595 };
3596 }
3597 Ok(checker.take_warnings())
3598 }
3599
3600 /// Elaborate `src` once, then compare `typecheck_verbose_with_version`
3601 /// against the manual per-binding drive: identical verdict, identical
3602 /// `TypeError` `Display` on error, identical `MatchWarning` list (incl.
3603 /// order — `MatchWarning` derives `PartialEq`) on success.
3604 fn assert_equivalent(src: &str) {
3605 let version = RustyfiVersion::V0_0;
3606 let store = SymbolStore::new();
3607 let program = elaborate_src(&store, src);
3608 let whole = typecheck_verbose_with_version(&program, version);
3609 let manual = drive_manually(&program, version);
3610 match (whole, manual) {
3611 (Ok(w1), Ok(w2)) => {
3612 assert_eq!(w1, w2, "warnings differ for {src:?}");
3613 }
3614 (Err(e1), Err(e2)) => {
3615 assert_eq!(
3616 format!("{e1}"),
3617 format!("{e2}"),
3618 "error strings differ for {src:?}"
3619 );
3620 }
3621 (Ok(w), Err(e)) => panic!(
3622 "{src:?}: whole-program accepted (warnings={w:?}), manual drive rejected: {e}"
3623 ),
3624 (Err(e), Ok(w)) => panic!(
3625 "{src:?}: whole-program rejected ({e}), manual drive accepted (warnings={w:?})"
3626 ),
3627 }
3628 }
3629
3630 #[test]
3631 fn per_binding_drive_matches_whole_program_across_binding_kinds() {
3632 let cases: &[&str] = &[
3633 // ---- plain `let` ----
3634 "let x = 1 in x + 1",
3635 "let x = 1 in x + true", // failing: type mismatch
3636 // ---- polymorphic `let` ----
3637 "let id = fun x -> x in (id 1, id true)",
3638 // ---- `let-inline` command binding (+ its application) ----
3639 "let-inline ctx \\emph it = read-inline ctx it
3640 in
3641 { \\emph{ ok } }",
3642 "let-inline ctx \\bad = ctx + 1
3643 in
3644 ()", // failing: not context-headed
3645 // ---- `let-block` command binding (+ its application) ----
3646 "let-block ctx +p it = line-break true true ctx (read-inline ctx it)
3647 in
3648 '< +p{ ok } >",
3649 "let-block ctx +duo a b = read-block ctx a
3650 in
3651 '< +duo{x} >", // failing: wrong arity
3652 // ---- `let-math` command binding ----
3653 "let-math \\g m = ${#m#m} in 0",
3654 "let-math \\f = 3 in 0", // failing: value isn't `math`
3655 // ---- `let-rec` group ----
3656 "let-rec is-even n = if n == 0 then true else is-odd (n - 1)
3657 and is-odd n = if n == 0 then false else is-even (n - 1)
3658 in
3659 is-even 4",
3660 "let-rec f n = if n == 0 then 0 else (f true)
3661 in
3662 f 1", // failing: recursive use at a mismatched type
3663 // ---- `let-mutable` (value restriction) ----
3664 "let-mutable x <- 0
3665 in
3666 (x <- 5)",
3667 "let-mutable r <- []
3668 in
3669 ((r <- (1 :: !r)) before (r <- (true :: !r)))", // failing: value restriction
3670 // ---- a `match` to also exercise warning accumulation ----
3671 "match Some 1 with
3672 | Some n -> n
3673 | None -> 0",
3674 ];
3675 for src in cases {
3676 assert_equivalent(src);
3677 }
3678 }
3679
3680 /// Session-incrementality. `declare_variant` after
3681 /// `infer_binding` affects only *later* checking against the session —
3682 /// a ctor referenced before its `declare_variant` fails with the same
3683 /// "unknown constructor" error the whole-program path gives for a
3684 /// genuinely-undeclared one; after `declare_variant`, it typechecks.
3685 #[test]
3686 fn session_incrementality_declare_variant_affects_only_later_bindings() {
3687 let store = SymbolStore::new();
3688 let program = elaborate_src(&store, "type t = | A of int in 0");
3689 assert_eq!(program.type_decls.len(), 1);
3690 let decl = &program.type_decls[0];
3691
3692 let mut checker = Checker::empty(&store);
3693 checker.install_builtin_variants(RustyfiVersion::V0_0);
3694 let env = base_type_env_with_version(&store, RustyfiVersion::V0_0);
3695
3696 // Before `declare_variant`, `A` is unknown — same message shape a
3697 // genuinely-undeclared constructor gets.
3698 let a_payload = Ast::Ctor("A".to_string(), Some(Box::new(Ast::Int(1))));
3699 let before = checker
3700 .infer_binding(
3701 &env,
3702 BindingView::Let {
3703 name: store.intern("before"),
3704 value: &a_payload,
3705 },
3706 )
3707 .expect_err("`A` should be unknown before declare_variant");
3708 assert_eq!(format!("{before}"), "unknown constructor 'A'");
3709
3710 let nosuch_payload = Ast::Ctor("NoSuchCtor".to_string(), None);
3711 let genuinely_unknown = checker
3712 .infer_binding(
3713 &env,
3714 BindingView::Let {
3715 name: store.intern("n"),
3716 value: &nosuch_payload,
3717 },
3718 )
3719 .expect_err("a genuinely undeclared ctor should also fail");
3720 assert_eq!(
3721 format!("{genuinely_unknown}"),
3722 "unknown constructor 'NoSuchCtor'"
3723 );
3724
3725 // After `declare_variant`, `A` becomes visible and typechecks —
3726 // this later binding sees it; the earlier `before` call above is
3727 // unaffected (it already returned its error).
3728 checker
3729 .declare_variant(decl)
3730 .expect("declare_variant should succeed");
3731 let after = checker.infer_binding(
3732 &env,
3733 BindingView::Let {
3734 name: store.intern("after"),
3735 value: &a_payload,
3736 },
3737 );
3738 assert!(
3739 after.is_ok(),
3740 "A(1) should typecheck after declare_variant: {after:?}"
3741 );
3742 }
3743}
3744
3745// ============================================================================
3746// The shadowing-fix follow-up to the version-scope env swap above:
3747// `version_scoped_type_env`'s `Ast::VersionScope` overwrite must not
3748// re-stomp a `PRIMITIVE_NAMES` entry the user already shadowed BEFORE the
3749// `VersionScope` is reached. A `#[cfg(test)]` unit module (mirroring
3750// `l3_per_binding_tests` above) since `Checker`/`TypeEnv`/`Ast` are all
3751// `pub(crate)` or crate-private-shaped enough that a hand-built synthetic
3752// `Ast` fixture (no parser/elaborator round-trip needed to pin this one
3753// shape) is the most direct way to exercise the exact env-swap path.
3754// ============================================================================
3755#[cfg(test)]
3756mod x2b_shadow_tests {
3757 use super::*;
3758
3759 /// `let page-break = 42 in <VersionScope V0_0> page-break` — infer
3760 /// the whole tree under a `V0_1`-ambient `Checker`. `page-break` is a
3761 /// `PRIMITIVE_NAMES` member (a version-forked one, no less: its `V0_0`
3762 /// scheme takes the `page` ADT, its `V0_1` scheme a `length * length`
3763 /// tuple — see `page_prims.rs`). An overwrite loop that replaced every
3764 /// `PRIMITIVE_NAMES` entry unconditionally on entering the
3765 /// `VersionScope` would re-stomp the user's `page-break = 42` with
3766 /// `V0_0`'s builtin `page-break` (a curried function type), inferring the
3767 /// inner `Var` as a function, not `int`. Instead
3768 /// `version_scoped_type_env` sees `page-break` recorded in
3769 /// `env.shadowed` (set by the `LetIn` arm's `env.with_all`, which goes
3770 /// through `TypeEnv::with`) and skips the overwrite for that one name, so
3771 /// the `Var` resolves through the untouched user binding and the whole
3772 /// expression types as `int`.
3773 #[test]
3774 fn version_scope_does_not_clobber_a_user_shadowed_primitive() {
3775 assert!(
3776 PRIMITIVE_NAMES.contains(&"page-break"),
3777 "fixture assumption: page-break must be a PRIMITIVE_NAMES member"
3778 );
3779
3780 let span = Span::default();
3781 let store = SymbolStore::new();
3782 let page_break = store.intern("page-break");
3783 let ast = Ast::LetIn(
3784 page_break,
3785 Box::new(Ast::Int(42)),
3786 Box::new(Ast::VersionScope(
3787 RustyfiVersion::V0_0,
3788 Box::new(Ast::Var(page_break, span)),
3789 )),
3790 );
3791 let program = Program {
3792 type_decls: Vec::new(),
3793 synonym_decls: Vec::new(),
3794 body: ast,
3795 store: &store,
3796 };
3797
3798 let mut checker = Checker::new_with_version(&program, RustyfiVersion::V0_1)
3799 .expect("checker construction over an empty-decls program should succeed");
3800 let env = base_type_env_with_version(&store, RustyfiVersion::V0_1);
3801 let ty = checker.infer(&env, &program.body).unwrap_or_else(|e| {
3802 panic!(
3803 "inferring the version-scoped `Var` over the user's shadowed \
3804 `page-break = 42` binding should type-check as `int`, not error: {e}"
3805 )
3806 });
3807 assert!(
3808 matches!(ty, MonoType::Base(BaseType::Int)),
3809 "the VersionScope env swap must respect the user's `page-break` shadow \
3810 (expected MonoType::Base(BaseType::Int), got {ty:?} instead) — a \
3811 MonoType::Func here would mean version_scoped_type_env re-stomped the \
3812 user binding with V0_0's builtin page-break scheme"
3813 );
3814 }
3815
3816 /// Companion positive control (same shape as the test above, MINUS the
3817 /// enclosing user shadow): a version-forked primitive referenced inside
3818 /// a `VersionScope` still resolves to `version`'s own (function-typed)
3819 /// scheme. Contrasts directly with
3820 /// `version_scope_does_not_clobber_a_user_shadowed_primitive`'s `int`
3821 /// result: same `page-break` name, same `VersionScope(V0_0, _)`, the only
3822 /// difference being the absence of a prior `let page-break = …` shadow.
3823 #[test]
3824 fn version_scope_still_resolves_unshadowed_forked_primitive() {
3825 let span = Span::default();
3826 let store = SymbolStore::new();
3827 let ast = Ast::VersionScope(
3828 RustyfiVersion::V0_0,
3829 Box::new(Ast::Var(store.intern("page-break"), span)),
3830 );
3831 let program = Program {
3832 type_decls: Vec::new(),
3833 synonym_decls: Vec::new(),
3834 body: ast,
3835 store: &store,
3836 };
3837 let mut checker = Checker::new_with_version(&program, RustyfiVersion::V0_1)
3838 .expect("checker construction over an empty-decls program should succeed");
3839 let env = base_type_env_with_version(&store, RustyfiVersion::V0_1);
3840 let ty = checker.infer(&env, &program.body).unwrap_or_else(|e| {
3841 panic!(
3842 "an unshadowed page-break reference inside a VersionScope should still \
3843 type-check (X2a's original capability, unaffected by X2b): {e}"
3844 )
3845 });
3846 assert!(
3847 matches!(ty, MonoType::Func(..)),
3848 "page-break (unshadowed) inside a VersionScope should still resolve to its \
3849 builtin (function-typed) scheme, got {ty:?} instead — the X2b shadow guard \
3850 must not have blocked this NON-shadowed overwrite"
3851 );
3852 }
3853}
3854
3855// ============================================================================
3856// Acceptance test against the real `stdja.satyh` `sig … end` block (
3857// command values are covered by `crates/rustyfi-lang/tests/typecheck.rs`'s
3858// end-to-end fixtures; this module covers the `SigItem`/`constraint` lowering
3859// directly, since `lower_sig_item` is a crate-private entry point no
3860// sig-enforcement pass calls yet).
3861// ============================================================================
3862#[cfg(test)]
3863mod sig_constraint_tests {
3864 use super::*;
3865 use rustyfi_syntax::cst::{SigAnnot, TopBinding};
3866
3867 fn parse_module_sig(src: &str) -> SigAnnot {
3868 let file = rustyfi_syntax::parse_file(src).expect("parse failed");
3869 for b in &file.prelude {
3870 if let TopBinding::Module { sig: Some(sig), .. } = b {
3871 return sig.clone();
3872 }
3873 }
3874 panic!("no `module .. : sig .. end` found in {src:?}");
3875 }
3876
3877 #[test]
3878 fn constraint_suffix_lowers_to_a_kind_record_bound_on_its_tyvar() {
3879 let sig = parse_module_sig(
3880 "module M : sig\n\
3881 val document : 'a -> config ?-> block-text -> document\n\
3882 constraint 'a :: (| title : inline-text; author : inline-text |)\n\
3883 end = struct\n\
3884 let document x c bt = bt\n\
3885 end",
3886 );
3887 let mut ctx = TypeContext::new();
3888 let mut saw_record_kind = false;
3889 for item in &sig.items {
3890 let (name, ty) =
3891 lower_sig_item(item, &mut ctx, RustyfiVersion::V0_0).expect("a value item");
3892 assert_eq!(name, "document");
3893 // Walk the lowered `Func` chain: `'a`'s fresh variable is the
3894 // very first domain (`Func(Var('a), Func(option(config),
3895 // Func(block-text, document)))` — see `lower_type_expr`'s doc
3896 // comment for the `?->` shape).
3897 if let MonoType::Func(_row, dom, _) = &ty {
3898 if let MonoType::Var(v) = &**dom {
3899 if let Kind::Record(labels) = v.kind() {
3900 saw_record_kind = true;
3901 let expected: BTreeSet<String> =
3902 ["title", "author"].iter().map(|s| s.to_string()).collect();
3903 assert_eq!(labels, expected);
3904 }
3905 }
3906 }
3907 }
3908 assert!(
3909 saw_record_kind,
3910 "expected 'a's fresh variable to carry a Kind::Record bound"
3911 );
3912 }
3913
3914 #[test]
3915 fn kind_record_bound_accepts_a_row_with_every_required_label() {
3916 // Direct demonstration that the constraint's lowered `Kind::Record`
3917 // bound rides on *existing* `unify`/`bind_var` machinery for free —
3918 // no sig-enforcement pass exists yet to drive this against a real
3919 // `struct` implementation, but
3920 // the positive-presence check itself already works once something
3921 // does.
3922 let mut ctx = TypeContext::new();
3923 let labels: BTreeSet<String> = ["title", "author"].iter().map(|s| s.to_string()).collect();
3924 let v = ctx.fresh_var_with_kind(Kind::Record(labels));
3925 let constrained = MonoType::Var(v);
3926 let full = MonoType::Record(Row::Cons(
3927 "title".to_string(),
3928 Box::new(t_inline_text()),
3929 Box::new(Row::Cons(
3930 "author".to_string(),
3931 Box::new(t_inline_text()),
3932 Box::new(Row::Empty),
3933 )),
3934 ));
3935 unify(&constrained, &full).expect("row has both required labels");
3936 }
3937
3938 #[test]
3939 fn kind_record_bound_rejects_a_row_missing_a_required_label() {
3940 let mut ctx = TypeContext::new();
3941 let labels: BTreeSet<String> = ["title", "author"].iter().map(|s| s.to_string()).collect();
3942 let v = ctx.fresh_var_with_kind(Kind::Record(labels));
3943 let constrained = MonoType::Var(v);
3944 let missing_author = MonoType::Record(Row::Cons(
3945 "title".to_string(),
3946 Box::new(t_inline_text()),
3947 Box::new(Row::Empty),
3948 ));
3949 let err = unify(&constrained, &missing_author)
3950 .expect_err("row is missing the required 'author' label");
3951 assert!(
3952 format!("{err:?}").contains("author"),
3953 "error should name the missing label: {err:?}"
3954 );
3955 }
3956
3957 #[test]
3958 fn real_stdja_sig_block_lowers_every_item_to_a_monotype() {
3959 // Mirrors the whole `sig … end` block of the real upstream
3960 // `stdja.satyh:24-51` (v0.0.6 checkout) — command values, command
3961 // types, `?->`, and the `constraint` suffix all together. Every item
3962 // parses and lowers without error (an empty `struct end` body is
3963 // enough — sig enforcement against a real implementation is not
3964 // this test's job).
3965 let sig = parse_module_sig(
3966 "module StdJa : sig\n\
3967 val default-config : config\n\
3968 val document : 'a -> config ?-> block-text -> document\n\
3969 constraint 'a :: (|\n\
3970 title : inline-text;\n\
3971 author : inline-text;\n\
3972 show-toc : bool;\n\
3973 show-title : bool;\n\
3974 |)\n\
3975 val font-latin-roman : string * float * float\n\
3976 direct \\ref : [string] inline-cmd\n\
3977 direct \\ref-page : [string] inline-cmd\n\
3978 direct \\figure : [inline-text; block-text] inline-cmd\n\
3979 direct +p : [inline-text] block-cmd\n\
3980 direct +pn : [inline-text] block-cmd\n\
3981 direct +section : [string?; string?; inline-text; block-text] block-cmd\n\
3982 direct +subsection : [string?; string?; inline-text; block-text] block-cmd\n\
3983 direct \\emph : [inline-text] inline-cmd\n\
3984 end = struct\n\
3985 end",
3986 );
3987 let mut ctx = TypeContext::new();
3988 let mut names = Vec::new();
3989 for item in &sig.items {
3990 let (name, _ty) =
3991 lower_sig_item(item, &mut ctx, RustyfiVersion::V0_0).expect("a value item");
3992 names.push(name);
3993 }
3994 assert_eq!(
3995 names,
3996 vec![
3997 "default-config",
3998 "document",
3999 "font-latin-roman",
4000 "\\ref",
4001 "\\ref-page",
4002 "\\figure",
4003 "+p",
4004 "+pn",
4005 "+section",
4006 "+subsection",
4007 "\\emph",
4008 ]
4009 );
4010 }
4011}