beck_core/
ty.rs

1//! Types, effect rows, unification, and the tier lattice.
2//!
3//! [`docs/03-type-and-effect-system.md`](../../../../../docs/03-type-and-effect-system.md) §3.1:
4//! "Hindley–Milner inference with bidirectional checking. Full inference inside bodies; mandatory
5//! annotations on public signatures."
6//!
7//! Phase 2's change is §3.2: **every function type carries an effect row**, and the row is
8//! inferred. `Ty::Fun` therefore has three components, not two, and `Subst` unifies rows alongside
9//! types. The rows themselves live in [`crate::row`]; this module is where they meet the type
10//! system.
11//!
12//! One deliberate omission remains, and it is named rather than implied: **row polymorphism on
13//! records** (§3.1). Models are nominal. Effect rows are polymorphic; record rows are not.
14
15use std::cell::RefCell;
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt;
18use std::rc::Rc;
19use std::sync::Arc;
20
21pub use crate::row::{Ambient, Effect, Row, RowVarId};
22
23/// Where code runs. §3.3's table of what each tier can discharge.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum Tier {
26    /// Pure and therefore *unplaced*: legal on every tier, compiled to each tier that needs it.
27    /// "That duplication is the payoff, not waste" (§3.3).
28    Any,
29    Client,
30    Server,
31    /// The fold/view engine — pure computation over streams and signals, plus `durable`.
32    Data,
33}
34
35/// The tiers a program can actually be placed on, in the order `beck explain place` reports them.
36pub const CONCRETE_TIERS: [Tier; 3] = [Tier::Client, Tier::Server, Tier::Data];
37
38impl Tier {
39    pub fn name(self) -> &'static str {
40        match self {
41            Tier::Any => "any",
42            Tier::Client => "client",
43            Tier::Server => "server",
44            Tier::Data => "data",
45        }
46    }
47
48    pub fn parse(s: &str) -> Option<Tier> {
49        Some(match s {
50            "any" => Tier::Any,
51            "client" => Tier::Client,
52            "server" => Tier::Server,
53            "data" => Tier::Data,
54            _ => return None,
55        })
56    }
57
58    /// Can this tier discharge that effect? §3.3's table.
59    ///
60    /// `Tier::Any` is the **intersection**: unplaced means legal everywhere, so `any` discharges
61    /// exactly what all three concrete tiers discharge. That is why an ambient effect never forces
62    /// a placement and `durable` always does, without either being a special case.
63    pub fn discharges(self, e: &Effect) -> bool {
64        match self {
65            Tier::Any => CONCRETE_TIERS.iter().all(|t| t.discharges(e)),
66            Tier::Client => match e {
67                Effect::Dom
68                | Effect::Nondet
69                | Effect::Partial
70                | Effect::Raises(_)
71                | Effect::Ambient(_) => true,
72                // §3.3: `net.out(own-origin)` — and only that. A browser cannot reach an arbitrary
73                // host, so a `net.out(payments.example.com)` on the client is a placement error and
74                // not a CORS bug discovered in production.
75                Effect::NetOut(host) => host.as_ref() == "origin",
76                _ => false,
77            },
78            // "server: discharges ingress, durable, net.*, fs, env, spawn, cap.*; cannot: dom".
79            Tier::Server => !matches!(e, Effect::Dom),
80            // "data (the fold/view engine): pure computation over streams/signals; durable.
81            //  cannot: dom, net, ambient time/rand".
82            //
83            // `partial` is discharged: a fold that diverges aborts the process, which is the same
84            // failure mode as any other failed append (§18.5 item 6) and does not make replay a
85            // different function of the log. `nondet` is not, and that is §3.7's rule.
86            // `raises` is discharged everywhere, including here: failing is *control flow* and not
87            // a resource. A fold that raises is a fold that produced no state from that event,
88            // which is the same shape as `partial` and is a function of the log either way.
89            Tier::Data => matches!(
90                e,
91                Effect::Durable | Effect::Partial | Effect::Raises(_) | Effect::Ambient(_)
92            ),
93        }
94    }
95
96    /// Every tier that can discharge this whole row, in report order. An open row is treated as its
97    /// known atoms: a row variable stands for a caller's effects, which the caller must place.
98    pub fn candidates(row: &Row) -> Vec<Tier> {
99        CONCRETE_TIERS
100            .into_iter()
101            .filter(|t| row.atoms.iter().all(|e| t.discharges(e)))
102            .collect()
103    }
104}
105
106impl Effect {
107    /// Does this effect make a fold a different function of the log? §3.7's replay-purity rule,
108    /// stated as a property of the atom rather than as "the row is empty".
109    ///
110    /// `log` and `metrics` do not (they are write-only observations, §19.8), `partial` does not
111    /// (a fold that aborts produces no state, rather than a different one), and `raises` does not
112    /// (the same value raised for the same input, every replay). Everything else does.
113    pub fn breaks_replay(&self) -> bool {
114        !matches!(
115            self,
116            Effect::Ambient(_) | Effect::Partial | Effect::Raises(_)
117        )
118    }
119}
120
121pub type TyVarId = u32;
122
123/// A unification variable's binding, shared so that unifying in one place is visible everywhere.
124#[derive(Clone, Debug, Default)]
125pub struct Subst {
126    bindings: Rc<RefCell<BTreeMap<TyVarId, Ty>>>,
127    rows: Rc<RefCell<BTreeMap<RowVarId, Row>>>,
128    next: Rc<RefCell<TyVarId>>,
129    next_row: Rc<RefCell<RowVarId>>,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum Ty {
134    Var(TyVarId),
135    /// A named type constructor applied to arguments: `Int`, `list[T]`, `Map[K,V]`, `Signal[T]`,
136    /// `secret[T]`, and every user `model`/`union`/`newtype`.
137    Con(Arc<str>, Vec<Ty>),
138    /// A function, with the effect row it performs when applied. §3.2's `(A) -> B ! e`.
139    Fun(Vec<Ty>, Box<Ty>, Row),
140}
141
142impl Ty {
143    pub fn con(name: &str) -> Ty {
144        Ty::Con(Arc::from(name), Vec::new())
145    }
146
147    pub fn app(name: &str, args: Vec<Ty>) -> Ty {
148        Ty::Con(Arc::from(name), args)
149    }
150
151    /// A pure function — the common case, and the one worth being short.
152    pub fn fun(params: Vec<Ty>, ret: Ty) -> Ty {
153        Ty::Fun(params, Box::new(ret), Row::empty())
154    }
155
156    /// A function with an effect row.
157    pub fn fun_eff(params: Vec<Ty>, ret: Ty, row: Row) -> Ty {
158        Ty::Fun(params, Box::new(ret), row)
159    }
160
161    pub const INT: &'static str = "Int";
162    pub const STR: &'static str = "Str";
163    pub const BOOL: &'static str = "Bool";
164    pub const FLOAT: &'static str = "Float";
165    pub const UNIT: &'static str = "Unit";
166    pub const HTML: &'static str = "Html";
167    pub const ATTR: &'static str = "Attr";
168    pub const LIST: &'static str = "list";
169    pub const MAP: &'static str = "Map";
170    pub const OPTION: &'static str = "Option";
171    pub const RESULT: &'static str = "Result";
172    pub const STREAM: &'static str = "Stream";
173    pub const SIGNAL: &'static str = "Signal";
174    pub const ENVELOPE: &'static str = "Envelope";
175    /// §3.5: "`secret[T]` is not Sendable". The one type constructor whose whole purpose is to fail
176    /// a boundary check.
177    pub const SECRET: &'static str = "secret";
178    /// The other half of that story, and the quadrant `secret[T]` alone leaves empty: **may be
179    /// written to the log, may never cross a boundary**.
180    ///
181    /// An event-sourced system has to record what happened, including facts a client must never
182    /// see — why an account was suspended, which rule fired, what an upstream vendor called the
183    /// customer. `secret[T]` cannot express that, because a secret is *also* not storable (§3.7's
184    /// F5: tokens must never be persisted into an immutable log). Without `internal[T]` the choice
185    /// is between dropping the fact from the audit trail and trusting that no view ever renders it,
186    /// and "trusting" is the word this language exists to delete.
187    pub const INTERNAL: &'static str = "internal";
188
189    pub fn int() -> Ty {
190        Ty::con(Ty::INT)
191    }
192    pub fn str_() -> Ty {
193        Ty::con(Ty::STR)
194    }
195    pub fn bool_() -> Ty {
196        Ty::con(Ty::BOOL)
197    }
198    pub fn unit() -> Ty {
199        Ty::con(Ty::UNIT)
200    }
201    pub fn html() -> Ty {
202        Ty::con(Ty::HTML)
203    }
204    pub fn list(t: Ty) -> Ty {
205        Ty::app(Ty::LIST, vec![t])
206    }
207    pub fn map(k: Ty, v: Ty) -> Ty {
208        Ty::app(Ty::MAP, vec![k, v])
209    }
210    pub fn option(t: Ty) -> Ty {
211        Ty::app(Ty::OPTION, vec![t])
212    }
213    pub fn signal(t: Ty) -> Ty {
214        Ty::app(Ty::SIGNAL, vec![t])
215    }
216    pub fn stream(t: Ty) -> Ty {
217        Ty::app(Ty::STREAM, vec![t])
218    }
219    pub fn secret(t: Ty) -> Ty {
220        Ty::app(Ty::SECRET, vec![t])
221    }
222    pub fn internal(t: Ty) -> Ty {
223        Ty::app(Ty::INTERNAL, vec![t])
224    }
225
226    pub fn con_name(&self) -> Option<&str> {
227        match self {
228            Ty::Con(n, _) => Some(n),
229            _ => None,
230        }
231    }
232
233    fn occurs(&self, v: TyVarId, s: &Subst) -> bool {
234        match s.resolve_shallow(self) {
235            Ty::Var(u) => u == v,
236            Ty::Con(_, args) => args.iter().any(|a| a.occurs(v, s)),
237            Ty::Fun(ps, r, _) => ps.iter().any(|a| a.occurs(v, s)) || r.occurs(v, s),
238        }
239    }
240}
241
242/// A type scheme: `forall vars rows. ty`. Let-polymorphism over *both* dimensions, which is what
243/// §3.2 means by "effect polymorphism is what keeps one standard library".
244#[derive(Clone, Debug)]
245pub struct Scheme {
246    pub vars: Vec<TyVarId>,
247    pub row_vars: Vec<RowVarId>,
248    /// The **named** type parameters of a user-written `def map[T, U](…)`.
249    ///
250    /// A prelude scheme quantifies over numbered variables because nobody reads its source; a
251    /// user's quantifies over names, because the name is what the programmer wrote, what the body
252    /// is checked against, what a diagnostic has to print, and what `beck iface` publishes. Inside
253    /// the body each of these is a *rigid* `Ty::Con(name, [])` — an opaque type that unifies with
254    /// itself and nothing else, which is exactly the property that makes the definition honest
255    /// about being polymorphic. [`Subst::instantiate`] turns them back into fresh variables at
256    /// every call site. `docs/32` §32.7.
257    pub params: Vec<Arc<str>>,
258    pub ty: Ty,
259}
260
261impl Scheme {
262    pub fn mono(ty: Ty) -> Scheme {
263        Scheme {
264            vars: Vec::new(),
265            row_vars: Vec::new(),
266            params: Vec::new(),
267            ty,
268        }
269    }
270
271    /// A scheme over named type parameters — what a `def` with a `[T, U]` list gets.
272    pub fn generic(params: Vec<Arc<str>>, ty: Ty) -> Scheme {
273        Scheme {
274            vars: Vec::new(),
275            row_vars: Vec::new(),
276            params,
277            ty,
278        }
279    }
280}
281
282impl Subst {
283    pub fn new() -> Subst {
284        Subst::default()
285    }
286
287    pub fn fresh(&self) -> Ty {
288        let mut n = self.next.borrow_mut();
289        let id = *n;
290        *n += 1;
291        Ty::Var(id)
292    }
293
294    pub fn fresh_row_var(&self) -> RowVarId {
295        let mut n = self.next_row.borrow_mut();
296        let id = *n;
297        *n += 1;
298        id
299    }
300
301    pub fn fresh_row(&self) -> Row {
302        Row::var(self.fresh_row_var())
303    }
304
305    fn resolve_shallow(&self, t: &Ty) -> Ty {
306        let mut cur = t.clone();
307        loop {
308            match cur {
309                Ty::Var(v) => match self.bindings.borrow().get(&v) {
310                    Some(next) => cur = next.clone(),
311                    None => return Ty::Var(v),
312                },
313                other => return other,
314            }
315        }
316    }
317
318    /// Fully apply the substitution — what diagnostics and the emitted `Core` see.
319    pub fn resolve(&self, t: &Ty) -> Ty {
320        match self.resolve_shallow(t) {
321            Ty::Var(v) => Ty::Var(v),
322            Ty::Con(n, args) => Ty::Con(n, args.iter().map(|a| self.resolve(a)).collect()),
323            Ty::Fun(ps, r, row) => Ty::Fun(
324                ps.iter().map(|p| self.resolve(p)).collect(),
325                Box::new(self.resolve(&r)),
326                self.resolve_row(&row),
327            ),
328        }
329    }
330
331    /// Expand a row through its variable bindings, to a fixed point.
332    ///
333    /// The `seen` set is not a safety net; it is the semantics. A definition's row variable can be
334    /// bound to a row that mentions itself — that is what mutual recursion between two effectful
335    /// functions *is* — and because a row is a union, stopping at an already-expanded variable
336    /// computes exactly the least fixed point rather than diverging.
337    pub fn resolve_row(&self, r: &Row) -> Row {
338        let mut out = Row {
339            atoms: r.atoms.clone(),
340            tails: BTreeSet::new(),
341        };
342        let mut seen: BTreeSet<RowVarId> = BTreeSet::new();
343        let mut work: Vec<RowVarId> = r.tails.iter().copied().collect();
344        while let Some(v) = work.pop() {
345            if !seen.insert(v) {
346                continue;
347            }
348            match self.rows.borrow().get(&v) {
349                Some(next) => {
350                    out.atoms.extend(next.atoms.iter().cloned());
351                    work.extend(next.tails.iter().copied());
352                }
353                None => {
354                    out.tails.insert(v);
355                }
356            }
357        }
358        out
359    }
360
361    pub fn bind_row(&self, v: RowVarId, r: Row) {
362        self.rows.borrow_mut().insert(v, r);
363    }
364
365    /// Instantiate a scheme with fresh type and row variables.
366    pub fn instantiate(&self, s: &Scheme) -> Ty {
367        self.instantiate_named(s).0
368    }
369
370    /// [`Subst::instantiate`], and the fresh variable each **named** parameter became.
371    ///
372    /// A bounded definition needs the map: `def sort[T: Ord](xs: list[T])` is lowered with a
373    /// dictionary parameter per method of `Ord`, and the call site can only say which impl to pass
374    /// once it knows what this call's `T` turned out to be. `docs/39` §39.4.
375    pub fn instantiate_named(&self, s: &Scheme) -> (Ty, BTreeMap<Arc<str>, Ty>) {
376        if s.vars.is_empty() && s.row_vars.is_empty() && s.params.is_empty() {
377            return (s.ty.clone(), BTreeMap::new());
378        }
379        let tys: BTreeMap<TyVarId, Ty> = s.vars.iter().map(|v| (*v, self.fresh())).collect();
380        let rows: BTreeMap<RowVarId, RowVarId> = s
381            .row_vars
382            .iter()
383            .map(|v| (*v, self.fresh_row_var()))
384            .collect();
385        let ty = subst_vars(&s.ty, &tys, &rows);
386        if s.params.is_empty() {
387            return (ty, BTreeMap::new());
388        }
389        // A fresh variable per named parameter, per use — which is what makes two calls of the same
390        // `map` at two element types two different types rather than one over-constrained one.
391        let named: BTreeMap<Arc<str>, Ty> =
392            s.params.iter().map(|p| (p.clone(), self.fresh())).collect();
393        (subst_named(&ty, &named), named)
394    }
395
396    /// Unify two types that are **alternatives** rather than actual-and-expected, and return the
397    /// type of whichever one runs.
398    ///
399    /// [`Subst::unify`] is asymmetric on purpose: its first argument is the actual type and its
400    /// second the expected one, and [`Subst::subsume_row`] leans on that so a function which does
401    /// less than its context allows is accepted. The two branches of an `if` are neither. Making one
402    /// of them the "expected" type of the other says that a branch returning `identity` — inferred
403    /// pure, so its row is closed — is the standard the other branch has to meet, and the other
404    /// branch returning a call's result carries a row *variable*. A variable is not a subset of the
405    /// empty row, so the two are reported as a conflict, with nothing missing to name:
406    ///
407    /// ```text
408    /// error[B0320]: the two branches may not perform {} here
409    /// ```
410    ///
411    /// which is `docs/25-benchmarks-and-expressiveness.md` §25.6 item 6, and what exercise 1.43
412    /// costs. The answer is the one every row-typed language reaches: the alternatives do not meet
413    /// each other, they both flow into a **fresh row**, and the result performs whatever either of
414    /// them might. Sound in the direction §3.2 requires — the join contains both branches' atoms, so
415    /// an effect can never be lost — and it leaves a free tail, exactly as a written function type
416    /// does, so a later context may widen it again.
417    pub fn unify_join(&self, a: &Ty, b: &Ty) -> Result<Ty, Mismatch> {
418        let (ra, rb) = (self.resolve_shallow(a), self.resolve_shallow(b));
419        match (&ra, &rb) {
420            // A variable on either side: nothing to join yet, and binding it is what `unify`
421            // already does correctly.
422            (Ty::Var(_), _) | (_, Ty::Var(_)) => {
423                self.unify(&ra, &rb)?;
424                Ok(self.resolve_shallow(&ra))
425            }
426            (Ty::Con(n1, a1), Ty::Con(n2, a2)) if n1 == n2 && a1.len() == a2.len() => {
427                let mut args = Vec::with_capacity(a1.len());
428                for (x, y) in a1.iter().zip(a2) {
429                    args.push(self.unify_join(x, y)?);
430                }
431                Ok(Ty::Con(n1.clone(), args))
432            }
433            (Ty::Fun(p1, r1, e1), Ty::Fun(p2, r2, e2)) => {
434                if p1.len() != p2.len() {
435                    return Err(Mismatch::Arity(p1.len(), p2.len()));
436                }
437                // Parameters are contravariant, so joining them would be unsound in the other
438                // direction. Two alternatives must accept the same arguments: ordinary unification.
439                for (x, y) in p1.iter().zip(p2) {
440                    self.unify(x, y)?;
441                }
442                let ret = self.unify_join(r1, r2)?;
443                let row = Row::var(self.fresh_row_var());
444                self.subsume_row(e1, &row)?;
445                self.subsume_row(e2, &row)?;
446                Ok(Ty::Fun(p1.clone(), Box::new(ret), row))
447            }
448            _ => {
449                self.unify(&ra, &rb)?;
450                Ok(self.resolve_shallow(&ra))
451            }
452        }
453    }
454
455    pub fn unify(&self, a: &Ty, b: &Ty) -> Result<(), Mismatch> {
456        let (ra, rb) = (self.resolve_shallow(a), self.resolve_shallow(b));
457        match (&ra, &rb) {
458            (Ty::Var(x), Ty::Var(y)) if x == y => Ok(()),
459            (Ty::Var(v), other) | (other, Ty::Var(v)) => {
460                if other.occurs(*v, self) {
461                    return Err(Mismatch::Infinite);
462                }
463                self.bindings.borrow_mut().insert(*v, other.clone());
464                Ok(())
465            }
466            (Ty::Con(n1, a1), Ty::Con(n2, a2)) => {
467                if n1 != n2 || a1.len() != a2.len() {
468                    return Err(Mismatch::different(self.resolve(&ra), self.resolve(&rb)));
469                }
470                for (x, y) in a1.iter().zip(a2) {
471                    self.unify(x, y)?;
472                }
473                Ok(())
474            }
475            (Ty::Fun(p1, r1, e1), Ty::Fun(p2, r2, e2)) => {
476                if p1.len() != p2.len() {
477                    return Err(Mismatch::Arity(p1.len(), p2.len()));
478                }
479                for (x, y) in p1.iter().zip(p2) {
480                    self.unify(x, y)?;
481                }
482                self.unify(r1, r2)?;
483                // Rows *subsume*, they do not equate: §3.1 permits "no subtyping beyond
484                // effect-row subsumption", and this is that one exception. The first argument is
485                // the actual type and the second the expected one throughout the checker, so a
486                // function that does less than its context allows is accepted — which is the whole
487                // reason a pure `lambda t: t.done` can be passed where `(a -> b ! e)` is wanted.
488                self.subsume_row(e1, e2)
489            }
490            _ => Err(Mismatch::different(self.resolve(&ra), self.resolve(&rb))),
491        }
492    }
493
494    /// Require `actual ⊆ expected` — the effect-row subsumption §3.1 permits and nothing else.
495    ///
496    /// Rows are sets, so this is: whatever the actual row does, the expected row must already
497    /// allow, or must have a variable free to absorb it. Concretely, three cases and no more:
498    ///
499    /// * everything the actual side does is already named on the expected side — nothing to do;
500    /// * the expected side has a free row variable — bind it to the difference, leaving a fresh
501    ///   variable behind so a *later* call site can widen it again. This is what makes
502    ///   `(a -> b ! e)` accept a pure function at one call and an effectful one at the next;
503    /// * the expected side is closed and lacks something — the rows genuinely differ, and that is
504    ///   the error.
505    ///
506    /// **The direction is the design.** Equality would make a pure lambda fail to match
507    /// `(a -> b ! e)` unless `e` were solved first, and would make the same higher-order function
508    /// unusable at two call sites with different arguments. Subsumption over-approximates in one
509    /// direction only: a definition's inferred row may be a superset of what one call actually
510    /// performs, which can cost a placement candidate but can never lose an effect.
511    pub fn subsume_row(&self, actual: &Row, expected: &Row) -> Result<(), Mismatch> {
512        let a = self.resolve_row(actual);
513        let e = self.resolve_row(expected);
514
515        let missing: BTreeSet<Effect> = a.atoms.difference(&e.atoms).cloned().collect();
516        let extra_tails: BTreeSet<RowVarId> = a.tails.difference(&e.tails).copied().collect();
517        if missing.is_empty() && extra_tails.is_empty() {
518            return Ok(());
519        }
520        // Deterministically the lowest-numbered free variable, so the same program always produces
521        // the same solution — §3.4's determinism guardrail starts here, not at the solver.
522        let Some(v) = e.tails.iter().next().copied() else {
523            // Naming what is missing is only possible when something *is*. When the actual side's
524            // extra is a row **variable** — an unknown row, from a call whose effects are not
525            // decided here — the honest report is that the expected side is closed, not a list of
526            // nothing. `Effects("")` used to render as "may not perform {}", which fails §4.5 on
527            // its own terms: no user can act on it (docs/25 §25.6 item 6).
528            if missing.is_empty() {
529                return Err(Mismatch::UnknownEffects);
530            }
531            return Err(Mismatch::Effects(
532                missing
533                    .iter()
534                    .map(|x| x.name())
535                    .collect::<Vec<_>>()
536                    .join(", "),
537            ));
538        };
539        let rest = self.fresh_row_var();
540        let mut tails = extra_tails;
541        tails.insert(rest);
542        self.bind_row(
543            v,
544            Row {
545                atoms: missing,
546                tails,
547            },
548        );
549        Ok(())
550    }
551
552    /// Two rows denote the same set of effects. Used where a signature is *compared* rather than
553    /// checked — `.becki` agreement and `--wire-compat`.
554    pub fn rows_equal(&self, a: &Row, b: &Row) -> bool {
555        self.resolve_row(a) == self.resolve_row(b)
556    }
557
558    /// The free variables of a resolved type.
559    pub fn free_vars(&self, t: &Ty, out: &mut Vec<TyVarId>) {
560        match self.resolve_shallow(t) {
561            Ty::Var(v) => {
562                if !out.contains(&v) {
563                    out.push(v);
564                }
565            }
566            Ty::Con(_, args) => {
567                for a in &args {
568                    self.free_vars(a, out);
569                }
570            }
571            Ty::Fun(ps, r, _) => {
572                for p in &ps {
573                    self.free_vars(p, out);
574                }
575                self.free_vars(&r, out);
576            }
577        }
578    }
579
580    /// The free *row* variables of a resolved type — what a definition generalises over.
581    pub fn free_row_vars(&self, t: &Ty, out: &mut Vec<RowVarId>) {
582        match self.resolve_shallow(t) {
583            Ty::Var(_) => {}
584            Ty::Con(_, args) => {
585                for a in &args {
586                    self.free_row_vars(a, out);
587                }
588            }
589            Ty::Fun(ps, r, row) => {
590                for p in &ps {
591                    self.free_row_vars(p, out);
592                }
593                self.free_row_vars(&r, out);
594                for v in self.resolve_row(&row).tails {
595                    if !out.contains(&v) {
596                        out.push(v);
597                    }
598                }
599            }
600        }
601    }
602}
603
604fn subst_vars(t: &Ty, m: &BTreeMap<TyVarId, Ty>, rows: &BTreeMap<RowVarId, RowVarId>) -> Ty {
605    match t {
606        Ty::Var(v) => m.get(v).cloned().unwrap_or(Ty::Var(*v)),
607        Ty::Con(n, args) => Ty::Con(
608            n.clone(),
609            args.iter().map(|a| subst_vars(a, m, rows)).collect(),
610        ),
611        Ty::Fun(ps, r, row) => Ty::Fun(
612            ps.iter().map(|p| subst_vars(p, m, rows)).collect(),
613            Box::new(subst_vars(r, m, rows)),
614            Row {
615                atoms: row.atoms.clone(),
616                tails: row
617                    .tails
618                    .iter()
619                    .map(|v| rows.get(v).copied().unwrap_or(*v))
620                    .collect(),
621            },
622        ),
623    }
624}
625
626/// Replace each rigid type parameter with whatever it was instantiated to.
627///
628/// A parameter is a nullary `Con`, so this is a leaf substitution: `list[T]` becomes `list[?7]` and
629/// a `T` that happens to be applied to arguments is left alone, because a type parameter cannot be
630/// a type *constructor* — §32.9 records that as a limit rather than working round it.
631fn subst_named(t: &Ty, m: &BTreeMap<Arc<str>, Ty>) -> Ty {
632    match t {
633        Ty::Var(v) => Ty::Var(*v),
634        Ty::Con(n, args) if args.is_empty() => m.get(n).cloned().unwrap_or_else(|| t.clone()),
635        Ty::Con(n, args) => Ty::Con(n.clone(), args.iter().map(|a| subst_named(a, m)).collect()),
636        Ty::Fun(ps, r, row) => Ty::Fun(
637            ps.iter().map(|p| subst_named(p, m)).collect(),
638            Box::new(subst_named(r, m)),
639            row.clone(),
640        ),
641    }
642}
643
644#[derive(Clone, Debug)]
645pub enum Mismatch {
646    /// Boxed because a `Ty` is a tree and this is the *error* path: making every successful
647    /// unification carry the size of a failed one is the wrong trade.
648    Different(Box<(Ty, Ty)>),
649    Arity(usize, usize),
650    Infinite,
651    /// Two function types agree on their arguments and result but not on what they do.
652    Effects(String),
653    /// The same, where what the actual side may do is not yet known — an unsolved row variable
654    /// against a context whose row is closed. Distinct from [`Mismatch::Effects`] because there is
655    /// no effect to name, and a message that names none is one no user can act on.
656    UnknownEffects,
657}
658
659impl Mismatch {
660    fn different(a: Ty, b: Ty) -> Mismatch {
661        Mismatch::Different(Box::new((a, b)))
662    }
663}
664
665impl fmt::Display for Ty {
666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667        match self {
668            Ty::Var(v) => write!(f, "?{v}"),
669            Ty::Con(n, args) if args.is_empty() => write!(f, "{n}"),
670            Ty::Con(n, args) => {
671                write!(f, "{n}[")?;
672                for (i, a) in args.iter().enumerate() {
673                    if i > 0 {
674                        write!(f, ", ")?;
675                    }
676                    write!(f, "{a}")?;
677                }
678                write!(f, "]")
679            }
680            Ty::Fun(ps, r, row) => {
681                write!(f, "(")?;
682                for (i, p) in ps.iter().enumerate() {
683                    if i > 0 {
684                        write!(f, ", ")?;
685                    }
686                    write!(f, "{p}")?;
687                }
688                write!(f, ") -> {r}")?;
689                // A pure function prints without a row: `! {}` on every signature would be noise,
690                // and §3.2 elides the ambient set for the same reason.
691                let visible: Vec<String> = row.visible().iter().map(|e| e.name()).collect();
692                if visible.is_empty() && row.tails.is_empty() {
693                    Ok(())
694                } else {
695                    let tails: Vec<String> = row.tails.iter().map(|v| format!("e{v}")).collect();
696                    let parts = if tails.is_empty() {
697                        visible.join(", ")
698                    } else if visible.is_empty() {
699                        tails.join(" | ")
700                    } else {
701                        format!("{} | {}", visible.join(", "), tails.join(" | "))
702                    };
703                    write!(f, " ! {{{parts}}}")
704                }
705            }
706        }
707    }
708}
709
710/// The base a declaration's type parameters are numbered from.
711///
712/// The *n*th parameter of a declaration is `Ty::Var(SCHEME_BASE + n)` wherever it appears in that
713/// declaration's field types, so instantiating `Tree[Str]` is an index rather than a search. The
714/// base is far above any unification variable the checker will mint, which is what lets one `Ty`
715/// carry both without a tag.
716pub const SCHEME_BASE: u32 = 1_000_000;
717
718/// A user-declared type: a `model` (record), a `union` (ADT), a `newtype`, or an alias.
719///
720/// Comparable because a `.becki` interface is compared (§3.6, §4.3): two builds agree on a module's
721/// contract exactly when their type declarations are equal.
722///
723/// `params` is the declaration's type-parameter *names*, in order — `union Tree[T]` has `["T"]`.
724/// The names are what a `.becki` renders and what a doc page shows; the field types refer to the
725/// parameters positionally through [`SCHEME_BASE`], so a rename is a rename and nothing more.
726/// Arity is `params.len()`, declared rather than inferred from use: a parameter no field mentions
727/// is still a parameter, and `Phantom[Int]` and `Phantom[Str]` are still different types.
728#[derive(Clone, Debug, PartialEq, Eq)]
729pub enum TyDecl {
730    Model {
731        name: Arc<str>,
732        params: Vec<Arc<str>>,
733        fields: Vec<(Arc<str>, Ty)>,
734    },
735    Union {
736        name: Arc<str>,
737        params: Vec<Arc<str>>,
738        variants: Vec<Variant>,
739    },
740    /// §3.1's "zero-cost nominal newtype": ids of different entities must not be interchangeable.
741    Newtype {
742        name: Arc<str>,
743        params: Vec<Arc<str>>,
744        inner: Ty,
745    },
746    Alias {
747        name: Arc<str>,
748        params: Vec<Arc<str>>,
749        ty: Ty,
750    },
751}
752
753#[derive(Clone, Debug, PartialEq, Eq)]
754pub struct Variant {
755    pub name: Arc<str>,
756    pub fields: Vec<(Arc<str>, Ty)>,
757}
758
759impl TyDecl {
760    pub fn name(&self) -> &Arc<str> {
761        match self {
762            TyDecl::Model { name, .. }
763            | TyDecl::Union { name, .. }
764            | TyDecl::Newtype { name, .. }
765            | TyDecl::Alias { name, .. } => name,
766        }
767    }
768
769    pub fn params(&self) -> &[Arc<str>] {
770        match self {
771            TyDecl::Model { params, .. }
772            | TyDecl::Union { params, .. }
773            | TyDecl::Newtype { params, .. }
774            | TyDecl::Alias { params, .. } => params,
775        }
776    }
777
778    /// How many type arguments a mention of this name must carry.
779    pub fn arity(&self) -> usize {
780        self.params().len()
781    }
782
783    /// `[T, U]`, or the empty string when there is nothing to quantify.
784    pub fn param_brackets(&self) -> String {
785        if self.params().is_empty() {
786            return String::new();
787        }
788        format!(
789            "[{}]",
790            self.params()
791                .iter()
792                .map(|p| p.to_string())
793                .collect::<Vec<_>>()
794                .join(", ")
795        )
796    }
797
798    /// One of this declaration's field types as the declaration wrote it: the positional
799    /// parameters put back under the names they were given.
800    ///
801    /// Everything downstream of a `TyDecl` holds its parameters positionally, which is what makes
802    /// instantiation an index. Rendering is the one place that has to undo it — a `.becki` is
803    /// *source*, and `value: ?1000000` is not something the parser can read back.
804    pub fn as_written(&self, t: &Ty) -> Ty {
805        if self.params().is_empty() {
806            return t.clone();
807        }
808        let args: Vec<Ty> = self.params().iter().map(|p| Ty::con(p)).collect();
809        instantiate_decl(t, &args)
810    }
811}
812
813/// A published `trait`: the signatures it requires, over an abstract `Self`.
814///
815/// The checker keeps a trait's methods as **syntax** while it is desugaring impls and bounds, because
816/// splicing is what that pass does. This is the other half: the same declaration as *types*, which
817/// is what a `.becki` compares, what `--wire-compat` classifies, and what an importing module reads.
818/// A trait that crossed a boundary as syntax would carry spans into a file that does not own them.
819#[derive(Clone, Debug, PartialEq, Eq)]
820pub struct TraitSig {
821    pub name: Arc<str>,
822    pub methods: Vec<MethodSig>,
823}
824
825#[derive(Clone, Debug, PartialEq, Eq)]
826pub struct MethodSig {
827    pub name: Arc<str>,
828    /// Parameter names and types, with the abstract receiver as `Ty::con("Self")`.
829    pub params: Vec<(Arc<str>, Ty)>,
830    pub ret: Ty,
831    /// The declared row — the bound every implementation is held to (`docs/37` §37.5), and
832    /// therefore what a caller in another module may assume.
833    pub effects: Vec<Effect>,
834}
835
836/// A published `impl Trait for Type`.
837///
838/// There is no body here and there never will be: an importing module needs to know *that* the
839/// implementation exists and what its signature is, and the implementation itself stays where it
840/// was written.
841#[derive(Clone, Debug, PartialEq, Eq)]
842pub struct ImplSig {
843    pub trait_name: Arc<str>,
844    /// The impl's own type parameters: `["T"]` for `impl[T] Priced for Bundle[T]`.
845    pub params: Vec<Arc<str>>,
846    /// The target, with those parameters as rigid names — `Bundle[T]`.
847    pub target: Ty,
848    /// What each method actually performs, by name, for the methods that perform anything.
849    ///
850    /// The trait declares an abstract signature; this impl's methods may be **more** effectful than
851    /// it (`docs/47`), so a caller in another module cannot take the row off the trait. It has to
852    /// be published with the impl, and this is where it crosses. Empty rows are omitted: most
853    /// impls are pure and a `.becki` full of `uses` clauses saying nothing is a `.becki` nobody
854    /// reviews.
855    pub effects: Vec<(Arc<str>, Vec<Effect>)>,
856}
857
858impl ImplSig {
859    /// The head constructor dispatch keys on.
860    pub fn head(&self) -> Arc<str> {
861        self.target
862            .con_name()
863            .map(Arc::from)
864            .unwrap_or_else(|| Arc::from("?"))
865    }
866}
867
868/// Replace the positional parameters of a declaration with `args`.
869///
870/// A field type of `Some(value: ?1000000)` under `Option[Int]` is `value: Int`, and every pass that
871/// reads a declaration's fields against a concrete type goes through here.
872pub fn instantiate_decl(t: &Ty, args: &[Ty]) -> Ty {
873    match t {
874        Ty::Var(v) if *v >= SCHEME_BASE => args
875            .get((*v - SCHEME_BASE) as usize)
876            .cloned()
877            .unwrap_or_else(|| t.clone()),
878        Ty::Var(_) => t.clone(),
879        Ty::Con(n, xs) => Ty::Con(
880            n.clone(),
881            xs.iter().map(|x| instantiate_decl(x, args)).collect(),
882        ),
883        Ty::Fun(ps, r, row) => Ty::Fun(
884            ps.iter().map(|x| instantiate_decl(x, args)).collect(),
885            Box::new(instantiate_decl(r, args)),
886            row.clone(),
887        ),
888    }
889}
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894
895    #[test]
896    fn unification_binds_and_propagates() {
897        let s = Subst::new();
898        let a = s.fresh();
899        assert!(s.unify(&a, &Ty::int()).is_ok());
900        assert_eq!(s.resolve(&a), Ty::int());
901        assert!(s.unify(&a, &Ty::str_()).is_err());
902    }
903
904    #[test]
905    fn structural_unification_descends() {
906        let s = Subst::new();
907        let a = s.fresh();
908        let b = s.fresh();
909        assert!(s
910            .unify(
911                &Ty::map(a.clone(), b.clone()),
912                &Ty::map(Ty::int(), Ty::str_())
913            )
914            .is_ok());
915        assert_eq!(s.resolve(&a), Ty::int());
916        assert_eq!(s.resolve(&b), Ty::str_());
917    }
918
919    #[test]
920    fn the_occurs_check_rejects_infinite_types() {
921        let s = Subst::new();
922        let a = s.fresh();
923        assert!(matches!(
924            s.unify(&a, &Ty::list(a.clone())),
925            Err(Mismatch::Infinite)
926        ));
927    }
928
929    #[test]
930    fn instantiation_is_fresh_per_use() {
931        let s = Subst::new();
932        let v = 0;
933        let scheme = Scheme {
934            params: Vec::new(),
935            vars: vec![v],
936            row_vars: Vec::new(),
937            ty: Ty::fun(vec![Ty::Var(v)], Ty::Var(v)),
938        };
939        let one = s.instantiate(&scheme);
940        let two = s.instantiate(&scheme);
941        if let Ty::Fun(ps, _, _) = &one {
942            assert!(s.unify(&ps[0], &Ty::int()).is_ok());
943        }
944        if let Ty::Fun(ps, _, _) = &two {
945            assert!(s.unify(&ps[0], &Ty::str_()).is_ok());
946        }
947    }
948
949    #[test]
950    fn the_tier_table_matches_the_design() {
951        // §3.3: client cannot discharge `ingress` or `durable`; server cannot discharge `dom`.
952        assert!(!Tier::Client.discharges(&Effect::Ingress));
953        assert!(!Tier::Client.discharges(&Effect::Durable));
954        assert!(Tier::Client.discharges(&Effect::Dom));
955        assert!(Tier::Server.discharges(&Effect::Ingress));
956        assert!(Tier::Server.discharges(&Effect::Durable));
957        assert!(!Tier::Server.discharges(&Effect::Dom));
958        assert!(Tier::Data.discharges(&Effect::Durable));
959        assert!(!Tier::Data.discharges(&Effect::Nondet));
960        // `any` means unplaced-pure: it discharges exactly the intersection, so anything a tier
961        // refuses forces a placement, and an ambient effect never does.
962        assert!(!Tier::Any.discharges(&Effect::Durable));
963        assert!(Tier::Any.discharges(&Effect::Ambient(Ambient::Log)));
964    }
965
966    #[test]
967    fn only_the_own_origin_is_reachable_from_a_browser() {
968        // §3.3's table says `net.out(own-origin)` and means it: a client that could name any host
969        // would be a placement decision made by CORS at runtime.
970        assert!(Tier::Client.discharges(&Effect::NetOut(Arc::from("origin"))));
971        assert!(!Tier::Client.discharges(&Effect::NetOut(Arc::from("payments.example.com"))));
972        assert!(Tier::Server.discharges(&Effect::NetOut(Arc::from("payments.example.com"))));
973    }
974
975    #[test]
976    fn a_closed_row_accepts_less_and_refuses_more() {
977        let s = Subst::new();
978        assert!(s
979            .subsume_row(&Row::of([Effect::Dom]), &Row::of([Effect::Dom]))
980            .is_ok());
981        // Doing less than the context allows is fine — that is subsumption.
982        assert!(s
983            .subsume_row(&Row::empty(), &Row::of([Effect::Dom]))
984            .is_ok());
985        // Doing something the context does not allow is the error.
986        assert!(s
987            .subsume_row(&Row::of([Effect::Dom]), &Row::of([Effect::Durable]))
988            .is_err());
989    }
990
991    #[test]
992    fn an_open_row_absorbs_what_is_passed_to_it_twice() {
993        // The everyday case: `map_list`'s `(a -> b ! e)` meets a lambda that touches the dom, and
994        // then — at another call site through the same monomorphic parameter — a pure one.
995        let s = Subst::new();
996        let e = s.fresh_row();
997        assert!(s.subsume_row(&Row::of([Effect::Dom]), &e).is_ok());
998        assert!(s.subsume_row(&Row::empty(), &e).is_ok());
999        assert!(s.subsume_row(&Row::of([Effect::Durable]), &e).is_ok());
1000        assert_eq!(
1001            s.resolve_row(&e).atoms,
1002            BTreeSet::from([Effect::Dom, Effect::Durable]),
1003            "a row variable widened at two call sites holds the union, never a contradiction"
1004        );
1005    }
1006
1007    #[test]
1008    fn effect_polymorphism_carries_a_callers_row_to_the_result() {
1009        // `map_list : (list[a], (a -> b ! e)) -> list[b] ! e`, applied to an effectful function,
1010        // must make the *application* effectful. That is the whole point of the row variable.
1011        let s = Subst::new();
1012        let e = s.fresh_row_var();
1013        let scheme = Scheme {
1014            params: Vec::new(),
1015            vars: vec![],
1016            row_vars: vec![e],
1017            ty: Ty::fun_eff(
1018                vec![Ty::fun_eff(vec![Ty::int()], Ty::int(), Row::var(e))],
1019                Ty::int(),
1020                Row::var(e),
1021            ),
1022        };
1023        let Ty::Fun(params, _, latent) = s.instantiate(&scheme) else {
1024            panic!("a function");
1025        };
1026        // Pass something that mints ids. The actual is the argument, the expected the parameter.
1027        assert!(s
1028            .unify(
1029                &Ty::fun_eff(vec![Ty::int()], Ty::int(), Row::of([Effect::Nondet])),
1030                &params[0],
1031            )
1032            .is_ok());
1033        // The trailing variable is deliberate: subsumption leaves room for a *later* call site to
1034        // widen the same row. What matters is that the atom arrived.
1035        assert_eq!(
1036            s.resolve_row(&latent).atoms,
1037            BTreeSet::from([Effect::Nondet])
1038        );
1039    }
1040
1041    #[test]
1042    fn a_recursive_row_resolves_to_its_least_fixed_point_rather_than_diverging() {
1043        // Two mutually recursive effectful functions: `r_f = {dom} ∪ r_g`, `r_g = {durable} ∪ r_f`.
1044        let s = Subst::new();
1045        let (f, g) = (s.fresh_row_var(), s.fresh_row_var());
1046        s.bind_row(f, Row::of([Effect::Dom]).union(&Row::var(g)));
1047        s.bind_row(g, Row::of([Effect::Durable]).union(&Row::var(f)));
1048        let resolved = s.resolve_row(&Row::var(f));
1049        assert_eq!(resolved, Row::of([Effect::Dom, Effect::Durable]));
1050        assert!(resolved.is_closed());
1051    }
1052
1053    #[test]
1054    fn a_pure_function_prints_without_a_row_and_an_effectful_one_with_it() {
1055        assert_eq!(
1056            Ty::fun(vec![Ty::int()], Ty::int()).to_string(),
1057            "(Int) -> Int"
1058        );
1059        assert_eq!(
1060            Ty::fun_eff(vec![], Ty::unit(), Row::of([Effect::Durable])).to_string(),
1061            "() -> Unit ! {durable}"
1062        );
1063        // Ambient effects are elided from the signature, exactly as §3.2 says.
1064        assert_eq!(
1065            Ty::fun_eff(
1066                vec![],
1067                Ty::unit(),
1068                Row::of([Effect::Ambient(Ambient::Log), Effect::Dom])
1069            )
1070            .to_string(),
1071            "() -> Unit ! {dom}"
1072        );
1073    }
1074}