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