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).
1528fn synonym_refs(ty: &MonoType, synonyms: &HashMap<String, SynonymDecl>, out: &mut Vec<String>) {
1529 match ty {
1530 MonoType::Var(_) | MonoType::Base(_) => {}
1531 MonoType::Func(row, dom, cod) => {
1532 synonym_refs_row(row, synonyms, out);
1533 synonym_refs(dom, synonyms, out);
1534 synonym_refs(cod, synonyms, out);
1535 }
1536 MonoType::Product(ts) => ts.iter().for_each(|t| synonym_refs(t, synonyms, out)),
1537 MonoType::List(t) | MonoType::Ref(t) | MonoType::Code(t) => synonym_refs(t, synonyms, out),
1538 MonoType::Record(row) => synonym_refs_row(row, synonyms, out),
1539 MonoType::Variant(name, args) => {
1540 if synonyms.contains_key(name) {
1541 out.push(name.clone());
1542 }
1543 args.iter().for_each(|t| synonym_refs(t, synonyms, out));
1544 }
1545 MonoType::InlineCmd(cs) | MonoType::BlockCmd(cs) | MonoType::MathCmd(cs) => {
1546 cs.iter().for_each(|c| synonym_refs(&c.ty, synonyms, out));
1547 }
1548 }
1549}
1550
1551fn synonym_refs_row(row: &Row, synonyms: &HashMap<String, SynonymDecl>, out: &mut Vec<String>) {
1552 match row {
1553 Row::Empty | Row::Var(_) => {}
1554 Row::Cons(_, t, rest) => {
1555 synonym_refs(t, synonyms, out);
1556 synonym_refs_row(rest, synonyms, out);
1557 }
1558 }
1559}
1560
1561/// Reject a cyclic synonym (`type a = b` / `type b = a`, or a directly
1562/// self-referential `type a = a * a`) with a clear error instead of letting
1563/// [`expand_synonyms`] recurse forever. Run unconditionally over every
1564/// registered synonym at [`Checker::new`] time, so a cyclic pair is caught
1565/// even if nothing in the program actually references it.
1566fn check_synonym_cycles(synonyms: &HashMap<String, SynonymDecl>) -> Result<(), TypeError> {
1567 for start in synonyms.keys() {
1568 let mut stack = vec![start.clone()];
1569 check_synonym_cycles_from(start, synonyms, &mut stack)?;
1570 }
1571 Ok(())
1572}
1573
1574fn check_synonym_cycles_from(
1575 name: &str,
1576 synonyms: &HashMap<String, SynonymDecl>,
1577 stack: &mut Vec<String>,
1578) -> Result<(), TypeError> {
1579 let mut refs = Vec::new();
1580 synonym_refs(&synonyms[name].body, synonyms, &mut refs);
1581 for r in refs {
1582 if stack.contains(&r) {
1583 let mut cycle = stack.clone();
1584 cycle.push(r);
1585 return Err(TypeError::simple(
1586 None,
1587 format!("cyclic type synonym: {}", cycle.join(" -> ")),
1588 ));
1589 }
1590 stack.push(r.clone());
1591 check_synonym_cycles_from(&r, synonyms, stack)?;
1592 stack.pop();
1593 }
1594 Ok(())
1595}
1596
1597/// Recursively and transparently expand every synonym reference inside
1598/// `ty`, so `unify` never sees a synonym's name — only real base/product/
1599/// function/variant types. `synonyms` was already validated acyclic by
1600/// [`check_synonym_cycles`] (at `Checker::new` time), so the recursion here
1601/// is guaranteed to terminate; arity (a reference's argument count against
1602/// the synonym's own parameter count) is checked here, the one place that
1603/// actually has concrete arguments to check it against.
1604fn expand_synonyms(
1605 ty: &MonoType,
1606 synonyms: &HashMap<String, SynonymDecl>,
1607) -> Result<MonoType, TypeError> {
1608 match ty {
1609 MonoType::Var(_) | MonoType::Base(_) => Ok(ty.clone()),
1610 MonoType::Func(row, dom, cod) => Ok(MonoType::Func(
1611 Box::new(expand_synonyms_row(row, synonyms)?),
1612 Box::new(expand_synonyms(dom, synonyms)?),
1613 Box::new(expand_synonyms(cod, synonyms)?),
1614 )),
1615 MonoType::Product(ts) => Ok(MonoType::Product(
1616 ts.iter()
1617 .map(|t| expand_synonyms(t, synonyms))
1618 .collect::<Result<_, _>>()?,
1619 )),
1620 MonoType::List(t) => Ok(MonoType::List(Box::new(expand_synonyms(t, synonyms)?))),
1621 MonoType::Ref(t) => Ok(MonoType::Ref(Box::new(expand_synonyms(t, synonyms)?))),
1622 MonoType::Code(t) => Ok(MonoType::Code(Box::new(expand_synonyms(t, synonyms)?))),
1623 MonoType::Record(row) => Ok(MonoType::Record(expand_synonyms_row(row, synonyms)?)),
1624 MonoType::Variant(name, args) => {
1625 let args: Vec<MonoType> = args
1626 .iter()
1627 .map(|t| expand_synonyms(t, synonyms))
1628 .collect::<Result<_, _>>()?;
1629 let Some(syn) = synonyms.get(name) else {
1630 return Ok(MonoType::Variant(name.clone(), args));
1631 };
1632 if args.len() != syn.param_vars.len() {
1633 return Err(TypeError::simple(
1634 None,
1635 format!(
1636 "type synonym '{name}' expects {} argument{}, got {}",
1637 syn.param_vars.len(),
1638 if syn.param_vars.len() == 1 { "" } else { "s" },
1639 args.len()
1640 ),
1641 ));
1642 }
1643 let mut var_map: HashMap<usize, MonoType> = HashMap::new();
1644 for (pv, arg) in syn.param_vars.iter().zip(args.iter()) {
1645 var_map.insert(types::ptr_key(pv), arg.clone());
1646 }
1647 let substituted = types::substitute(&syn.body, &var_map, &HashMap::new());
1648 expand_synonyms(&substituted, synonyms)
1649 }
1650 MonoType::InlineCmd(cs) => Ok(MonoType::InlineCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1651 MonoType::BlockCmd(cs) => Ok(MonoType::BlockCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1652 MonoType::MathCmd(cs) => Ok(MonoType::MathCmd(expand_synonyms_cmd_args(cs, synonyms)?)),
1653 }
1654}
1655
1656fn expand_synonyms_row(
1657 row: &Row,
1658 synonyms: &HashMap<String, SynonymDecl>,
1659) -> Result<Row, TypeError> {
1660 match row {
1661 Row::Empty => Ok(Row::Empty),
1662 Row::Var(v) => Ok(Row::Var(v.clone())),
1663 Row::Cons(label, t, rest) => Ok(Row::Cons(
1664 label.clone(),
1665 Box::new(expand_synonyms(t, synonyms)?),
1666 Box::new(expand_synonyms_row(rest, synonyms)?),
1667 )),
1668 }
1669}
1670
1671fn expand_synonyms_cmd_args(
1672 cs: &[CmdArgType],
1673 synonyms: &HashMap<String, SynonymDecl>,
1674) -> Result<Vec<CmdArgType>, TypeError> {
1675 cs.iter()
1676 .map(|c| {
1677 Ok(CmdArgType {
1678 optional: c.optional,
1679 opt_labels: c
1680 .opt_labels
1681 .iter()
1682 .map(|(l, t)| Ok((l.clone(), expand_synonyms(t, synonyms)?)))
1683 .collect::<Result<_, TypeError>>()?,
1684 ty: expand_synonyms(&c.ty, synonyms)?,
1685 })
1686 })
1687 .collect()
1688}
1689
1690// ============================================================================
1691// The checker.
1692// ============================================================================
1693
1694pub(crate) struct Checker<'s> {
1695 /// The interner every identifier in the tree being checked came from.
1696 /// Needed both to mint derived lookup keys (`"{modpfx}.{ctor}"`) and to
1697 /// resolve a `Symbol` back to text for an error message — see
1698 /// [`Checker::text`].
1699 store: &'s SymbolStore,
1700 ctx: TypeContext,
1701 /// Constructor name -> the (`Rc`-shared) declaration it belongs to.
1702 /// Later declarations shadow earlier ones of the same ctor name, mirroring
1703 /// ordinary name shadowing elsewhere in this port.
1704 ctors: HashMap<String, Rc<VariantDecl>>,
1705 /// The same declarations, keyed by *type* name instead — needed by the
1706 /// exhaustiveness pass (`exhaustive::check_match`) to enumerate a
1707 /// variant's full constructor set given the scrutinee's resolved
1708 /// `MonoType::Variant(name, _)`.
1709 variants: HashMap<String, Rc<VariantDecl>>,
1710 /// The synonym table. A field rather than a local of `new_with_version`
1711 /// so `declare_variant` can expand ctor payloads through it after
1712 /// construction time (per-binding registration).
1713 synonyms: HashMap<String, SynonymDecl>,
1714 /// Non-fatal diagnostics accumulated by the exhaustiveness/redundancy
1715 /// pass (see `typecheck_verbose`); v0.0.6's `exhchecker.ml` warns and
1716 /// continues rather than rejecting the program.
1717 warnings: Vec<MatchWarning>,
1718 /// The target version this session is checking against — set by
1719 /// [`Checker::install_builtin_variants`] (every real construction path's
1720 /// call; `Checker::empty` alone never does, defaulting to `V0_0`).
1721 /// Threaded into `name_to_mono`'s surface-type-name fork and
1722 /// `Ast::LetMathIn`'s scheme rule (`math_command_scheme` vs
1723 /// `math_command_scheme_v01`).
1724 version: RustyfiVersion,
1725 /// The generation of the code currently being inferred, when that is
1726 /// NOT [`Checker::version`] — set (save/restore) by the
1727 /// `Ast::VersionScope` infer arm, `None` outside every such scope.
1728 /// Deliberately a SEPARATE field rather than a temporary overwrite of
1729 /// `version`, since that one must stay pinned to `V0_1` for a merged
1730 /// whole-program session (`v1::module_check::check_program_inner`). See
1731 /// [`Checker::binding_version`], its only consumer.
1732 scoped_version: Option<RustyfiVersion>,
1733 /// The module path of the member body currently being inferred (pushed by
1734 /// the `Ast::ModuleScope` arm; empty at top level). A BARE constructor
1735 /// reference is looked up under `<path>.Ctor` (innermost prefix first)
1736 /// before the bare fallback — so two modules' same-named constructors
1737 /// (`Term.Paren` vs `Type.Paren`) no longer collide. See [`Ast::ModuleScope`].
1738 ctor_scope: Vec<String>,
1739 /// The stage this expression is being read at. Starts at whatever the
1740 /// file declared (`@stage:`, default [`Stage::Stage1`]) and is shifted by
1741 /// `&`/`~`: a quote reads its body one stage LATER, a splice one stage
1742 /// EARLIER. Only `Next`/`Prev` consult it.
1743 stage: Stage,
1744}
1745
1746/// One elaborated top-level-shaped binding, viewed by reference — the
1747/// typecheck-side mirror of `elaborate::Binding` and of the four Let-shaped
1748/// `Ast` spine variants (`ast.rs`). The module checker constructs these
1749/// directly from its own per-`val` walk; the whole-program path never
1750/// constructs one explicitly (its `infer` arms pass the same references
1751/// through).
1752pub(crate) enum BindingView<'a, 's> {
1753 /// `Ast::LetIn` — plain value OR `\`/`+`-sigiled command binding; the
1754 /// sigil dispatch (`command_scheme`) stays inside the checker.
1755 Let {
1756 name: Symbol<'s>,
1757 value: &'a Ast<'s>,
1758 },
1759 /// `Ast::LetMathIn` — a math-command binding (distinct variant by
1760 /// construction).
1761 LetMath {
1762 name: Symbol<'s>,
1763 value: &'a Ast<'s>,
1764 },
1765 /// `Ast::LetRecIn`'s binding group (all names in scope in all bodies).
1766 LetRec(&'a [(Symbol<'s>, Rc<Ast<'s>>)]),
1767 /// `Ast::LetMutableIn` — value restriction, never generalized.
1768 LetMutable { name: Symbol<'s>, init: &'a Ast<'s> },
1769}
1770
1771impl<'s> Checker<'s> {
1772 // See `base_type_env`'s `#[allow(dead_code)]` note above — same shape,
1773 // same reason.
1774 #[allow(dead_code)]
1775 fn new(program: &Program<'s>) -> Result<Checker<'s>, TypeError> {
1776 Self::new_with_version(program, RustyfiVersion::V0_0)
1777 }
1778
1779 /// Bare session: empty tables, fresh `TypeContext`. Registers NOTHING —
1780 /// not even builtins — so `new_with_version` can compose the exact
1781 /// statement order of the original monolithic constructor.
1782 pub(crate) fn empty(store: &'s SymbolStore) -> Checker<'s> {
1783 Checker {
1784 store,
1785 ctx: TypeContext::new(),
1786 ctors: HashMap::new(),
1787 variants: HashMap::new(),
1788 synonyms: HashMap::new(),
1789 warnings: Vec::new(),
1790 version: RustyfiVersion::V0_0,
1791 scoped_version: None,
1792 ctor_scope: Vec::new(),
1793 // A document is stage 1; a library overrides this from its
1794 // `@stage:` header before checking begins.
1795 stage: Stage::default(),
1796 }
1797 }
1798
1799 /// Set the session's target version with NO other side effect — a pure
1800 /// field write, safe to call before anything else.
1801 /// `new_with_version`/`v1::module_check::check_program`'s session-setup
1802 /// sequences both call this FIRST, ahead of their order-critical
1803 /// `declare_synonym` loop, so a V0_1 synonym body that names
1804 /// `math-text`/`math-boxes` resolves correctly even though
1805 /// `install_builtin_variants` (which also sets this field) does not run
1806 /// until afterward. A no-op for every 0.0.6 path: `empty()` already
1807 /// defaults to `V0_0`.
1808 pub(crate) fn set_version(&mut self, version: RustyfiVersion) {
1809 self.version = version;
1810 }
1811
1812 /// A symbol's source text. Every diagnostic this module formats goes
1813 /// through here: `Symbol`'s own `Debug` is index-only by design, and the
1814 /// golden tests diff the resolved strings.
1815 fn text(&self, sym: Symbol<'s>) -> &'s str {
1816 self.store.resolve(sym)
1817 }
1818
1819 /// Register the builtin variant decls for `version`, and record
1820 /// `version` on `self` — redundant with `set_version` for a path that
1821 /// calls both, kept so this method alone suffices for a caller that
1822 /// skips `set_version` (e.g. the bare-builtins test construction).
1823 pub(crate) fn install_builtin_variants(&mut self, version: RustyfiVersion) {
1824 self.version = version;
1825 self.install_additional_builtin_variants(version);
1826 }
1827
1828 /// Register `version`'s builtin variant/ctor set WITHOUT
1829 /// touching `self.version` (unlike [`Checker::install_builtin_
1830 /// variants`], which also sets the whole-session version tag) — called
1831 /// from the `Ast::VersionScope` `infer` arm below, lazily, the first
1832 /// time a version-scoped subtree is reached, so a `V0_0`-only ADT like
1833 /// `page` (`A0Paper`/…/`A4Paper`/`UserDefinedPaper`, gated on
1834 /// `has_page_adt()`, `prim_types.rs`'s `builtin_variants_with_version`)
1835 /// is constructible/matchable inside a spliced dependency's internal
1836 /// `page-break A4Paper …` call even though the whole-program `Checker`
1837 /// was built under `V0_1` (where `page` isn't registered at all).
1838 /// `self.ctors`/`self.variants` are flat, last-writer-wins, program-
1839 /// global tables already (`hide_ctors`'s doc comment); every
1840 /// `builtin_variants_with_version` entry OTHER than `page` is identical
1841 /// between the two versions (only `page` is gated by `has_page_adt()`,
1842 /// `prim_types.rs:2119`), so this is a safe additive merge, not a
1843 /// replace — idempotent across repeated `VersionScope` nodes of the same
1844 /// version.
1845 pub(crate) fn install_additional_builtin_variants(&mut self, version: RustyfiVersion) {
1846 for decl in builtin_variants_with_version(version) {
1847 let decl = Rc::new(decl);
1848 self.variants.insert(decl.name.clone(), decl.clone());
1849 for (cname, _) in &decl.ctors {
1850 self.ctors.insert(cname.clone(), decl.clone());
1851 }
1852 }
1853 }
1854
1855 /// One synonym registration. Does NOT cycle-check — register all, then
1856 /// call `check_cycles`. Fallible:
1857 /// `check_type_expr_v0_1_only` rejects a `?(l:ty)->` domain in the
1858 /// synonym's body under `V0_0` before it is ever lowered.
1859 pub(crate) fn declare_synonym(&mut self, decl: &UserSynonymDecl) -> Result<(), TypeError> {
1860 check_type_expr_v0_1_only(&decl.body, self.version)?;
1861 self.synonyms
1862 .insert(decl.name.clone(), build_synonym_decl(decl, self.version));
1863 Ok(())
1864 }
1865
1866 /// Cycle-check the accumulated synonym table — a thin wrapper over the
1867 /// existing free fn `check_synonym_cycles`.
1868 pub(crate) fn check_cycles(&self) -> Result<(), TypeError> {
1869 check_synonym_cycles(&self.synonyms)
1870 }
1871
1872 /// One variant-decl registration.
1873 pub(crate) fn declare_variant(&mut self, decl: &UserTypeDecl) -> Result<(), TypeError> {
1874 let decl = Rc::new(build_variant_decl(decl, &self.synonyms, self.version)?);
1875 self.variants.insert(decl.name.clone(), decl.clone());
1876 for (cname, _) in &decl.ctors {
1877 self.ctors.insert(cname.clone(), decl.clone());
1878 }
1879 // If the variant's own type name is module-qualified (`M.t`), also
1880 // register each constructor under a qualified key (`M.Ctor`) so a
1881 // within-module bare reference (via `Checker::lookup_ctor`, driven by
1882 // `Ast::ModuleScope`) resolves to THIS module's ctor even when another
1883 // module declares the same bare ctor name. Builtins have undotted type
1884 // names, so this adds nothing for them.
1885 if let Some((modpfx, _)) = decl.name.rsplit_once('.') {
1886 for (cname, _) in &decl.ctors {
1887 self.ctors.insert(format!("{modpfx}.{cname}"), decl.clone());
1888 }
1889 }
1890 Ok(())
1891 }
1892
1893 /// The original whole-program constructor, re-expressed as the exact
1894 /// same statement sequence through the methods above: synonyms →
1895 /// cycle-check → builtins → user variant decls. This order-preservation
1896 /// is load-bearing for session-incrementality.
1897 fn new_with_version(
1898 program: &Program<'s>,
1899 version: RustyfiVersion,
1900 ) -> Result<Checker<'s>, TypeError> {
1901 // Synonyms are registered (and checked for cycles) before any
1902 // variant decl is lowered, since a variant's ctor payload may name a
1903 // synonym (`build_variant_decl` expands through `synonyms`).
1904 let mut c = Checker::empty(program.store);
1905 c.set_version(version);
1906 for usd in &program.synonym_decls {
1907 c.declare_synonym(usd)?;
1908 }
1909 c.check_cycles()?;
1910 c.install_builtin_variants(version);
1911 for utd in &program.type_decls {
1912 c.declare_variant(utd)?;
1913 }
1914 Ok(c)
1915 }
1916
1917 fn fresh(&mut self) -> MonoType {
1918 MonoType::Var(self.ctx.fresh_var())
1919 }
1920
1921 fn unify_ctx(
1922 &mut self,
1923 expected: &MonoType,
1924 found: &MonoType,
1925 span: Option<Span>,
1926 what: &str,
1927 ) -> Result<(), TypeError> {
1928 unify(expected, found).map_err(|e| TypeError::from_unify(span, what, e))
1929 }
1930
1931 /// Turn a `\`/`+`-named `LetIn` binding's ordinarily-inferred value type
1932 /// `tv` into the genuine command type (`MonoType::InlineCmd`/`BlockCmd`)
1933 /// it gets bound under: a user-defined command is typed as
1934 /// `[τ1; ..; τn] inline-cmd` (resp. `block-cmd`), matching v0.0.6's real
1935 /// `HorzCommandType`/`VertCommandType` (`typechecker.ml`'s
1936 /// `UTLetHorzIn`/`UTLetVertIn` rules), not a plain "context-curried"
1937 /// function.
1938 ///
1939 /// Two shapes reach this function, per [`command_sigil`]'s call site:
1940 ///
1941 /// * a genuine `let-inline`/`let-block` definition, whose value is the
1942 /// `Lambda(ctxvar, Lambda(p1, .., Lambda(pn, body)))` chain
1943 /// `elaborate::elaborate_let_inline` builds — [`peel_func_chain`]
1944 /// recovers that `Func` chain; the leading domain must unify with
1945 /// `context`, the final codomain with `inline-boxes`/`block-boxes`,
1946 /// and the domains between become the command's `CmdArgType` list.
1947 /// * a qualified-name *alias* of an already-command-typed binding (a
1948 /// module's own `M.\cmd` re-export, or an `open` re-binding — both
1949 /// build `LetIn(name, Ast::Var(qualified), body)`): the aliased name
1950 /// was already run through this function at its own definition site,
1951 /// so `tv` here already *is* the command type — this branch passes it
1952 /// through unchanged (re-generalized) instead of peeling a `Func`
1953 /// chain out of something that isn't one.
1954 fn command_scheme(
1955 &mut self,
1956 name: &str,
1957 sigil: char,
1958 tv: MonoType,
1959 span: Option<Span>,
1960 ) -> Result<PolyType, TypeError> {
1961 debug_assert!(sigil == '\\' || sigil == '+');
1962 let is_inline = sigil == '\\';
1963 let (want_result, kind, other_kind) = if is_inline {
1964 (t_inline_boxes(), "inline", "block")
1965 } else {
1966 (t_block_boxes(), "block", "inline")
1967 };
1968
1969 match &*resolve(&tv) {
1970 MonoType::InlineCmd(_) if is_inline => {
1971 return Ok(generalize(self.ctx.level(), &tv));
1972 }
1973 MonoType::BlockCmd(_) if !is_inline => {
1974 return Ok(generalize(self.ctx.level(), &tv));
1975 }
1976 MonoType::InlineCmd(_) | MonoType::BlockCmd(_) => {
1977 return Err(TypeError::simple(
1978 span,
1979 format!(
1980 "'{name}' is bound to a {other_kind} command, but its \
1981 name marks it as {article} {kind} command",
1982 article = if kind == "inline" { "an" } else { "a" },
1983 ),
1984 ));
1985 }
1986 // A qualified-name alias (`M.\cmd` re-export, or an `open`) of a
1987 // GENUINE `let-math` binding: math commands share the `\` sigil
1988 // with inline commands (there is no separate math-command token),
1989 // so an alias site only ever reaches this generic `Ast::LetIn`
1990 // path (never `Ast::LetMathIn`, which is produced only at a math
1991 // command's OWN definition site — top-level `let-math` via
1992 // `walk_bindings`, or the expression-level `let-math .. in ..`
1993 // form, `elaborate.rs`'s `Expr::LetMathIn` arm — never at an
1994 // alias site; see that variant's doc comment). Pass a already-
1995 // `MathCmd`-typed alias through unchanged, exactly like the
1996 // `InlineCmd`/`BlockCmd` arms above do for their own kind.
1997 MonoType::MathCmd(_) if is_inline => {
1998 return Ok(generalize(self.ctx.level(), &tv));
1999 }
2000 _ => {}
2001 }
2002
2003 // V0_1 harvests each param's closed
2004 // `?(l:τ,…)` label map from the `Row` that `Ast::LambdaOpt` leaves on
2005 // that param's own arrow (`peel_func_chain_rows`), instead of the
2006 // 0.0.6 "`_ option` domain ⇒ optional slot" heuristic below (which
2007 // stays untouched, byte-identical, under V0_0 — it never sees a
2008 // non-`Row::Empty` row at all, since V0_0 code never builds
2009 // `Ast::LambdaOpt`).
2010 let params: Vec<CmdArgType> = if self.version.has_row_polymorphism() {
2011 let (mut slots, result) = peel_func_chain_rows(tv);
2012 if slots.is_empty() {
2013 return Err(TypeError::simple(
2014 span,
2015 format!(
2016 "the binding for '{name}' must be a function taking a \
2017 context as its first argument (e.g. via `val inline ctx \
2018 {name} .. = ..`)"
2019 ),
2020 ));
2021 }
2022 let (ctx_row, ctx_ty) = slots.remove(0);
2023 // A labeled bundle can never legally land on the ctx binder
2024 // (`elaborate_let_inline` always wraps it in a plain
2025 // `Ast::Lambda`, which infers `Row::Empty` — `prim_types::arrow`)
2026 // — guard it defensively rather than silently dropping/mis-
2027 // attributing a label.
2028 if !matches!(&*resolve_row(&ctx_row), Row::Empty) {
2029 return Err(TypeError::simple(
2030 span,
2031 format!(
2032 "the context argument of '{name}' cannot carry a labeled \
2033 optional bundle"
2034 ),
2035 ));
2036 }
2037 self.unify_ctx(
2038 &t_context(),
2039 &ctx_ty,
2040 span,
2041 &format!("the context argument of '{name}'"),
2042 )?;
2043 self.unify_ctx(
2044 &want_result,
2045 &result,
2046 span,
2047 &format!("the result of '{name}'"),
2048 )?;
2049 slots
2050 .into_iter()
2051 .map(|(row, dom)| harvest_slot(row, dom))
2052 .collect()
2053 } else {
2054 let (mut doms, result) = peel_func_chain(tv);
2055 if doms.is_empty() {
2056 return Err(TypeError::simple(
2057 span,
2058 format!(
2059 "the binding for '{name}' must be a function taking a \
2060 context as its first argument (e.g. via `let-inline ctx \
2061 {name} .. = ..`)"
2062 ),
2063 ));
2064 }
2065 let ctx_ty = doms.remove(0);
2066 self.unify_ctx(
2067 &t_context(),
2068 &ctx_ty,
2069 span,
2070 &format!("the context argument of '{name}'"),
2071 )?;
2072 self.unify_ctx(
2073 &want_result,
2074 &result,
2075 span,
2076 &format!("the result of '{name}'"),
2077 )?;
2078 // Optional command params, simplified (Sub-area 2): this grammar
2079 // has no def-site `?:param` marker, so a param counts as optional
2080 // exactly when its INFERRED domain resolves to `_ option` — i.e.
2081 // the body actually uses it as an `option` (`match p with Some ..
2082 // | None -> ..`). `CmdArgType.ty` then stores the option's INNER
2083 // type, matching the `[ty?; ..]` signature-lowering shape 1:1
2084 // (`lower_type_atom`'s `TypeAtom::Cmd` arm); `check_cmd_args`
2085 // re-wraps it in `option(..)` per call, since call-site args
2086 // always arrive pre-wrapped as `Some`/`None`
2087 // (`elaborate.rs`'s `app_arg_to_ast`).
2088 doms.into_iter()
2089 .map(|d| match resolve(&d).into_owned() {
2090 MonoType::Variant(vname, mut vargs)
2091 if vname == "option" && vargs.len() == 1 =>
2092 {
2093 optional(vargs.pop().unwrap())
2094 }
2095 _ => mandatory(d),
2096 })
2097 .collect()
2098 };
2099 let cmd_ty = if is_inline {
2100 MonoType::InlineCmd(params)
2101 } else {
2102 MonoType::BlockCmd(params)
2103 };
2104 Ok(generalize(self.ctx.level(), &cmd_ty))
2105 }
2106
2107 /// `Ast::LetMathIn`'s scheme-building rule — the math-command analog of
2108 /// `command_scheme` above, but simpler: a math command has **no**
2109 /// implicit context argument (see `elaborate.rs`'s
2110 /// `elaborate_let_math`), so every domain of `tv`'s function-chain
2111 /// becomes a `CmdArgType` (the same optional-param heuristic as
2112 /// `command_scheme`), and the bare result — not a peeled first argument
2113 /// — must be `math`. A zero-arity binding (`tv` not a `Func` at all,
2114 /// e.g. `let-math \to = rel \`→\``) falls out naturally:
2115 /// `peel_func_chain` returns no domains and `tv` itself as the result.
2116 fn math_command_scheme(
2117 &mut self,
2118 name: &str,
2119 tv: MonoType,
2120 span: Option<Span>,
2121 ) -> Result<PolyType, TypeError> {
2122 let (doms, result) = peel_func_chain(tv);
2123 self.unify_ctx(
2124 &t_math_text(),
2125 &result,
2126 span,
2127 &format!("the result of math command '{name}'"),
2128 )?;
2129 let params: Vec<CmdArgType> = doms
2130 .into_iter()
2131 .map(|d| match resolve(&d).into_owned() {
2132 MonoType::Variant(vname, mut vargs) if vname == "option" && vargs.len() == 1 => {
2133 optional(vargs.pop().unwrap())
2134 }
2135 _ => mandatory(d),
2136 })
2137 .collect();
2138 Ok(generalize(self.ctx.level(), &MonoType::MathCmd(params)))
2139 }
2140
2141 /// `Ast::LetMathIn`'s V0_1 scheme-building rule — the `val math` analog
2142 /// of `math_command_scheme` above. The lowering
2143 /// (`v1/lower.rs::lower_bind_v1`) ALWAYS synthesizes exactly three
2144 /// trailing lambdas around a `val math` body — `fun ctx -> fun sub ->
2145 /// fun sup -> …` — so `tv`'s function chain always has at least 3
2146 /// domains; the LAST three are peeled off as `(d_ctx, d_sub, d_sup)`
2147 /// (`context`, `option math-text`, `option math-text`), the bare result
2148 /// must be `math-boxes`, and the REMAINING leading domains become the
2149 /// command's ordinary `CmdArgType` params.
2150 ///
2151 /// Like `command_scheme`'s V0_1 branch,
2152 /// a leading user parameter may be a `?(l = x, …)` bundle, so this uses
2153 /// the row-carrying `peel_func_chain_rows` + `harvest_slot` instead of
2154 /// the plain `_ option` heuristic. The synthesized ctx/sub/sup trailing
2155 /// trio can never legally carry a bundle (`lower_value_math` always
2156 /// wraps them in plain `fun`s, inferring `Row::Empty`) — guarded
2157 /// defensively, since an off-by-one here would silently turn `sub`/`sup`
2158 /// into a labeled slot or eat the last user param (the trio is at the
2159 /// TAIL of the domain chain, opposite inline/block where ctx is FIRST).
2160 fn math_command_scheme_v01(
2161 &mut self,
2162 name: &str,
2163 tv: MonoType,
2164 span: Option<Span>,
2165 ) -> Result<PolyType, TypeError> {
2166 let (mut slots, result) = peel_func_chain_rows(tv);
2167 if slots.len() < 3 {
2168 return Err(TypeError::simple(
2169 span,
2170 format!(
2171 "'val math' command '{name}' must take a context and (via the \
2172 synthesized `with sub sup`/`%math-attach-scripts` wrapper) two \
2173 optional scripts as its trailing arguments — see the math-split spec"
2174 ),
2175 ));
2176 }
2177 let (row_sup, d_sup) = slots.pop().unwrap();
2178 let (row_sub, d_sub) = slots.pop().unwrap();
2179 let (row_ctx, d_ctx) = slots.pop().unwrap();
2180 for (which, row) in [
2181 ("context", &row_ctx),
2182 ("'sub'", &row_sub),
2183 ("'sup'", &row_sup),
2184 ] {
2185 if !matches!(&*resolve_row(row), Row::Empty) {
2186 return Err(TypeError::simple(
2187 span,
2188 format!(
2189 "the {which} argument of 'val math' command '{name}' cannot \
2190 carry a labeled optional bundle"
2191 ),
2192 ));
2193 }
2194 }
2195 self.unify_ctx(
2196 &t_context(),
2197 &d_ctx,
2198 span,
2199 &format!("the context argument of 'val math' command '{name}'"),
2200 )?;
2201 self.unify_ctx(
2202 &t_option(t_math_text()),
2203 &d_sub,
2204 span,
2205 &format!("the 'sub' argument of 'val math' command '{name}'"),
2206 )?;
2207 self.unify_ctx(
2208 &t_option(t_math_text()),
2209 &d_sup,
2210 span,
2211 &format!("the 'sup' argument of 'val math' command '{name}'"),
2212 )?;
2213 self.unify_ctx(
2214 &t_math_boxes(),
2215 &result,
2216 span,
2217 &format!(
2218 "the result of 0.1 math command '{name}' — a `math-boxes`, \
2219 usually via `read-math`"
2220 ),
2221 )?;
2222 let params: Vec<CmdArgType> = slots
2223 .into_iter()
2224 .map(|(row, dom)| harvest_slot(row, dom))
2225 .collect();
2226 Ok(generalize(self.ctx.level(), &MonoType::MathCmd(params)))
2227 }
2228
2229 /// Shared by `check_itext`'s `IText::Cmd`, `check_btext`'s `BText::Cmd`,
2230 /// and `check_math_elem`'s `MathElem::Cmd`: check a command application's
2231 /// argument count (exact — every optional slot is either explicitly
2232 /// marked at the call site or `None`-padded by elaboration, so it is
2233 /// never actually *absent* from `args`) and each argument's type against
2234 /// `params`. An `optional` param's `args[i].arg` is always a
2235 /// `Some(..)`/`None` value (`app_arg_to_ast`'s desugaring), so it's
2236 /// checked against `option(param.ty)`, not `param.ty` directly.
2237 ///
2238 /// Each `args[i]` additionally carries
2239 /// a (possibly empty) supplied `?(l = e, …)` bundle (`args[i].opts`) —
2240 /// every label must be declared in that slot's own closed
2241 /// `param.opt_labels` map (upstream's `UnexpectedOptionalLabel`,
2242 /// `typechecker.ml:900-901`); a declared label this call omits simply
2243 /// defaults to `None` at runtime, nothing to check here for it. `opts`
2244 /// is `[]` for every 0.0.6-reachable call, so this loop is a no-op
2245 /// there.
2246 fn check_cmd_args(
2247 &mut self,
2248 env: &TypeEnv<'s>,
2249 name: &str,
2250 span: Span,
2251 params: &[CmdArgType],
2252 args: &[CmdArg<'s>],
2253 ) -> Result<(), TypeError> {
2254 if params.len() != args.len() {
2255 return Err(TypeError::simple(
2256 Some(span),
2257 format!(
2258 "command '{name}' expects {} argument{}, got {}",
2259 params.len(),
2260 if params.len() == 1 { "" } else { "s" },
2261 args.len()
2262 ),
2263 ));
2264 }
2265 for (i, (param, arg)) in params.iter().zip(args.iter()).enumerate() {
2266 for (label, val) in &arg.opts {
2267 match param.opt_labels.iter().find(|(l, _)| l == label) {
2268 Some((_, lty)) => {
2269 let tval = self.infer(env, val)?;
2270 self.unify_ctx(
2271 lty,
2272 &tval,
2273 ast_span(val).or(Some(span)),
2274 &format!("optional argument `{label}` of '{name}'"),
2275 )?;
2276 }
2277 None => {
2278 return Err(TypeError::simple(
2279 ast_span(val).or(Some(span)),
2280 format!(
2281 "command '{name}' has no optional label `{label}` \
2282 on argument {}",
2283 i + 1
2284 ),
2285 ));
2286 }
2287 }
2288 }
2289 let targ = self.infer(env, &arg.arg)?;
2290 let expected = if param.optional {
2291 t_option(param.ty.clone())
2292 } else {
2293 param.ty.clone()
2294 };
2295 self.unify_ctx(
2296 &expected,
2297 &targ,
2298 ast_span(&arg.arg).or(Some(span)),
2299 &format!("argument {} of '{name}'", i + 1),
2300 )?;
2301 }
2302 Ok(())
2303 }
2304
2305 // ---- expressions -------------------------------------------------------
2306
2307 /// Infer the scheme(s) of ONE binding against a static `env`,
2308 /// WITHOUT extending anything. Returns the
2309 /// `(name, PolyType)` pairs in binding order (singleton for
2310 /// Let/LetMath/LetMutable; group order for LetRec). The caller decides
2311 /// what to put in the environment — the whole-program path commits them
2312 /// verbatim (`env.with_all`).
2313 ///
2314 /// Level discipline, sigil dispatch, generalization, and the
2315 /// value-restriction asymmetry all live INSIDE this method, so callers
2316 /// cannot get them wrong.
2317 pub(crate) fn infer_binding(
2318 &mut self,
2319 env: &TypeEnv<'s>,
2320 binding: BindingView<'_, 's>,
2321 ) -> Result<Vec<(Symbol<'s>, PolyType)>, TypeError> {
2322 match binding {
2323 BindingView::Let { name, value } => {
2324 self.ctx.enter_level();
2325 let tv = self.infer(env, value)?;
2326 self.ctx.leave_level();
2327 let scheme = match command_sigil(self.text(name)) {
2328 // A `\`/`+`-named binding: either a genuine `let-inline`/
2329 // `let-block` definition (`value` is the
2330 // `Lambda(ctxvar, Lambda(p1, .., body))` chain
2331 // `elaborate_let_inline` builds) or a qualified-name
2332 // alias of one (`value` is a bare `Ast::Var`, from a
2333 // module's own `M.\cmd` re-export or an `open`) — see
2334 // `command_scheme`.
2335 Some(sigil) => {
2336 self.command_scheme(self.text(name), sigil, tv, ast_span(value))?
2337 }
2338 None => generalize(self.ctx.level(), &tv),
2339 };
2340 Ok(vec![(name, scheme)])
2341 }
2342
2343 // `let-math \cmd param* = expr in body` — structurally
2344 // identical to the `Let` command- binding rule above, but for a
2345 // binding that is ALREADY known (by construction, via the
2346 // dedicated Ast variant) to be a math command, so there is no
2347 // sigil to dispatch on and no "which kind of `\`-binding is
2348 // this" ambiguity to resolve.
2349 BindingView::LetMath { name, value } => {
2350 self.ctx.enter_level();
2351 let tv = self.infer(env, value)?;
2352 self.ctx.leave_level();
2353 // V0_0's `let-math` and V0_1's `val math` both lower to the
2354 // SAME `Ast::LetMathIn` — only the SCHEME RULE forks, since
2355 // a `val math` binding's lowering always synthesizes exactly
2356 // three trailing ctx/sub/sup lambdas that
2357 // `math_command_scheme`'s v0.0.6 rule knows nothing about.
2358 //
2359 // It forks on the BINDING's generation, not the session's
2360 // (`binding_version`, not `self.version`): in a merged
2361 // cross-version program the session is always `V0_1` while a
2362 // spliced 0.0.6 package's `let-math` RHS carries its own
2363 // `Ast::VersionScope(V0_0, _)`. On every single-version
2364 // program the two agree by construction (no `VersionScope`
2365 // node exists there at all).
2366 let scheme = if self.binding_version(value).math_is_split() {
2367 self.math_command_scheme_v01(self.text(name), tv, ast_span(value))?
2368 } else {
2369 self.math_command_scheme(self.text(name), tv, ast_span(value))?
2370 };
2371 Ok(vec![(name, scheme)])
2372 }
2373
2374 BindingView::LetRec(bindings) => {
2375 self.ctx.enter_level();
2376 // The group's own stage, so a `@stage: 0` library's mutually
2377 // recursive clauses can still see each other (they are read at
2378 // stage 0, and a stage-0 read of a stage-1 binder is refused).
2379 let group_stage = self.binding_stage_rec(bindings);
2380 let mut rec_env = env.clone();
2381 let mut vars = Vec::with_capacity(bindings.len());
2382 for (name, _) in bindings {
2383 let v = self.fresh();
2384 vars.push(v.clone());
2385 rec_env = rec_env.with(*name, PolyType::mono(v), group_stage);
2386 }
2387 for ((name, val), v) in bindings.iter().zip(vars.iter()) {
2388 let tv = self.infer(&rec_env, val)?;
2389 self.unify_ctx(
2390 v,
2391 &tv,
2392 ast_span(val),
2393 &format!("let-rec binding '{}'", self.text(*name)),
2394 )?;
2395 }
2396 self.ctx.leave_level();
2397 let mut schemes = Vec::with_capacity(bindings.len());
2398 for ((name, _), v) in bindings.iter().zip(vars.iter()) {
2399 let scheme = generalize(self.ctx.level(), v);
2400 schemes.push((*name, scheme));
2401 }
2402 Ok(schemes)
2403 }
2404
2405 BindingView::LetMutable { name, init } => {
2406 // NO generalization: `let-mutable`'s binding is the
2407 // classic ML "value restriction" case — a mutable reference
2408 // must stay monomorphic, or `let-mutable r <- [] in ((r <-
2409 // 1 :: !r); (r <- true :: !r); !r)`-style code could smuggle
2410 // an `int` and a `bool` through the very same cell. Binding
2411 // it via `PolyType::mono` (not `generalize`) enforces this
2412 // directly: every use of `name` in `body` shares the exact
2413 // same `Ref` type, not a fresh instantiation.
2414 let tinit = self.infer(env, init)?;
2415 Ok(vec![(name, PolyType::mono(reff(tinit)))])
2416 }
2417 }
2418 }
2419
2420 /// Infer one expression against a static env — a `pub(crate)` wrapper
2421 /// over the private `infer` below, which stays private so the ~40
2422 /// internal `self.infer(` call sites need not change. `v1::module_check`
2423 /// uses this for the document body and any non-binding expression.
2424 pub(crate) fn infer_expr(
2425 &mut self,
2426 env: &TypeEnv<'s>,
2427 ast: &Ast<'s>,
2428 ) -> Result<MonoType, TypeError> {
2429 self.infer(env, ast)
2430 }
2431
2432 /// Drain accumulated non-fatal match warnings (exhaustiveness /
2433 /// redundancy) — for per-binding callers; the whole-program path reads
2434 /// the `warnings` field directly.
2435 pub(crate) fn take_warnings(&mut self) -> Vec<MatchWarning> {
2436 std::mem::take(&mut self.warnings)
2437 }
2438
2439 /// Mutable access to the inference context — sig lowering needs it
2440 /// (`lower_sig_item(item, &mut ctx)`), and its subsumption check mints
2441 /// fresh vars through it.
2442 pub(crate) fn ctx_mut(&mut self) -> &mut TypeContext {
2443 &mut self.ctx
2444 }
2445
2446 /// Expand every synonym reference inside `ty` against this session's
2447 /// synonym table — a `pub(crate)` accessor over the private free fn
2448 /// [`expand_synonyms`], added for `v1/module_check.rs`:
2449 /// a sig `val`'s declared type may mention the
2450 /// module's own `type t = ..` synonym (by its pre-qualified `"M.t"`
2451 /// name, `v1/lower.rs`'s `TypeNameEnv`), and must expand through the
2452 /// SAME table the impl side's `build_variant_decl`/ordinary inference
2453 /// already does, so e.g. `val f : t -> t` over `type t = int` checks
2454 /// against the impl's expanded `int -> int`.
2455 pub(crate) fn expand_synonyms_in(&self, ty: &MonoType) -> Result<MonoType, TypeError> {
2456 expand_synonyms(ty, &self.synonyms)
2457 }
2458
2459 /// Deregister constructors hidden by a signature seal (
2460 /// `v1/module_check.rs`'s ctor-hide trigger — see that module's doc
2461 /// comment). Each entry is removed only if the currently-registered
2462 /// decl's type name matches, guarding against bare-name ctor collisions
2463 /// (`Checker.ctors` is last-writer-wins program-globally, 0.0.6-
2464 /// inherited): if a LATER unsealed variant re-registered the same ctor
2465 /// name after this one, its entry survives the hide untouched.
2466 pub(crate) fn hide_ctors(&mut self, entries: &[(String, String)]) {
2467 for (ctor, tyname) in entries {
2468 if self.ctors.get(ctor).is_some_and(|d| &d.name == tyname) {
2469 self.ctors.remove(ctor);
2470 }
2471 // Also drop the module-qualified key registered by
2472 // `declare_variant` (guarded by the same decl-identity check).
2473 if let Some((modpfx, _)) = tyname.rsplit_once('.') {
2474 let q = format!("{modpfx}.{ctor}");
2475 if self.ctors.get(&q).is_some_and(|d| &d.name == tyname) {
2476 self.ctors.remove(&q);
2477 }
2478 }
2479 }
2480 }
2481
2482 /// `f ?(l = e, …) arg` — SATySFi 0.1 labeled-optional application
2483 /// inference. Kept OUT of the hot [`Checker::infer`] match (via
2484 /// `#[inline(never)]`) so its labeled-optional locals (`Vec`, `Row`) do
2485 /// not enlarge the deeply-recursed `infer` stack frame — see
2486 /// `infer_lambda_opt`.
2487 #[inline(never)]
2488 fn infer_apply_opt(
2489 &mut self,
2490 env: &TypeEnv<'s>,
2491 func: &Ast<'s>,
2492 opts: &[(String, Ast<'s>)],
2493 arg: &Ast<'s>,
2494 ) -> Result<MonoType, TypeError> {
2495 let tf = self.infer(env, func)?;
2496 let ta = self.infer(env, arg)?;
2497 let tr = self.fresh();
2498 let mut opt_tys = Vec::with_capacity(opts.len());
2499 for (label, e) in opts {
2500 opt_tys.push((label.clone(), self.infer(env, e)?));
2501 }
2502 let mut row = Row::Var(self.ctx.fresh_row_var());
2503 for (label, ty) in opt_tys.into_iter().rev() {
2504 row = Row::Cons(label, Box::new(ty), Box::new(row));
2505 }
2506 self.unify_ctx(
2507 &tf,
2508 &MonoType::Func(Box::new(row), Box::new(ta), Box::new(tr.clone())),
2509 ast_span(func),
2510 "function application",
2511 )?;
2512 Ok(tr)
2513 }
2514
2515 /// `fun ?(l = x, …) p -> body` — SATySFi 0.1 labeled-optional lambda
2516 /// inference. Kept OUT of the hot [`Checker::infer`] match
2517 /// (`#[inline(never)]`): `infer` recurses to the AST depth of the program
2518 /// under check, and Rust sizes a function's stack frame to its LARGEST
2519 /// match arm's locals; inlining this arm's `env.clone()` + `Vec`/`Row`
2520 /// locals into every `infer` frame measurably enlarged the deep recursion
2521 /// (enough to overflow the test harness's small default thread stack on a
2522 /// big merged program). Extracting it restores `infer`'s frame to its
2523 /// pre-optional-arg size.
2524 #[inline(never)]
2525 fn infer_lambda_opt(
2526 &mut self,
2527 env: &TypeEnv<'s>,
2528 opts: &[(String, Symbol<'s>)],
2529 param: Symbol<'s>,
2530 body: &Ast<'s>,
2531 ) -> Result<MonoType, TypeError> {
2532 let mut inner = env.clone();
2533 let mut opt_tys = Vec::with_capacity(opts.len());
2534 for (label, binder) in opts {
2535 let tl = self.fresh();
2536 inner = inner.with(*binder, PolyType::mono(t_option(tl.clone())), self.stage);
2537 opt_tys.push((label.clone(), tl));
2538 }
2539 let tp = self.fresh();
2540 inner = inner.with(param, PolyType::mono(tp.clone()), self.stage);
2541 let tb = self.infer(&inner, body)?;
2542 let mut row = Row::Empty;
2543 for (label, tl) in opt_tys.into_iter().rev() {
2544 row = Row::Cons(label, Box::new(tl), Box::new(row));
2545 }
2546 Ok(MonoType::Func(Box::new(row), Box::new(tp), Box::new(tb)))
2547 }
2548
2549 /// The stage a binding whose right-hand side is `value` is introduced at
2550 /// — upstream's `pre.stage` at the point of `Typeenv.add`.
2551 ///
2552 /// Upstream reads a whole FILE at one stage, so `pre.stage` is simply the
2553 /// ambient stage there. This port flattens every library into one
2554 /// `let`-chain and instead marks each spliced binding's RHS with
2555 /// [`Ast::StageScope`] (`elaborate.rs`'s per-item wrap), so the ambient
2556 /// stage is only right for a binding the current file wrote itself. The
2557 /// peeling through `ModuleScope`/`VersionScope` is because those wrappers
2558 /// are applied INSIDE `push_named_binding`/the `LetRec` arm, i.e. after
2559 /// the stage wrap, for a module member.
2560 ///
2561 /// Only the `ModuleScope` half of that peel actually fires: `elaborate.rs`
2562 /// applies `maybe_v006_scope` to a binding's RHS and `stage_wrap_item`
2563 /// outside it, so a cross-version staged binding is always
2564 /// `StageScope(_, VersionScope(_, ..))`. The `VersionScope` arm is kept
2565 /// anyway, because `elaborate::already_staged` peels the same two and the
2566 /// two must not disagree. Deleting the `ModuleScope` arm breaks
2567 /// `xver_staging.rs`'s
2568 /// `the_file_stage_and_the_version_scope_compose_on_a_module_member`.
2569 pub(crate) fn binding_stage(&self, value: &Ast<'s>) -> Stage {
2570 fn declared<'s>(a: &Ast<'s>) -> Option<Stage> {
2571 match a {
2572 Ast::StageScope(st, _) => Some(*st),
2573 Ast::ModuleScope(_, b) | Ast::VersionScope(_, b) => declared(b),
2574 _ => None,
2575 }
2576 }
2577 declared(value).unwrap_or(self.stage)
2578 }
2579
2580 /// The GENERATION one binding was authored in — the version analogue of
2581 /// [`Checker::binding_stage`], and needed for exactly the same reason: a
2582 /// merged cross-version program has ONE `Checker::version`, hard-coded
2583 /// to `V0_1` (`v1::module_check::check_program_inner`), while each
2584 /// spliced 0.0.6 dependency's bindings carry their own
2585 /// `Ast::VersionScope(V0_0, _)` on the RHS (`elaborate::
2586 /// maybe_v006_scope`). Any scheme rule that FORKS on the version must
2587 /// ask the binding, not the session, or a 0.0.6 package's binding is
2588 /// read under 0.1's rule.
2589 ///
2590 /// Concretely: `let-math`. `math_command_scheme` (0.0.6) and
2591 /// `math_command_scheme_v01` are two different rules for the same
2592 /// `Ast::LetMathIn`, and 0.1's demands three synthesized trailing
2593 /// `ctx`/`sub`/`sup` lambdas a 0.0.6 `let-math \frac = math-frac` does
2594 /// not have — dispatching on `self.version` refused EVERY `let-math` in
2595 /// a crossed 0.0.6 package, including the bundled `math.satyh` that
2596 /// `@require:` reaches transitively (`texlogo`, `latexcmds`, `siunitx`,
2597 /// …).
2598 ///
2599 /// The wrapper peel is `binding_stage`'s, minus its terminating arm:
2600 /// `elaborate::walk_bindings` puts `VersionScope` INSIDE `StageScope`
2601 /// (`already_staged`'s doc comment), so a staged spliced binding is
2602 /// `StageScope(_, VersionScope(_, ..))` and this must look through
2603 /// `StageScope` too.
2604 ///
2605 /// The peel alone is not enough, hence the `scoped_version` fallback:
2606 /// `maybe_v006_scope` wraps a TOP-LEVEL binding's RHS, but an
2607 /// EXPRESSION-level `let-math \c = e in body` (`elaborate.rs`'s
2608 /// `Expr::LetMathIn` arm, e.g. `siunitx`'s `let-math \C = ord \`C\` in
2609 /// ${\math-sup{}{\circ}\C}`) is a node inside another binding's
2610 /// already-wrapped RHS and carries no wrapper of its own — so the
2611 /// ambient generation, recorded by the `Ast::VersionScope` infer arm as
2612 /// it descends, answers for it instead. Outside every scope this is
2613 /// `None` and the session's own version answers.
2614 pub(crate) fn binding_version(&self, value: &Ast<'s>) -> RustyfiVersion {
2615 fn declared<'s>(a: &Ast<'s>) -> Option<RustyfiVersion> {
2616 match a {
2617 Ast::VersionScope(v, _) => Some(*v),
2618 Ast::StageScope(_, b) | Ast::ModuleScope(_, b) => declared(b),
2619 _ => None,
2620 }
2621 }
2622 declared(value)
2623 .or(self.scoped_version)
2624 .unwrap_or(self.version)
2625 }
2626
2627 /// [`Checker::binding_stage`] for a `let-rec` GROUP. Every clause of one
2628 /// group comes from one item of one file, so they share a stage; the
2629 /// elaborator wraps them identically and the first is representative.
2630 pub(crate) fn binding_stage_rec(&self, bindings: &[(Symbol<'s>, Rc<Ast<'s>>)]) -> Stage {
2631 match bindings.first() {
2632 Some((_, v)) => self.binding_stage(v),
2633 None => self.stage,
2634 }
2635 }
2636
2637 /// Look `name` up for an OCCURRENCE, enforcing the staging matrix
2638 /// ([`Stage::can_reference`]) — upstream's `UTContentOf` arm, which is
2639 /// where a stage-0 name used from stage 1 (or the reverse) is refused.
2640 ///
2641 /// `Ok(None)` means unbound, left to each caller because each has its own
2642 /// "should not happen post-elaboration" wording. `what` names the kind of
2643 /// occurrence for the diagnostic (`"variable"`, `"inline command"`, …).
2644 fn staged<'e>(
2645 &self,
2646 env: &'e TypeEnv<'s>,
2647 name: Symbol<'s>,
2648 span: Option<Span>,
2649 what: &str,
2650 ) -> Result<Option<&'e PolyType>, TypeError> {
2651 let Some(entry) = env.entry(name) else {
2652 return Ok(None);
2653 };
2654 if !self.stage.can_reference(entry.stage) {
2655 return Err(TypeError::simple(
2656 span,
2657 format!(
2658 "invalid occurrence of {what} '{}' as to stage: it is bound at {}, \
2659 but this is {}",
2660 self.text(name),
2661 entry.stage.as_str(),
2662 self.stage.as_str()
2663 ),
2664 ));
2665 }
2666 Ok(Some(&entry.poly))
2667 }
2668
2669 /// `infer`, reading `ast` at a different stage and restoring afterwards --
2670 /// the type-side twin of the `ctor_scope` push/pop below it.
2671 fn infer_at(
2672 &mut self,
2673 stage: Stage,
2674 env: &TypeEnv<'s>,
2675 ast: &Ast<'s>,
2676 ) -> Result<MonoType, TypeError> {
2677 let saved = std::mem::replace(&mut self.stage, stage);
2678 let result = self.infer(env, ast);
2679 self.stage = saved;
2680 result
2681 }
2682
2683 fn infer(&mut self, env: &TypeEnv<'s>, ast: &Ast<'s>) -> Result<MonoType, TypeError> {
2684 match ast {
2685 // A binding spliced in from a file whose `@stage:` was not the
2686 // default: read it at that stage, so its quotes are legal.
2687 Ast::StageScope(stage, body) => self.infer_at(*stage, env, body),
2688 // `&e` — quote. Legal only at stage 0; its body is read one
2689 // stage later, and the result is that body's type wrapped in
2690 // `code` (upstream `typechecker.ml`'s `UTNext` arm).
2691 Ast::Next(inner) => {
2692 if self.stage != Stage::Stage0 {
2693 return Err(TypeError::simple(
2694 None,
2695 format!(
2696 "`&` (next-stage quote) is only valid at stage 0, but this is {}",
2697 self.stage.as_str()
2698 ),
2699 ));
2700 }
2701 let ty = self.infer_at(Stage::Stage1, env, inner)?;
2702 Ok(MonoType::Code(Box::new(ty)))
2703 }
2704 // `~e` — splice. Legal only at stage 1; its body is read one stage
2705 // earlier and must produce `code b`, which this expression then
2706 // stands for (upstream's `UTPrev` arm).
2707 Ast::Prev(inner) => {
2708 if self.stage != Stage::Stage1 {
2709 return Err(TypeError::simple(
2710 None,
2711 format!(
2712 "`~` (previous-stage splice) is only valid at stage 1, but this is {}",
2713 self.stage.as_str()
2714 ),
2715 ));
2716 }
2717 let ty = self.infer_at(Stage::Stage0, env, inner)?;
2718 let beta = MonoType::Var(self.ctx.fresh_var());
2719 unify(&ty, &MonoType::Code(Box::new(beta.clone())))
2720 .map_err(|e| TypeError::from_unify(None, "a `~` splice", e))?;
2721 Ok(beta)
2722 }
2723 Ast::Unit => Ok(t_unit()),
2724 Ast::Bool(_) => Ok(t_bool()),
2725 Ast::Int(_) => Ok(t_int()),
2726 Ast::Float(_) => Ok(t_float()),
2727 Ast::Length(_) => Ok(t_length()),
2728 Ast::Str(_) => Ok(t_string()),
2729
2730 Ast::Var(name, span) => match self.staged(env, *name, Some(*span), "variable")? {
2731 Some(poly) => Ok(instantiate(poly, self.ctx.level())),
2732 // Should not happen post-elaboration: `elaborate.rs`'s
2733 // `scoped_var` already rejects any unbound name before this
2734 // ever runs. Surfaced as a (spanned) error rather than a
2735 // panic anyway, since "should not happen" isn't "cannot".
2736 None => Err(TypeError::simple(
2737 Some(*span),
2738 format!(
2739 "internal error: unbound variable '{}' reached the typechecker",
2740 self.text(*name)
2741 ),
2742 )),
2743 },
2744
2745 Ast::Apply(f, a) => {
2746 let tf = self.infer(env, f)?;
2747 let ta = self.infer(env, a)?;
2748 let tr = self.fresh();
2749 // Version-split the optional-argument row: 0.0.6 functions
2750 // provably carry no labeled optionals (`Row::Empty`, matching
2751 // `arrow()` and every prim). Under 0.1 a fresh open row var
2752 // absorbs the callee's
2753 // declared optional row, letting a *plain* call of an
2754 // opt-taking function typecheck (defaulting every optional to
2755 // `None` at run time) and making higher-order code
2756 // row-polymorphic after generalization.
2757 let opts_row = if self.version.has_row_polymorphism() {
2758 Row::Var(self.ctx.fresh_row_var())
2759 } else {
2760 Row::Empty
2761 };
2762 self.unify_ctx(
2763 &tf,
2764 &MonoType::Func(Box::new(opts_row), Box::new(ta), Box::new(tr.clone())),
2765 ast_span(f),
2766 "function application",
2767 )?;
2768 Ok(tr)
2769 }
2770
2771 Ast::Lambda(param, body) => {
2772 let tp = self.fresh();
2773 let inner = env.with(*param, PolyType::mono(tp.clone()), self.stage);
2774 let tb = self.infer(&inner, body)?;
2775 Ok(arrow(tp, tb))
2776 }
2777
2778 // `f ?(l = e, …) arg` (SATySFi 0.1; upstream typechecker.ml's
2779 // `Apply(labmap, …)`). The callee must unify with a function
2780 // whose optional row carries at least each supplied `l : τ_l`,
2781 // with an open tail (a fresh row var) for any further optionals
2782 // the callee declares that this call omits.
2783 Ast::ApplyOpt { func, opts, arg } => self.infer_apply_opt(env, func, opts, arg),
2784
2785 // `fun ?(l = x, …) p -> body` (SATySFi 0.1; upstream
2786 // `Function(evid_labmap, …)`). Each labeled optional binder `x`
2787 // is bound at `option τ_l` inside the body; the resulting
2788 // function type carries a CLOSED row `?(l : τ_l, …)` — the very
2789 // same fresh `τ_l` shared between the binder's `option τ_l` and
2790 // the row's `Cons(l, τ_l)`.
2791 Ast::LambdaOpt { opts, param, body } => self.infer_lambda_opt(env, opts, *param, body),
2792
2793 Ast::LetIn(name, value, body) => {
2794 let schemes = self.infer_binding(env, BindingView::Let { name: *name, value })?;
2795 let inner = env.with_all(schemes, self.binding_stage(value));
2796 self.infer(&inner, body)
2797 }
2798
2799 // `let-math \cmd param* = expr in body` — see
2800 // `infer_binding`'s `BindingView::LetMath` arm.
2801 Ast::LetMathIn(name, value, body) => {
2802 let schemes =
2803 self.infer_binding(env, BindingView::LetMath { name: *name, value })?;
2804 let inner = env.with_all(schemes, self.binding_stage(value));
2805 self.infer(&inner, body)
2806 }
2807
2808 Ast::LetRecIn(bindings, body) => {
2809 let schemes = self.infer_binding(env, BindingView::LetRec(bindings))?;
2810 let inner = env.with_all(schemes, self.binding_stage_rec(bindings));
2811 self.infer(&inner, body)
2812 }
2813
2814 Ast::IfThenElse(cond, then_b, else_b) => {
2815 let tc = self.infer(env, cond)?;
2816 self.unify_ctx(&t_bool(), &tc, ast_span(cond), "the condition of 'if'")?;
2817 let tt = self.infer(env, then_b)?;
2818 let te = self.infer(env, else_b)?;
2819 self.unify_ctx(&tt, &te, ast_span(else_b), "the branches of 'if'")?;
2820 Ok(tt)
2821 }
2822
2823 Ast::Match(scrutinee, arms) => {
2824 let tscrut = self.infer(env, scrutinee)?;
2825 let mut result: Option<MonoType> = None;
2826 for arm in arms {
2827 let arm_env = self.bind_pattern(env.clone(), &arm.pat, &tscrut)?;
2828 if let Some(guard) = &arm.guard {
2829 let tg = self.infer(&arm_env, guard)?;
2830 self.unify_ctx(&t_bool(), &tg, ast_span(guard), "a match guard")?;
2831 }
2832 let tbody = self.infer(&arm_env, &arm.body)?;
2833 match &result {
2834 None => result = Some(tbody),
2835 Some(r) => {
2836 self.unify_ctx(r, &tbody, ast_span(&arm.body), "the arms of 'match'")?
2837 }
2838 }
2839 }
2840 // Exhaustiveness/redundancy: non-fatal, so it runs
2841 // only after every arm has
2842 // typechecked, against `tscrut` as resolved as inference will
2843 // ever make it. See `exhaustive::check_match`'s doc comment.
2844 let resolved_scrut = resolve(&tscrut);
2845 let new_warnings = crate::exhaustive::check_match(
2846 self.store,
2847 &resolved_scrut,
2848 ast_span(scrutinee),
2849 arms,
2850 &self.variants,
2851 );
2852 self.warnings.extend(new_warnings);
2853 // `Match`'s `arms` is always non-empty (`c::Expr::Match`
2854 // requires a `first` arm plus zero or more `rest`), so
2855 // `result` is always `Some` in practice; the fallback fresh
2856 // variable is defensive only.
2857 Ok(result.unwrap_or_else(|| self.fresh()))
2858 }
2859
2860 Ast::Tuple(items) => {
2861 let tys = items
2862 .iter()
2863 .map(|it| self.infer(env, it))
2864 .collect::<Result<Vec<_>, _>>()?;
2865 Ok(product(tys))
2866 }
2867
2868 Ast::Ctor(name, payload) => self.infer_ctor(env, name, payload.as_deref(), None),
2869
2870 Ast::Record(fields) => {
2871 let mut typed = Vec::with_capacity(fields.len());
2872 for (label, e) in fields {
2873 typed.push((label.clone(), self.infer(env, e)?));
2874 }
2875 let mut row = Row::Empty;
2876 for (label, ty) in typed.into_iter().rev() {
2877 row = Row::Cons(label, Box::new(ty), Box::new(row));
2878 }
2879 Ok(MonoType::Record(row))
2880 }
2881
2882 Ast::List(items) => {
2883 let elem = self.fresh();
2884 for it in items {
2885 let t = self.infer(env, it)?;
2886 self.unify_ctx(&elem, &t, ast_span(it), "a list element")?;
2887 }
2888 Ok(list(elem))
2889 }
2890
2891 Ast::InlineText(elems) => {
2892 for e in elems.iter() {
2893 self.check_itext(env, e)?;
2894 }
2895 Ok(t_inline_text())
2896 }
2897
2898 Ast::BlockText(elems) => {
2899 for e in elems.iter() {
2900 self.check_btext(env, e)?;
2901 }
2902 Ok(t_block_text())
2903 }
2904
2905 Ast::MathText(elems) => {
2906 for e in elems.iter() {
2907 self.check_math_elem(env, e)?;
2908 }
2909 Ok(MonoType::Base(BaseType::MathText))
2910 }
2911
2912 Ast::LetMutableIn(name, init, body) => {
2913 // NO generalization: `let-mutable`'s binding is the
2914 // classic ML "value restriction" case — see
2915 // `infer_binding`'s `BindingView::LetMutable` arm.
2916 let schemes =
2917 self.infer_binding(env, BindingView::LetMutable { name: *name, init })?;
2918 let inner = env.with_all(schemes, self.binding_stage(init));
2919 self.infer(&inner, body)
2920 }
2921
2922 Ast::Overwrite(name, span, value) => {
2923 let t_ref = match self.staged(env, *name, Some(*span), "mutable variable")? {
2924 Some(poly) => instantiate(poly, self.ctx.level()),
2925 None => {
2926 return Err(TypeError::simple(
2927 Some(*span),
2928 format!(
2929 "internal error: unbound mutable variable '{}' reached the typechecker",
2930 self.text(*name)
2931 ),
2932 ))
2933 }
2934 };
2935 let inner = self.fresh();
2936 self.unify_ctx(
2937 &t_ref,
2938 &reff(inner.clone()),
2939 Some(*span),
2940 &format!("the overwrite target '{}'", self.text(*name)),
2941 )?;
2942 let tvalue = self.infer(env, value)?;
2943 // Prefer the overwrite's own (always-present) span over
2944 // `ast_span(value)`, which is `None` for most value shapes
2945 // (literals carry no span at all — see `ast.rs`'s module
2946 // doc comment) and would otherwise leave this common error
2947 // unlocated.
2948 self.unify_ctx(
2949 &inner,
2950 &tvalue,
2951 ast_span(value).or(Some(*span)),
2952 &format!("the overwrite value for '{}'", self.text(*name)),
2953 )?;
2954 Ok(t_unit())
2955 }
2956
2957 Ast::WhileDo(cond, body) => {
2958 let tc = self.infer(env, cond)?;
2959 self.unify_ctx(&t_bool(), &tc, ast_span(cond), "the condition of 'while'")?;
2960 let tb = self.infer(env, body)?;
2961 self.unify_ctx(&t_unit(), &tb, ast_span(body), "the body of 'while'")?;
2962 Ok(t_unit())
2963 }
2964
2965 Ast::Sequential(a, b) => {
2966 let ta = self.infer(env, a)?;
2967 // v0.0.6 requires the left-hand side of `before`/`;` to be
2968 // `unit` (`typechecker.ml`'s `UTSequential` case): not just
2969 // "evaluated and discarded" but type-checked as `unit`
2970 // specifically, so e.g. a stray non-unit expression used
2971 // only for effect (but returning, say, an `int`) is rejected
2972 // rather than silently ignored.
2973 self.unify_ctx(
2974 &t_unit(),
2975 &ta,
2976 ast_span(a),
2977 "the left-hand side of 'before'",
2978 )?;
2979 self.infer(env, b)
2980 }
2981
2982 Ast::AccessField(e, label, span) => {
2983 let te = self.infer(env, e)?;
2984 let field = self.fresh();
2985 let rv = self.ctx.fresh_row_var();
2986 let open_row = MonoType::Record(Row::Cons(
2987 label.clone(),
2988 Box::new(field.clone()),
2989 Box::new(Row::Var(rv)),
2990 ));
2991 self.unify_ctx(
2992 &open_row,
2993 &te,
2994 Some(*span),
2995 &format!("the field access '#{label}'"),
2996 )?;
2997 Ok(field)
2998 }
2999
3000 Ast::UpdateField(base, label, value) => {
3001 let tbase = self.infer(env, base)?;
3002 let tvalue = self.infer(env, value)?;
3003 let rv = self.ctx.fresh_row_var();
3004 let open_row = MonoType::Record(Row::Cons(
3005 label.clone(),
3006 Box::new(tvalue),
3007 Box::new(Row::Var(rv)),
3008 ));
3009 self.unify_ctx(
3010 &open_row,
3011 &tbase,
3012 ast_span(base),
3013 &format!("the record update of '{label}'"),
3014 )?;
3015 Ok(tbase)
3016 }
3017
3018 // Swap the active primitive-type env to `version`'s
3019 // for `body` — see `version_scoped_type_env`'s doc comment —
3020 // and make sure `version`'s builtin ADTs (e.g. `page`,
3021 // `V0_0`-only) are registered in the (otherwise
3022 // whole-program-tagged) ctor table — see
3023 // `install_additional_builtin_variants`'s doc comment. Never
3024 // reached on a pure single-version program (no
3025 // `Ast::VersionScope` node is ever produced there).
3026 Ast::VersionScope(version, body) => {
3027 self.install_additional_builtin_variants(*version);
3028 let scoped = version_scoped_type_env(self.store, env, *version);
3029 // Also make the generation available to any version-forking
3030 // rule reached INSIDE the body — an expression-level
3031 // `let-math .. in ..` is the one that needs it, since its
3032 // own `Ast::LetMathIn` carries no wrapper of its own. See
3033 // `Checker::binding_version`.
3034 let saved = self.scoped_version.replace(*version);
3035 let r = self.infer(&scoped, body);
3036 self.scoped_version = saved;
3037 r
3038 }
3039 // A module member's body: resolve its bare constructor references
3040 // against `path`'s constructors first. `path` is the full absolute
3041 // module path (nested modules wrap with `["M","N"]`), so replace
3042 // rather than push.
3043 Ast::ModuleScope(path, body) => {
3044 let saved = std::mem::replace(&mut self.ctor_scope, path.clone());
3045 let r = self.infer(env, body);
3046 self.ctor_scope = saved;
3047 r
3048 }
3049 }
3050 }
3051
3052 /// Look up a constructor honoring the current [`Checker::ctor_scope`]: try
3053 /// the innermost-out module-qualified keys (`M.N.Ctor`, `M.Ctor`) before
3054 /// the bare fallback (`Ctor`). Keeps the returned decl's ctor NAME strings
3055 /// bare — only the table KEY is qualified — so eval/exhaustiveness/error
3056 /// text are untouched.
3057 fn lookup_ctor(&self, name: &str) -> Option<Rc<VariantDecl>> {
3058 for k in (1..=self.ctor_scope.len()).rev() {
3059 let key = format!("{}.{}", self.ctor_scope[..k].join("."), name);
3060 if let Some(d) = self.ctors.get(&key) {
3061 return Some(d.clone());
3062 }
3063 }
3064 self.ctors.get(name).cloned()
3065 }
3066
3067 /// Shared by `Ast::Ctor` and pattern-matching's `Pattern::Ctor`: look up
3068 /// `name`'s declaration, mint fresh type arguments for its (possibly
3069 /// zero) parameters, and check the payload — either an already-inferred
3070 /// expression type to unify against (`Ast::Ctor`'s case, via `infer`
3071 /// directly) or nothing (patterns bind their own payload separately, in
3072 /// `bind_pattern`). `expected_result`, if given, is unified against the
3073 /// application's result type — used by nothing yet in this port's
3074 /// rules but kept general for symmetry; always `None` from `infer`,
3075 /// which just returns the result type instead.
3076 fn infer_ctor(
3077 &mut self,
3078 env: &TypeEnv<'s>,
3079 name: &str,
3080 payload: Option<&Ast<'s>>,
3081 expected_result: Option<&MonoType>,
3082 ) -> Result<MonoType, TypeError> {
3083 let decl = self
3084 .lookup_ctor(name)
3085 .ok_or_else(|| TypeError::simple(None, format!("unknown constructor '{name}'")))?;
3086 let args: Vec<MonoType> = (0..decl.params).map(|_| self.fresh()).collect();
3087 let (payload_ty, result_ty) = decl.instantiate_ctor(name, &args).ok_or_else(|| {
3088 TypeError::simple(
3089 None,
3090 format!("constructor '{name}' applied with the wrong number of type arguments"),
3091 )
3092 })?;
3093 if let Some(expected) = expected_result {
3094 self.unify_ctx(expected, &result_ty, None, &format!("constructor '{name}'"))?;
3095 }
3096 match (payload_ty, payload) {
3097 (Some(expected), Some(actual)) => {
3098 let actual_ty = self.infer(env, actual)?;
3099 self.unify_ctx(
3100 &expected,
3101 &actual_ty,
3102 ast_span(actual),
3103 &format!("the payload of constructor '{name}'"),
3104 )?;
3105 }
3106 (None, None) => {}
3107 (Some(_), None) => {
3108 return Err(TypeError::simple(
3109 None,
3110 format!("constructor '{name}' expects a payload but none was given"),
3111 ))
3112 }
3113 (None, Some(_)) => {
3114 return Err(TypeError::simple(
3115 None,
3116 format!("constructor '{name}' takes no payload but one was given"),
3117 ))
3118 }
3119 }
3120 Ok(result_ty)
3121 }
3122
3123 // ---- patterns ------------------------------------------------------
3124
3125 /// Type-check `pat` against `ty`, extending (a clone of) `env` with
3126 /// every name it binds. Mirrors `typechecker.ml`'s `typecheck_pattern`.
3127 fn bind_pattern(
3128 &mut self,
3129 env: TypeEnv<'s>,
3130 pat: &Pattern<'s>,
3131 ty: &MonoType,
3132 ) -> Result<TypeEnv<'s>, TypeError> {
3133 match pat {
3134 Pattern::Wild => Ok(env),
3135 Pattern::Var(name) => Ok(env.with(*name, PolyType::mono(ty.clone()), self.stage)),
3136 Pattern::Unit => {
3137 self.unify_ctx(&t_unit(), ty, None, "a unit pattern")?;
3138 Ok(env)
3139 }
3140 Pattern::Bool(_) => {
3141 self.unify_ctx(&t_bool(), ty, None, "a boolean pattern")?;
3142 Ok(env)
3143 }
3144 Pattern::Int(_) => {
3145 self.unify_ctx(&t_int(), ty, None, "an integer pattern")?;
3146 Ok(env)
3147 }
3148 Pattern::Str(_) => {
3149 self.unify_ctx(&t_string(), ty, None, "a string pattern")?;
3150 Ok(env)
3151 }
3152 Pattern::Tuple(pats) => {
3153 let elem_tys: Vec<MonoType> = pats.iter().map(|_| self.fresh()).collect();
3154 self.unify_ctx(&product(elem_tys.clone()), ty, None, "a tuple pattern")?;
3155 let mut env = env;
3156 for (p, t) in pats.iter().zip(elem_tys.iter()) {
3157 env = self.bind_pattern(env, p, t)?;
3158 }
3159 Ok(env)
3160 }
3161 Pattern::EmptyList => {
3162 let elem = self.fresh();
3163 self.unify_ctx(&list(elem), ty, None, "an empty-list pattern")?;
3164 Ok(env)
3165 }
3166 Pattern::Cons(head, tail) => {
3167 let elem = self.fresh();
3168 self.unify_ctx(&list(elem.clone()), ty, None, "a cons pattern")?;
3169 let env = self.bind_pattern(env, head, &elem)?;
3170 self.bind_pattern(env, tail, &list(elem))
3171 }
3172 Pattern::Ctor(name, payload) => {
3173 let decl = self.lookup_ctor(name).ok_or_else(|| {
3174 TypeError::simple(None, format!("unknown constructor '{name}' in a pattern"))
3175 })?;
3176 let args: Vec<MonoType> = (0..decl.params).map(|_| self.fresh()).collect();
3177 let (payload_ty, result_ty) = decl.instantiate_ctor(name, &args).ok_or_else(|| {
3178 TypeError::simple(
3179 None,
3180 format!(
3181 "constructor '{name}' applied with the wrong number of type arguments in a pattern"
3182 ),
3183 )
3184 })?;
3185 self.unify_ctx(
3186 &result_ty,
3187 ty,
3188 None,
3189 &format!("the constructor pattern '{name}'"),
3190 )?;
3191 match (payload_ty, payload) {
3192 (Some(expected), Some(p)) => self.bind_pattern(env, p, &expected),
3193 (None, None) => Ok(env),
3194 (Some(_), None) => Err(TypeError::simple(
3195 None,
3196 format!(
3197 "constructor pattern '{name}' expects a payload but none was given"
3198 ),
3199 )),
3200 (None, Some(_)) => Err(TypeError::simple(
3201 None,
3202 format!("constructor pattern '{name}' takes no payload but one was given"),
3203 )),
3204 }
3205 }
3206 Pattern::As(inner, name) => {
3207 let env = self.bind_pattern(env, inner, ty)?;
3208 Ok(env.with(*name, PolyType::mono(ty.clone()), self.stage))
3209 }
3210 }
3211 }
3212
3213 // ---- inline / block / math text -------------------------------------
3214
3215 /// Check one inline-text element. A command's own type is a genuine
3216 /// `MonoType::InlineCmd(params)` (`[...] inline-cmd`, mirroring v0.0.6's
3217 /// `HorzCommandType`) — bound either by `Ast::LetIn`'s command-binding
3218 /// rule (`Checker::command_scheme`) or, for the port's built-in
3219 /// commands, directly by `prim_types::primitive_type`'s `\emph` entry.
3220 /// Checking an application here is exact-arity plus one unification per
3221 /// argument against `params`, via `check_cmd_args` — there is no
3222 /// `context -> arg1 -> .. -> inline-boxes` function shape to unify the
3223 /// whole command type against.
3224 fn check_itext(&mut self, env: &TypeEnv<'s>, it: &IText<'s>) -> Result<(), TypeError> {
3225 match it {
3226 IText::Text(_) | IText::CodeText(_) => Ok(()),
3227 IText::Cmd { name, span, args } => {
3228 let tcmd = match self.staged(env, *name, Some(*span), "inline command")? {
3229 Some(poly) => instantiate(poly, self.ctx.level()),
3230 None => {
3231 return Err(TypeError::simple(
3232 Some(*span),
3233 format!(
3234 "internal error: unbound inline command '{}' reached the typechecker",
3235 self.text(*name)
3236 ),
3237 ))
3238 }
3239 };
3240 match &*resolve(&tcmd) {
3241 MonoType::InlineCmd(params) => {
3242 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3243 }
3244 other => Err(TypeError::simple(
3245 Some(*span),
3246 format!(
3247 "internal error: inline command '{}' does not have an \
3248 inline-cmd type (found `{other}`)",
3249 self.text(*name)
3250 ),
3251 )),
3252 }
3253 }
3254 IText::Embed { expr, span } => {
3255 let te = self.infer(env, expr)?;
3256 self.unify_ctx(
3257 &t_inline_text(),
3258 &te,
3259 Some(*span),
3260 "an inline-text '#…;' embed",
3261 )?;
3262 Ok(())
3263 }
3264 IText::EmbedMath { elems, span: _ } => {
3265 // PERMISSIVE: there is no real type to check a quoted-math
3266 // embed's expressions against. Type each against its own
3267 // fresh variable, purely so unbound-name mistakes inside it
3268 // are still caught, without asserting anything about the
3269 // result.
3270 for me in elems.iter() {
3271 self.check_math_elem(env, me)?;
3272 }
3273 Ok(())
3274 }
3275 }
3276 }
3277
3278 /// Block-text analogue of `check_itext`'s `IText::Cmd` case — see its
3279 /// doc comment; a `BText::Cmd`'s type is `MonoType::BlockCmd(params)`.
3280 fn check_btext(&mut self, env: &TypeEnv<'s>, bt: &BText<'s>) -> Result<(), TypeError> {
3281 match bt {
3282 BText::Cmd { name, span, args } => {
3283 let tcmd = match self.staged(env, *name, Some(*span), "block command")? {
3284 Some(poly) => instantiate(poly, self.ctx.level()),
3285 None => {
3286 return Err(TypeError::simple(
3287 Some(*span),
3288 format!(
3289 "internal error: unbound block command '{}' reached the typechecker",
3290 self.text(*name)
3291 ),
3292 ))
3293 }
3294 };
3295 match &*resolve(&tcmd) {
3296 MonoType::BlockCmd(params) => {
3297 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3298 }
3299 other => Err(TypeError::simple(
3300 Some(*span),
3301 format!(
3302 "internal error: block command '{}' does not have a \
3303 block-cmd type (found `{other}`)",
3304 self.text(*name)
3305 ),
3306 )),
3307 }
3308 }
3309 BText::Embed { expr, span } => {
3310 let te = self.infer(env, expr)?;
3311 self.unify_ctx(
3312 &t_block_text(),
3313 &te,
3314 Some(*span),
3315 "a block-text '#…;' embed",
3316 )?;
3317 Ok(())
3318 }
3319 }
3320 }
3321
3322 /// Walk one quoted math element. `Chars`/`Group`/`Sub`/`Sup`/`Primes`
3323 /// carry no program-mode content of their own (nothing to check beyond
3324 /// recursing). `Cmd`/`Embed` are where math meets the ordinary
3325 /// expression language: a `Cmd`'s `name` must resolve to a genuine
3326 /// `MathCmd` type (checked exactly like `check_itext`'s `IText::Cmd`,
3327 /// via `check_cmd_args` — a math command's optional `?:`/`?*`-marked
3328 /// or marker-less-padded arguments are handled by `check_cmd_args` the
3329 /// same generic way), and an `Embed`'s (`#expr`) type must unify with
3330 /// `math` (a math command parameter, or another program-mode value
3331 /// that itself produces math — `Value::Math`/ `Value::MathText` are
3332 /// the two runtime shapes this unifies against, see `value.rs`).
3333 fn check_math_elem(&mut self, env: &TypeEnv<'s>, m: &MathElem<'s>) -> Result<(), TypeError> {
3334 match m {
3335 MathElem::Chars(_) => Ok(()),
3336 MathElem::Group(elems) => {
3337 for e in elems {
3338 self.check_math_elem(env, e)?;
3339 }
3340 Ok(())
3341 }
3342 MathElem::Sub(base, script) | MathElem::Sup(base, script) => {
3343 self.check_math_elem(env, base)?;
3344 for e in script {
3345 self.check_math_elem(env, e)?;
3346 }
3347 Ok(())
3348 }
3349 MathElem::Primes(base, _) => self.check_math_elem(env, base),
3350 MathElem::Cmd { name, span, args } => {
3351 let tcmd = match self.staged(env, *name, Some(*span), "math command")? {
3352 Some(poly) => instantiate(poly, self.ctx.level()),
3353 None => {
3354 return Err(TypeError::simple(
3355 Some(*span),
3356 format!(
3357 "internal error: unbound math command '{}' reached the typechecker",
3358 self.text(*name)
3359 ),
3360 ))
3361 }
3362 };
3363 match &*resolve(&tcmd) {
3364 MonoType::MathCmd(params) => {
3365 self.check_cmd_args(env, self.text(*name), *span, ¶ms, args)
3366 }
3367 other => Err(TypeError::simple(
3368 Some(*span),
3369 format!(
3370 "internal error: math command '{}' does not have a \
3371 math-cmd type (found `{other}`)",
3372 self.text(*name)
3373 ),
3374 )),
3375 }
3376 }
3377 MathElem::Embed { expr, span } => {
3378 let te = self.infer(env, expr)?;
3379 self.unify_ctx(&t_math_text(), &te, Some(*span), "a math '#…' embed")?;
3380 Ok(())
3381 }
3382 }
3383 }
3384}
3385
3386/// If `name` (an `Ast::LetIn` binding's name) is command-shaped, the sigil
3387/// that says which kind — `'\\'` for an inline command, `'+'` for a block
3388/// command — else `None` for an ordinary variable binding.
3389///
3390/// Looks only at the *local* segment (after the last `.`): a
3391/// module-qualified command is spelled e.g. `"M.\cmd"`, sigil on the local
3392/// part only (module names can never start with `\`/`+` — `qualify_key`'s
3393/// doc comment). A bare name has no `.` at all, so `rsplit('.').next()`
3394/// degrades to the whole string.
3395///
3396/// **Must also check the second character.** A genuine command sigil is
3397/// always immediately followed by an identifier, but a parenthesized
3398/// operator NAME (`cst.rs`'s `BindName`) can merely *start* with the same
3399/// character, e.g. `let (+++>) = ..` (`itemize.satyh`) or `let (+.) = ..`.
3400/// Requiring an alphabetic second character mirrors the lexer's own split
3401/// and keeps such an operator name an ordinary variable binding rather than
3402/// a false-positive command.
3403fn command_sigil(name: &str) -> Option<char> {
3404 let local = name.rsplit('.').next().unwrap_or(name);
3405 let mut chars = local.chars();
3406 match chars.next() {
3407 Some(c @ ('\\' | '+')) if chars.next().is_some_and(|c2| c2.is_ascii_alphabetic()) => {
3408 Some(c)
3409 }
3410 _ => None,
3411 }
3412}
3413
3414/// Greedily unwrap a (resolved) `Func` chain into its list of domains and
3415/// final codomain: `dom1 -> dom2 -> .. -> domN -> result` becomes
3416/// `(vec![dom1, .., domN], result)`. Only ever follows the *codomain* at
3417/// each step (never recurses into a domain, even one that is itself a
3418/// `Func`) — used by `Checker::command_scheme` to recover a `let-inline`/
3419/// `let-block` binding's `context -> arg1 -> .. -> argN -> result` shape
3420/// from its ordinarily-inferred function type.
3421fn peel_func_chain(ty: MonoType) -> (Vec<MonoType>, MonoType) {
3422 let mut doms = Vec::new();
3423 let mut cur = ty;
3424 loop {
3425 // Owned: this walk moves each arrow's domain/codomain out.
3426 match resolve(&cur).into_owned() {
3427 MonoType::Func(_row, dom, cod) => {
3428 doms.push(*dom);
3429 cur = *cod;
3430 }
3431 other => return (doms, other),
3432 }
3433 }
3434}
3435
3436/// [`peel_func_chain`]'s row-carrying twin:
3437/// same greedy unwrap, but keeps each arrow's own (resolved) optional-
3438/// argument [`Row`] alongside its domain, since a V0_1 command's `LambdaOpt`-
3439/// produced arrows carry each parameter's `?(l:τ,…)` bundle on that
3440/// PARAMETER's own arrow (the arrow whose *domain* is the labeled argument —
3441/// see `Checker::command_scheme`'s V0_1 harvest). Used only by
3442/// `command_scheme`; `check_cmd_args`/`math_command_scheme*` still use the
3443/// row-blind `peel_func_chain` (they never harvest labels).
3444fn peel_func_chain_rows(ty: MonoType) -> (Vec<(Row, MonoType)>, MonoType) {
3445 let mut slots = Vec::new();
3446 let mut cur = ty;
3447 loop {
3448 // Owned, as in `peel_func_chain`.
3449 match resolve(&cur).into_owned() {
3450 MonoType::Func(row, dom, cod) => {
3451 slots.push((*row, *dom));
3452 cur = *cod;
3453 }
3454 other => return (slots, other),
3455 }
3456 }
3457}
3458
3459/// Turn one V0_1 command parameter's [`Row`] (the row `Ast::LambdaOpt`'s
3460/// inference leaves on the `Func` arrow whose *domain* is that parameter —
3461/// see [`peel_func_chain_rows`]) into a closed-label-map [`CmdArgType`]: walk
3462/// the (resolved) row's `Cons` chain into a `Vec<(String, MonoType)>`, sorted
3463/// by label so `unify_cmd_args`'s equal-domain zip is order-insensitive.
3464/// A leftover `Row::Var` (an
3465/// under-constrained/free row — the ordinary case for a slot with no `?(…)`
3466/// bundle at all) defaults to no labels, same as `Row::Empty`. Shared by
3467/// `Checker::command_scheme`'s V0_1 branch and
3468/// `Checker::math_command_scheme_v01` so both harvest
3469/// identically.
3470fn harvest_slot(row: Row, dom: MonoType) -> CmdArgType {
3471 let mut opt_labels: Vec<(String, MonoType)> = Vec::new();
3472 let mut cur = resolve_row(&row).into_owned();
3473 loop {
3474 match cur {
3475 Row::Empty => break,
3476 Row::Var(_) => break,
3477 Row::Cons(label, lty, rest) => {
3478 opt_labels.push((label, *lty));
3479 cur = resolve_row(&rest).into_owned();
3480 }
3481 }
3482 }
3483 opt_labels.sort_by(|a, b| a.0.cmp(&b.0));
3484 labeled(opt_labels, dom)
3485}
3486
3487/// A best-effort span for an `Ast` node: only `Var`/`Overwrite`/
3488/// `AccessField` carry one directly (see `ast.rs`'s module doc comment);
3489/// everything else falls back to `None`; the resulting `TypeError` then just
3490/// prints without a location prefix.
3491pub(crate) fn ast_span<'s>(ast: &Ast<'s>) -> Option<Span> {
3492 match ast {
3493 Ast::Var(_, span) => Some(*span),
3494 Ast::Overwrite(_, span, _) => Some(*span),
3495 Ast::AccessField(_, _, span) => Some(*span),
3496 Ast::VersionScope(_, inner) => ast_span(inner),
3497 Ast::ModuleScope(_, inner) => ast_span(inner),
3498 _ => None,
3499 }
3500}
3501
3502/// Type-check a whole elaborated [`Program`], additionally returning every
3503/// non-fatal [`MatchWarning`] the exhaustiveness/redundancy pass collected
3504/// — v0.0.6's `exhchecker.ml` warns
3505/// on a non-exhaustive or redundant `match` rather than rejecting the
3506/// program, so these never turn a would-have-passed program into a
3507/// `TypeError`.
3508pub fn typecheck_verbose<'s>(program: &Program<'s>) -> Result<Vec<MatchWarning>, TypeError> {
3509 typecheck_verbose_with_version(program, RustyfiVersion::V0_0)
3510}
3511
3512/// Same as [`typecheck_verbose`], for a given target `version` — threads
3513/// through to `Checker::new_with_version`/`base_type_env_with_version` so a
3514/// `V0_1` program's `page-break` resolves against the `length * length`
3515/// tuple type (`prim_types::t_page_or_geometry`) and never sees `page`'s
3516/// `VariantDecl` (gated out of `builtin_variants_with_version(V0_1)`) — a
3517/// `V0_1` program that writes `A4Paper` gets the SAME "unbound constructor"
3518/// error upstream's own 0.1 compiler would give it, which is the faithful
3519/// behavior (the ADT is genuinely gone, not merely discouraged).
3520pub fn typecheck_verbose_with_version<'s>(
3521 program: &Program<'s>,
3522 version: RustyfiVersion,
3523) -> Result<Vec<MatchWarning>, TypeError> {
3524 let mut checker = Checker::new_with_version(program, version)?;
3525 let env = base_type_env_with_version(checker.store, version);
3526 checker.infer(&env, &program.body)?;
3527 Ok(checker.warnings)
3528}
3529
3530/// Type-check a whole elaborated [`Program`]. Validation only: on success the
3531/// caller proceeds to evaluate `program.body`; the evaluator is untouched by
3532/// this phase. A thin wrapper over [`typecheck_verbose`] that discards its
3533/// warnings.
3534pub fn typecheck<'s>(program: &Program<'s>) -> Result<(), TypeError> {
3535 typecheck_with_version(program, RustyfiVersion::V0_0)
3536}
3537
3538/// Same as [`typecheck`], for a given target `version`. See
3539/// `typecheck_verbose_with_version`'s doc comment.
3540pub fn typecheck_with_version<'s>(
3541 program: &Program<'s>,
3542 version: RustyfiVersion,
3543) -> Result<(), TypeError> {
3544 typecheck_verbose_with_version(program, version).map(|_warnings| ())
3545}
3546
3547// ============================================================================
3548// Per-binding ≡ whole-program equivalence, and session-incrementality.
3549// A `#[cfg(test)]` unit module since `BindingView`/`Checker`/`infer_binding`
3550// etc. are `pub(crate)` — an integration test can't reach them.
3551// ============================================================================
3552#[cfg(test)]
3553mod l3_per_binding_tests {
3554 use super::*;
3555 use crate::{elaborate, primitives};
3556
3557 fn elaborate_src<'s>(store: &'s SymbolStore, src: &str) -> Program<'s> {
3558 let file = rustyfi_syntax::parse_file(src).expect("parse failed");
3559 let env = primitives::base_env();
3560 let scope = elaborate::Scope::new(store, env.names());
3561 elaborate::elaborate_program(&file, &scope).expect("elaborate failed")
3562 }
3563
3564 /// Manually drive the checker per binding: walk `program.body`'s Let
3565 /// chain constructing `BindingView`s by hand, `infer_binding` +
3566 /// `with_all` at each step, `infer_expr` on the non-Let tail. This is
3567 /// exactly what `infer`'s own recursion does internally — driven here
3568 /// from outside the engine through the `pub(crate)` per-binding API, the
3569 /// same way `v1/module_check.rs` does.
3570 fn drive_manually<'s>(
3571 program: &Program<'s>,
3572 version: RustyfiVersion,
3573 ) -> Result<Vec<MatchWarning>, TypeError> {
3574 let mut checker = Checker::new_with_version(program, version)?;
3575 let mut env = base_type_env_with_version(program.store, version);
3576 let mut ast: &Ast<'s> = &program.body;
3577 loop {
3578 ast = match ast {
3579 Ast::LetIn(name, value, body) => {
3580 let schemes =
3581 checker.infer_binding(&env, BindingView::Let { name: *name, value })?;
3582 env = env.with_all(schemes, checker.binding_stage(value));
3583 body
3584 }
3585 Ast::LetMathIn(name, value, body) => {
3586 let schemes =
3587 checker.infer_binding(&env, BindingView::LetMath { name: *name, value })?;
3588 env = env.with_all(schemes, checker.binding_stage(value));
3589 body
3590 }
3591 Ast::LetRecIn(bindings, body) => {
3592 let schemes = checker.infer_binding(&env, BindingView::LetRec(bindings))?;
3593 env = env.with_all(schemes, checker.binding_stage_rec(bindings));
3594 body
3595 }
3596 Ast::LetMutableIn(name, init, body) => {
3597 let schemes = checker
3598 .infer_binding(&env, BindingView::LetMutable { name: *name, init })?;
3599 env = env.with_all(schemes, checker.binding_stage(init));
3600 body
3601 }
3602 other => {
3603 checker.infer_expr(&env, other)?;
3604 break;
3605 }
3606 };
3607 }
3608 Ok(checker.take_warnings())
3609 }
3610
3611 /// Elaborate `src` once, then compare `typecheck_verbose_with_version`
3612 /// against the manual per-binding drive: identical verdict, identical
3613 /// `TypeError` `Display` on error, identical `MatchWarning` list (incl.
3614 /// order — `MatchWarning` derives `PartialEq`) on success.
3615 fn assert_equivalent(src: &str) {
3616 let version = RustyfiVersion::V0_0;
3617 let store = SymbolStore::new();
3618 let program = elaborate_src(&store, src);
3619 let whole = typecheck_verbose_with_version(&program, version);
3620 let manual = drive_manually(&program, version);
3621 match (whole, manual) {
3622 (Ok(w1), Ok(w2)) => {
3623 assert_eq!(w1, w2, "warnings differ for {src:?}");
3624 }
3625 (Err(e1), Err(e2)) => {
3626 assert_eq!(
3627 format!("{e1}"),
3628 format!("{e2}"),
3629 "error strings differ for {src:?}"
3630 );
3631 }
3632 (Ok(w), Err(e)) => panic!(
3633 "{src:?}: whole-program accepted (warnings={w:?}), manual drive rejected: {e}"
3634 ),
3635 (Err(e), Ok(w)) => panic!(
3636 "{src:?}: whole-program rejected ({e}), manual drive accepted (warnings={w:?})"
3637 ),
3638 }
3639 }
3640
3641 #[test]
3642 fn per_binding_drive_matches_whole_program_across_binding_kinds() {
3643 let cases: &[&str] = &[
3644 // ---- plain `let` ----
3645 "let x = 1 in x + 1",
3646 "let x = 1 in x + true", // failing: type mismatch
3647 // ---- polymorphic `let` ----
3648 "let id = fun x -> x in (id 1, id true)",
3649 // ---- `let-inline` command binding (+ its application) ----
3650 "let-inline ctx \\emph it = read-inline ctx it
3651 in
3652 { \\emph{ ok } }",
3653 "let-inline ctx \\bad = ctx + 1
3654 in
3655 ()", // failing: not context-headed
3656 // ---- `let-block` command binding (+ its application) ----
3657 "let-block ctx +p it = line-break true true ctx (read-inline ctx it)
3658 in
3659 '< +p{ ok } >",
3660 "let-block ctx +duo a b = read-block ctx a
3661 in
3662 '< +duo{x} >", // failing: wrong arity
3663 // ---- `let-math` command binding ----
3664 "let-math \\g m = ${#m#m} in 0",
3665 "let-math \\f = 3 in 0", // failing: value isn't `math`
3666 // ---- `let-rec` group ----
3667 "let-rec is-even n = if n == 0 then true else is-odd (n - 1)
3668 and is-odd n = if n == 0 then false else is-even (n - 1)
3669 in
3670 is-even 4",
3671 "let-rec f n = if n == 0 then 0 else (f true)
3672 in
3673 f 1", // failing: recursive use at a mismatched type
3674 // ---- `let-mutable` (value restriction) ----
3675 "let-mutable x <- 0
3676 in
3677 (x <- 5)",
3678 "let-mutable r <- []
3679 in
3680 ((r <- (1 :: !r)) before (r <- (true :: !r)))", // failing: value restriction
3681 // ---- a `match` to also exercise warning accumulation ----
3682 "match Some 1 with
3683 | Some n -> n
3684 | None -> 0",
3685 ];
3686 for src in cases {
3687 assert_equivalent(src);
3688 }
3689 }
3690
3691 /// Session-incrementality. `declare_variant` after
3692 /// `infer_binding` affects only *later* checking against the session —
3693 /// a ctor referenced before its `declare_variant` fails with the same
3694 /// "unknown constructor" error the whole-program path gives for a
3695 /// genuinely-undeclared one; after `declare_variant`, it typechecks.
3696 #[test]
3697 fn session_incrementality_declare_variant_affects_only_later_bindings() {
3698 let store = SymbolStore::new();
3699 let program = elaborate_src(&store, "type t = | A of int in 0");
3700 assert_eq!(program.type_decls.len(), 1);
3701 let decl = &program.type_decls[0];
3702
3703 let mut checker = Checker::empty(&store);
3704 checker.install_builtin_variants(RustyfiVersion::V0_0);
3705 let env = base_type_env_with_version(&store, RustyfiVersion::V0_0);
3706
3707 // Before `declare_variant`, `A` is unknown — same message shape a
3708 // genuinely-undeclared constructor gets.
3709 let a_payload = Ast::Ctor("A".to_string(), Some(Box::new(Ast::Int(1))));
3710 let before = checker
3711 .infer_binding(
3712 &env,
3713 BindingView::Let {
3714 name: store.intern("before"),
3715 value: &a_payload,
3716 },
3717 )
3718 .expect_err("`A` should be unknown before declare_variant");
3719 assert_eq!(format!("{before}"), "unknown constructor 'A'");
3720
3721 let nosuch_payload = Ast::Ctor("NoSuchCtor".to_string(), None);
3722 let genuinely_unknown = checker
3723 .infer_binding(
3724 &env,
3725 BindingView::Let {
3726 name: store.intern("n"),
3727 value: &nosuch_payload,
3728 },
3729 )
3730 .expect_err("a genuinely undeclared ctor should also fail");
3731 assert_eq!(
3732 format!("{genuinely_unknown}"),
3733 "unknown constructor 'NoSuchCtor'"
3734 );
3735
3736 // After `declare_variant`, `A` becomes visible and typechecks —
3737 // this later binding sees it; the earlier `before` call above is
3738 // unaffected (it already returned its error).
3739 checker
3740 .declare_variant(decl)
3741 .expect("declare_variant should succeed");
3742 let after = checker.infer_binding(
3743 &env,
3744 BindingView::Let {
3745 name: store.intern("after"),
3746 value: &a_payload,
3747 },
3748 );
3749 assert!(
3750 after.is_ok(),
3751 "A(1) should typecheck after declare_variant: {after:?}"
3752 );
3753 }
3754}
3755
3756// ============================================================================
3757// The shadowing-fix follow-up to the version-scope env swap above:
3758// `version_scoped_type_env`'s `Ast::VersionScope` overwrite must not
3759// re-stomp a `PRIMITIVE_NAMES` entry the user already shadowed BEFORE the
3760// `VersionScope` is reached. A `#[cfg(test)]` unit module (mirroring
3761// `l3_per_binding_tests` above) since `Checker`/`TypeEnv`/`Ast` are all
3762// `pub(crate)` or crate-private-shaped enough that a hand-built synthetic
3763// `Ast` fixture (no parser/elaborator round-trip needed to pin this one
3764// shape) is the most direct way to exercise the exact env-swap path.
3765// ============================================================================
3766#[cfg(test)]
3767mod x2b_shadow_tests {
3768 use super::*;
3769
3770 /// `let page-break = 42 in <VersionScope V0_0> page-break` — infer
3771 /// the whole tree under a `V0_1`-ambient `Checker`. `page-break` is a
3772 /// `PRIMITIVE_NAMES` member (a version-forked one, no less: its `V0_0`
3773 /// scheme takes the `page` ADT, its `V0_1` scheme a `length * length`
3774 /// tuple — see `page_prims.rs`). An overwrite loop that replaced every
3775 /// `PRIMITIVE_NAMES` entry unconditionally on entering the
3776 /// `VersionScope` would re-stomp the user's `page-break = 42` with
3777 /// `V0_0`'s builtin `page-break` (a curried function type), inferring the
3778 /// inner `Var` as a function, not `int`. Instead
3779 /// `version_scoped_type_env` sees `page-break` recorded in
3780 /// `env.shadowed` (set by the `LetIn` arm's `env.with_all`, which goes
3781 /// through `TypeEnv::with`) and skips the overwrite for that one name, so
3782 /// the `Var` resolves through the untouched user binding and the whole
3783 /// expression types as `int`.
3784 #[test]
3785 fn version_scope_does_not_clobber_a_user_shadowed_primitive() {
3786 assert!(
3787 PRIMITIVE_NAMES.contains(&"page-break"),
3788 "fixture assumption: page-break must be a PRIMITIVE_NAMES member"
3789 );
3790
3791 let span = Span::default();
3792 let store = SymbolStore::new();
3793 let page_break = store.intern("page-break");
3794 let ast = Ast::LetIn(
3795 page_break,
3796 Box::new(Ast::Int(42)),
3797 Box::new(Ast::VersionScope(
3798 RustyfiVersion::V0_0,
3799 Box::new(Ast::Var(page_break, span)),
3800 )),
3801 );
3802 let program = Program {
3803 type_decls: Vec::new(),
3804 synonym_decls: Vec::new(),
3805 body: ast,
3806 store: &store,
3807 };
3808
3809 let mut checker = Checker::new_with_version(&program, RustyfiVersion::V0_1)
3810 .expect("checker construction over an empty-decls program should succeed");
3811 let env = base_type_env_with_version(&store, RustyfiVersion::V0_1);
3812 let ty = checker.infer(&env, &program.body).unwrap_or_else(|e| {
3813 panic!(
3814 "inferring the version-scoped `Var` over the user's shadowed \
3815 `page-break = 42` binding should type-check as `int`, not error: {e}"
3816 )
3817 });
3818 assert!(
3819 matches!(ty, MonoType::Base(BaseType::Int)),
3820 "the VersionScope env swap must respect the user's `page-break` shadow \
3821 (expected MonoType::Base(BaseType::Int), got {ty:?} instead) — a \
3822 MonoType::Func here would mean version_scoped_type_env re-stomped the \
3823 user binding with V0_0's builtin page-break scheme"
3824 );
3825 }
3826
3827 /// Companion positive control (same shape as the test above, MINUS the
3828 /// enclosing user shadow): a version-forked primitive referenced inside
3829 /// a `VersionScope` still resolves to `version`'s own (function-typed)
3830 /// scheme. Contrasts directly with
3831 /// `version_scope_does_not_clobber_a_user_shadowed_primitive`'s `int`
3832 /// result: same `page-break` name, same `VersionScope(V0_0, _)`, the only
3833 /// difference being the absence of a prior `let page-break = …` shadow.
3834 #[test]
3835 fn version_scope_still_resolves_unshadowed_forked_primitive() {
3836 let span = Span::default();
3837 let store = SymbolStore::new();
3838 let ast = Ast::VersionScope(
3839 RustyfiVersion::V0_0,
3840 Box::new(Ast::Var(store.intern("page-break"), span)),
3841 );
3842 let program = Program {
3843 type_decls: Vec::new(),
3844 synonym_decls: Vec::new(),
3845 body: ast,
3846 store: &store,
3847 };
3848 let mut checker = Checker::new_with_version(&program, RustyfiVersion::V0_1)
3849 .expect("checker construction over an empty-decls program should succeed");
3850 let env = base_type_env_with_version(&store, RustyfiVersion::V0_1);
3851 let ty = checker.infer(&env, &program.body).unwrap_or_else(|e| {
3852 panic!(
3853 "an unshadowed page-break reference inside a VersionScope should still \
3854 type-check (X2a's original capability, unaffected by X2b): {e}"
3855 )
3856 });
3857 assert!(
3858 matches!(ty, MonoType::Func(..)),
3859 "page-break (unshadowed) inside a VersionScope should still resolve to its \
3860 builtin (function-typed) scheme, got {ty:?} instead — the X2b shadow guard \
3861 must not have blocked this NON-shadowed overwrite"
3862 );
3863 }
3864}
3865
3866// ============================================================================
3867// Acceptance test against the real `stdja.satyh` `sig … end` block (
3868// command values are covered by `crates/rustyfi-lang/tests/typecheck.rs`'s
3869// end-to-end fixtures; this module covers the `SigItem`/`constraint` lowering
3870// directly, since `lower_sig_item` is a crate-private entry point no
3871// sig-enforcement pass calls yet).
3872// ============================================================================
3873#[cfg(test)]
3874mod sig_constraint_tests {
3875 use super::*;
3876 use rustyfi_syntax::cst::{SigAnnot, TopBinding};
3877
3878 fn parse_module_sig(src: &str) -> SigAnnot {
3879 let file = rustyfi_syntax::parse_file(src).expect("parse failed");
3880 for b in &file.prelude {
3881 if let TopBinding::Module { sig: Some(sig), .. } = b {
3882 return sig.clone();
3883 }
3884 }
3885 panic!("no `module .. : sig .. end` found in {src:?}");
3886 }
3887
3888 #[test]
3889 fn constraint_suffix_lowers_to_a_kind_record_bound_on_its_tyvar() {
3890 let sig = parse_module_sig(
3891 "module M : sig\n\
3892 val document : 'a -> config ?-> block-text -> document\n\
3893 constraint 'a :: (| title : inline-text; author : inline-text |)\n\
3894 end = struct\n\
3895 let document x c bt = bt\n\
3896 end",
3897 );
3898 let mut ctx = TypeContext::new();
3899 let mut saw_record_kind = false;
3900 for item in &sig.items {
3901 let (name, ty) =
3902 lower_sig_item(item, &mut ctx, RustyfiVersion::V0_0).expect("a value item");
3903 assert_eq!(name, "document");
3904 // Walk the lowered `Func` chain: `'a`'s fresh variable is the
3905 // very first domain (`Func(Var('a), Func(option(config),
3906 // Func(block-text, document)))` — see `lower_type_expr`'s doc
3907 // comment for the `?->` shape).
3908 if let MonoType::Func(_row, dom, _) = &ty {
3909 if let MonoType::Var(v) = &**dom {
3910 if let Kind::Record(labels) = v.kind() {
3911 saw_record_kind = true;
3912 let expected: BTreeSet<String> =
3913 ["title", "author"].iter().map(|s| s.to_string()).collect();
3914 assert_eq!(labels, expected);
3915 }
3916 }
3917 }
3918 }
3919 assert!(
3920 saw_record_kind,
3921 "expected 'a's fresh variable to carry a Kind::Record bound"
3922 );
3923 }
3924
3925 #[test]
3926 fn kind_record_bound_accepts_a_row_with_every_required_label() {
3927 // Direct demonstration that the constraint's lowered `Kind::Record`
3928 // bound rides on *existing* `unify`/`bind_var` machinery for free —
3929 // no sig-enforcement pass exists yet to drive this against a real
3930 // `struct` implementation, but
3931 // the positive-presence check itself already works once something
3932 // does.
3933 let mut ctx = TypeContext::new();
3934 let labels: BTreeSet<String> = ["title", "author"].iter().map(|s| s.to_string()).collect();
3935 let v = ctx.fresh_var_with_kind(Kind::Record(labels));
3936 let constrained = MonoType::Var(v);
3937 let full = MonoType::Record(Row::Cons(
3938 "title".to_string(),
3939 Box::new(t_inline_text()),
3940 Box::new(Row::Cons(
3941 "author".to_string(),
3942 Box::new(t_inline_text()),
3943 Box::new(Row::Empty),
3944 )),
3945 ));
3946 unify(&constrained, &full).expect("row has both required labels");
3947 }
3948
3949 #[test]
3950 fn kind_record_bound_rejects_a_row_missing_a_required_label() {
3951 let mut ctx = TypeContext::new();
3952 let labels: BTreeSet<String> = ["title", "author"].iter().map(|s| s.to_string()).collect();
3953 let v = ctx.fresh_var_with_kind(Kind::Record(labels));
3954 let constrained = MonoType::Var(v);
3955 let missing_author = MonoType::Record(Row::Cons(
3956 "title".to_string(),
3957 Box::new(t_inline_text()),
3958 Box::new(Row::Empty),
3959 ));
3960 let err = unify(&constrained, &missing_author)
3961 .expect_err("row is missing the required 'author' label");
3962 assert!(
3963 format!("{err:?}").contains("author"),
3964 "error should name the missing label: {err:?}"
3965 );
3966 }
3967
3968 #[test]
3969 fn real_stdja_sig_block_lowers_every_item_to_a_monotype() {
3970 // Mirrors the whole `sig … end` block of the real upstream
3971 // `stdja.satyh:24-51` (v0.0.6 checkout) — command values, command
3972 // types, `?->`, and the `constraint` suffix all together. Every item
3973 // parses and lowers without error (an empty `struct end` body is
3974 // enough — sig enforcement against a real implementation is not
3975 // this test's job).
3976 let sig = parse_module_sig(
3977 "module StdJa : sig\n\
3978 val default-config : config\n\
3979 val document : 'a -> config ?-> block-text -> document\n\
3980 constraint 'a :: (|\n\
3981 title : inline-text;\n\
3982 author : inline-text;\n\
3983 show-toc : bool;\n\
3984 show-title : bool;\n\
3985 |)\n\
3986 val font-latin-roman : string * float * float\n\
3987 direct \\ref : [string] inline-cmd\n\
3988 direct \\ref-page : [string] inline-cmd\n\
3989 direct \\figure : [inline-text; block-text] inline-cmd\n\
3990 direct +p : [inline-text] block-cmd\n\
3991 direct +pn : [inline-text] block-cmd\n\
3992 direct +section : [string?; string?; inline-text; block-text] block-cmd\n\
3993 direct +subsection : [string?; string?; inline-text; block-text] block-cmd\n\
3994 direct \\emph : [inline-text] inline-cmd\n\
3995 end = struct\n\
3996 end",
3997 );
3998 let mut ctx = TypeContext::new();
3999 let mut names = Vec::new();
4000 for item in &sig.items {
4001 let (name, _ty) =
4002 lower_sig_item(item, &mut ctx, RustyfiVersion::V0_0).expect("a value item");
4003 names.push(name);
4004 }
4005 assert_eq!(
4006 names,
4007 vec![
4008 "default-config",
4009 "document",
4010 "font-latin-roman",
4011 "\\ref",
4012 "\\ref-page",
4013 "\\figure",
4014 "+p",
4015 "+pn",
4016 "+section",
4017 "+subsection",
4018 "\\emph",
4019 ]
4020 );
4021 }
4022}