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/27` §27.2.
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/27` §27.5.
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    /// The free variables of a resolved type.
553    pub fn free_vars(&self, t: &Ty, out: &mut Vec<TyVarId>) {
554        match self.resolve_shallow(t) {
555            Ty::Var(v) => {
556                if !out.contains(&v) {
557                    out.push(v);
558                }
559            }
560            Ty::Con(_, args) => {
561                for a in &args {
562                    self.free_vars(a, out);
563                }
564            }
565            Ty::Fun(ps, r, _) => {
566                for p in &ps {
567                    self.free_vars(p, out);
568                }
569                self.free_vars(&r, out);
570            }
571        }
572    }
573
574    /// The free *row* variables of a resolved type — what a definition generalises over.
575    pub fn free_row_vars(&self, t: &Ty, out: &mut Vec<RowVarId>) {
576        match self.resolve_shallow(t) {
577            Ty::Var(_) => {}
578            Ty::Con(_, args) => {
579                for a in &args {
580                    self.free_row_vars(a, out);
581                }
582            }
583            Ty::Fun(ps, r, row) => {
584                for p in &ps {
585                    self.free_row_vars(p, out);
586                }
587                self.free_row_vars(&r, out);
588                for v in self.resolve_row(&row).tails {
589                    if !out.contains(&v) {
590                        out.push(v);
591                    }
592                }
593            }
594        }
595    }
596}
597
598fn subst_vars(t: &Ty, m: &BTreeMap<TyVarId, Ty>, rows: &BTreeMap<RowVarId, RowVarId>) -> Ty {
599    match t {
600        Ty::Var(v) => m.get(v).cloned().unwrap_or(Ty::Var(*v)),
601        Ty::Con(n, args) => Ty::Con(
602            n.clone(),
603            args.iter().map(|a| subst_vars(a, m, rows)).collect(),
604        ),
605        Ty::Fun(ps, r, row) => Ty::Fun(
606            ps.iter().map(|p| subst_vars(p, m, rows)).collect(),
607            Box::new(subst_vars(r, m, rows)),
608            Row {
609                atoms: row.atoms.clone(),
610                tails: row
611                    .tails
612                    .iter()
613                    .map(|v| rows.get(v).copied().unwrap_or(*v))
614                    .collect(),
615            },
616        ),
617    }
618}
619
620/// Replace each rigid type parameter with whatever it was instantiated to.
621///
622/// A parameter is a nullary `Con`, so this is a leaf substitution: `list[T]` becomes `list[?7]` and
623/// a `T` that happens to be applied to arguments is left alone, because a type parameter cannot be
624/// a type *constructor* — §27.10 records that as a limit rather than working round it.
625fn subst_named(t: &Ty, m: &BTreeMap<Arc<str>, Ty>) -> Ty {
626    match t {
627        Ty::Var(v) => Ty::Var(*v),
628        Ty::Con(n, args) if args.is_empty() => m.get(n).cloned().unwrap_or_else(|| t.clone()),
629        Ty::Con(n, args) => Ty::Con(n.clone(), args.iter().map(|a| subst_named(a, m)).collect()),
630        Ty::Fun(ps, r, row) => Ty::Fun(
631            ps.iter().map(|p| subst_named(p, m)).collect(),
632            Box::new(subst_named(r, m)),
633            row.clone(),
634        ),
635    }
636}
637
638#[derive(Clone, Debug)]
639pub enum Mismatch {
640    /// Boxed because a `Ty` is a tree and this is the *error* path: making every successful
641    /// unification carry the size of a failed one is the wrong trade.
642    Different(Box<(Ty, Ty)>),
643    Arity(usize, usize),
644    Infinite,
645    /// Two function types agree on their arguments and result but not on what they do.
646    Effects(String),
647    /// The same, where what the actual side may do is not yet known — an unsolved row variable
648    /// against a context whose row is closed. Distinct from [`Mismatch::Effects`] because there is
649    /// no effect to name, and a message that names none is one no user can act on.
650    UnknownEffects,
651}
652
653impl Mismatch {
654    fn different(a: Ty, b: Ty) -> Mismatch {
655        Mismatch::Different(Box::new((a, b)))
656    }
657}
658
659impl fmt::Display for Ty {
660    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661        match self {
662            Ty::Var(v) => write!(f, "?{v}"),
663            Ty::Con(n, args) if args.is_empty() => write!(f, "{n}"),
664            Ty::Con(n, args) => {
665                write!(f, "{n}[")?;
666                for (i, a) in args.iter().enumerate() {
667                    if i > 0 {
668                        write!(f, ", ")?;
669                    }
670                    write!(f, "{a}")?;
671                }
672                write!(f, "]")
673            }
674            Ty::Fun(ps, r, row) => {
675                write!(f, "(")?;
676                for (i, p) in ps.iter().enumerate() {
677                    if i > 0 {
678                        write!(f, ", ")?;
679                    }
680                    write!(f, "{p}")?;
681                }
682                write!(f, ") -> {r}")?;
683                // A pure function prints without a row: `! {}` on every signature would be noise,
684                // and §3.2 elides the ambient set for the same reason.
685                let visible: Vec<String> = row.visible().iter().map(|e| e.name()).collect();
686                if visible.is_empty() && row.tails.is_empty() {
687                    Ok(())
688                } else {
689                    let tails: Vec<String> = row.tails.iter().map(|v| format!("e{v}")).collect();
690                    let parts = if tails.is_empty() {
691                        visible.join(", ")
692                    } else if visible.is_empty() {
693                        tails.join(" | ")
694                    } else {
695                        format!("{} | {}", visible.join(", "), tails.join(" | "))
696                    };
697                    write!(f, " ! {{{parts}}}")
698                }
699            }
700        }
701    }
702}
703
704/// The base a declaration's type parameters are numbered from.
705///
706/// The *n*th parameter of a declaration is `Ty::Var(SCHEME_BASE + n)` wherever it appears in that
707/// declaration's field types, so instantiating `Tree[Str]` is an index rather than a search. The
708/// base is far above any unification variable the checker will mint, which is what lets one `Ty`
709/// carry both without a tag.
710pub const SCHEME_BASE: u32 = 1_000_000;
711
712/// A user-declared type: a `model` (record), a `union` (ADT), a `newtype`, or an alias.
713///
714/// Comparable because a `.becki` interface is compared (§3.6, §4.3): two builds agree on a module's
715/// contract exactly when their type declarations are equal.
716///
717/// `params` is the declaration's type-parameter *names*, in order — `union Tree[T]` has `["T"]`.
718/// The names are what a `.becki` renders and what a doc page shows; the field types refer to the
719/// parameters positionally through [`SCHEME_BASE`], so a rename is a rename and nothing more.
720/// Arity is `params.len()`, declared rather than inferred from use: a parameter no field mentions
721/// is still a parameter, and `Phantom[Int]` and `Phantom[Str]` are still different types.
722#[derive(Clone, Debug, PartialEq, Eq)]
723pub enum TyDecl {
724    Model {
725        name: Arc<str>,
726        params: Vec<Arc<str>>,
727        fields: Vec<(Arc<str>, Ty)>,
728    },
729    Union {
730        name: Arc<str>,
731        params: Vec<Arc<str>>,
732        variants: Vec<Variant>,
733    },
734    /// §3.1's "zero-cost nominal newtype": ids of different entities must not be interchangeable.
735    Newtype {
736        name: Arc<str>,
737        params: Vec<Arc<str>>,
738        inner: Ty,
739    },
740    Alias {
741        name: Arc<str>,
742        params: Vec<Arc<str>>,
743        ty: Ty,
744    },
745}
746
747#[derive(Clone, Debug, PartialEq, Eq)]
748pub struct Variant {
749    pub name: Arc<str>,
750    pub fields: Vec<(Arc<str>, Ty)>,
751}
752
753impl TyDecl {
754    pub fn name(&self) -> &Arc<str> {
755        match self {
756            TyDecl::Model { name, .. }
757            | TyDecl::Union { name, .. }
758            | TyDecl::Newtype { name, .. }
759            | TyDecl::Alias { name, .. } => name,
760        }
761    }
762
763    pub fn params(&self) -> &[Arc<str>] {
764        match self {
765            TyDecl::Model { params, .. }
766            | TyDecl::Union { params, .. }
767            | TyDecl::Newtype { params, .. }
768            | TyDecl::Alias { params, .. } => params,
769        }
770    }
771
772    /// How many type arguments a mention of this name must carry.
773    pub fn arity(&self) -> usize {
774        self.params().len()
775    }
776
777    /// `[T, U]`, or the empty string when there is nothing to quantify.
778    pub fn param_brackets(&self) -> String {
779        if self.params().is_empty() {
780            return String::new();
781        }
782        format!(
783            "[{}]",
784            self.params()
785                .iter()
786                .map(|p| p.to_string())
787                .collect::<Vec<_>>()
788                .join(", ")
789        )
790    }
791
792    /// One of this declaration's field types as the declaration wrote it: the positional
793    /// parameters put back under the names they were given.
794    ///
795    /// Everything downstream of a `TyDecl` holds its parameters positionally, which is what makes
796    /// instantiation an index. Rendering is the one place that has to undo it — a `.becki` is
797    /// *source*, and `value: ?1000000` is not something the parser can read back.
798    pub fn as_written(&self, t: &Ty) -> Ty {
799        if self.params().is_empty() {
800            return t.clone();
801        }
802        let args: Vec<Ty> = self.params().iter().map(|p| Ty::con(p)).collect();
803        instantiate_decl(t, &args)
804    }
805}
806
807/// A published `trait`: the signatures it requires, over an abstract `Self`.
808///
809/// The checker keeps a trait's methods as **syntax** while it is desugaring impls and bounds, because
810/// splicing is what that pass does. This is the other half: the same declaration as *types*, which
811/// is what a `.becki` compares, what `--wire-compat` classifies, and what an importing module reads.
812/// A trait that crossed a boundary as syntax would carry spans into a file that does not own them.
813#[derive(Clone, Debug, PartialEq, Eq)]
814pub struct TraitSig {
815    pub name: Arc<str>,
816    pub methods: Vec<MethodSig>,
817}
818
819#[derive(Clone, Debug, PartialEq, Eq)]
820pub struct MethodSig {
821    pub name: Arc<str>,
822    /// Parameter names and types, with the abstract receiver as `Ty::con("Self")`.
823    pub params: Vec<(Arc<str>, Ty)>,
824    pub ret: Ty,
825    /// The declared row — the bound every implementation is held to (`docs/27` §27.7), and
826    /// therefore what a caller in another module may assume.
827    pub effects: Vec<Effect>,
828}
829
830/// A published `impl Trait for Type`.
831///
832/// There is no body here and there never will be: an importing module needs to know *that* the
833/// implementation exists and what its signature is, and the implementation itself stays where it
834/// was written.
835#[derive(Clone, Debug, PartialEq, Eq)]
836pub struct ImplSig {
837    pub trait_name: Arc<str>,
838    /// The impl's own type parameters: `["T"]` for `impl[T] Priced for Bundle[T]`.
839    pub params: Vec<Arc<str>>,
840    /// The target, with those parameters as rigid names — `Bundle[T]`.
841    pub target: Ty,
842    /// What each method actually performs, by name, for the methods that perform anything.
843    ///
844    /// The trait declares an abstract signature; this impl's methods may be **more** effectful than
845    /// it (`docs/27`), so a caller in another module cannot take the row off the trait. It has to
846    /// be published with the impl, and this is where it crosses. Empty rows are omitted: most
847    /// impls are pure and a `.becki` full of `uses` clauses saying nothing is a `.becki` nobody
848    /// reviews.
849    pub effects: Vec<(Arc<str>, Vec<Effect>)>,
850}
851
852impl ImplSig {
853    /// The head constructor dispatch keys on.
854    pub fn head(&self) -> Arc<str> {
855        self.target
856            .con_name()
857            .map(Arc::from)
858            .unwrap_or_else(|| Arc::from("?"))
859    }
860}
861
862/// Replace the positional parameters of a declaration with `args`.
863///
864/// A field type of `Some(value: ?1000000)` under `Option[Int]` is `value: Int`, and every pass that
865/// reads a declaration's fields against a concrete type goes through here.
866pub fn instantiate_decl(t: &Ty, args: &[Ty]) -> Ty {
867    match t {
868        Ty::Var(v) if *v >= SCHEME_BASE => args
869            .get((*v - SCHEME_BASE) as usize)
870            .cloned()
871            .unwrap_or_else(|| t.clone()),
872        Ty::Var(_) => t.clone(),
873        Ty::Con(n, xs) => Ty::Con(
874            n.clone(),
875            xs.iter().map(|x| instantiate_decl(x, args)).collect(),
876        ),
877        Ty::Fun(ps, r, row) => Ty::Fun(
878            ps.iter().map(|x| instantiate_decl(x, args)).collect(),
879            Box::new(instantiate_decl(r, args)),
880            row.clone(),
881        ),
882    }
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888
889    #[test]
890    fn unification_binds_and_propagates() {
891        let s = Subst::new();
892        let a = s.fresh();
893        assert!(s.unify(&a, &Ty::int()).is_ok());
894        assert_eq!(s.resolve(&a), Ty::int());
895        assert!(s.unify(&a, &Ty::str_()).is_err());
896    }
897
898    #[test]
899    fn structural_unification_descends() {
900        let s = Subst::new();
901        let a = s.fresh();
902        let b = s.fresh();
903        assert!(s
904            .unify(
905                &Ty::map(a.clone(), b.clone()),
906                &Ty::map(Ty::int(), Ty::str_())
907            )
908            .is_ok());
909        assert_eq!(s.resolve(&a), Ty::int());
910        assert_eq!(s.resolve(&b), Ty::str_());
911    }
912
913    #[test]
914    fn the_occurs_check_rejects_infinite_types() {
915        let s = Subst::new();
916        let a = s.fresh();
917        assert!(matches!(
918            s.unify(&a, &Ty::list(a.clone())),
919            Err(Mismatch::Infinite)
920        ));
921    }
922
923    #[test]
924    fn instantiation_is_fresh_per_use() {
925        let s = Subst::new();
926        let v = 0;
927        let scheme = Scheme {
928            params: Vec::new(),
929            vars: vec![v],
930            row_vars: Vec::new(),
931            ty: Ty::fun(vec![Ty::Var(v)], Ty::Var(v)),
932        };
933        let one = s.instantiate(&scheme);
934        let two = s.instantiate(&scheme);
935        if let Ty::Fun(ps, _, _) = &one {
936            assert!(s.unify(&ps[0], &Ty::int()).is_ok());
937        }
938        if let Ty::Fun(ps, _, _) = &two {
939            assert!(s.unify(&ps[0], &Ty::str_()).is_ok());
940        }
941    }
942
943    #[test]
944    fn the_tier_table_matches_the_design() {
945        // §3.3: client cannot discharge `ingress` or `durable`; server cannot discharge `dom`.
946        assert!(!Tier::Client.discharges(&Effect::Ingress));
947        assert!(!Tier::Client.discharges(&Effect::Durable));
948        assert!(Tier::Client.discharges(&Effect::Dom));
949        assert!(Tier::Server.discharges(&Effect::Ingress));
950        assert!(Tier::Server.discharges(&Effect::Durable));
951        assert!(!Tier::Server.discharges(&Effect::Dom));
952        assert!(Tier::Data.discharges(&Effect::Durable));
953        assert!(!Tier::Data.discharges(&Effect::Nondet));
954        // `any` means unplaced-pure: it discharges exactly the intersection, so anything a tier
955        // refuses forces a placement, and an ambient effect never does.
956        assert!(!Tier::Any.discharges(&Effect::Durable));
957        assert!(Tier::Any.discharges(&Effect::Ambient(Ambient::Log)));
958    }
959
960    #[test]
961    fn only_the_own_origin_is_reachable_from_a_browser() {
962        // §3.3's table says `net.out(own-origin)` and means it: a client that could name any host
963        // would be a placement decision made by CORS at runtime.
964        assert!(Tier::Client.discharges(&Effect::NetOut(Arc::from("origin"))));
965        assert!(!Tier::Client.discharges(&Effect::NetOut(Arc::from("payments.example.com"))));
966        assert!(Tier::Server.discharges(&Effect::NetOut(Arc::from("payments.example.com"))));
967    }
968
969    #[test]
970    fn a_closed_row_accepts_less_and_refuses_more() {
971        let s = Subst::new();
972        assert!(s
973            .subsume_row(&Row::of([Effect::Dom]), &Row::of([Effect::Dom]))
974            .is_ok());
975        // Doing less than the context allows is fine — that is subsumption.
976        assert!(s
977            .subsume_row(&Row::empty(), &Row::of([Effect::Dom]))
978            .is_ok());
979        // Doing something the context does not allow is the error.
980        assert!(s
981            .subsume_row(&Row::of([Effect::Dom]), &Row::of([Effect::Durable]))
982            .is_err());
983    }
984
985    #[test]
986    fn an_open_row_absorbs_what_is_passed_to_it_twice() {
987        // The everyday case: `map_list`'s `(a -> b ! e)` meets a lambda that touches the dom, and
988        // then — at another call site through the same monomorphic parameter — a pure one.
989        let s = Subst::new();
990        let e = s.fresh_row();
991        assert!(s.subsume_row(&Row::of([Effect::Dom]), &e).is_ok());
992        assert!(s.subsume_row(&Row::empty(), &e).is_ok());
993        assert!(s.subsume_row(&Row::of([Effect::Durable]), &e).is_ok());
994        assert_eq!(
995            s.resolve_row(&e).atoms,
996            BTreeSet::from([Effect::Dom, Effect::Durable]),
997            "a row variable widened at two call sites holds the union, never a contradiction"
998        );
999    }
1000
1001    #[test]
1002    fn effect_polymorphism_carries_a_callers_row_to_the_result() {
1003        // `map_list : (list[a], (a -> b ! e)) -> list[b] ! e`, applied to an effectful function,
1004        // must make the *application* effectful. That is the whole point of the row variable.
1005        let s = Subst::new();
1006        let e = s.fresh_row_var();
1007        let scheme = Scheme {
1008            params: Vec::new(),
1009            vars: vec![],
1010            row_vars: vec![e],
1011            ty: Ty::fun_eff(
1012                vec![Ty::fun_eff(vec![Ty::int()], Ty::int(), Row::var(e))],
1013                Ty::int(),
1014                Row::var(e),
1015            ),
1016        };
1017        let Ty::Fun(params, _, latent) = s.instantiate(&scheme) else {
1018            panic!("a function");
1019        };
1020        // Pass something that mints ids. The actual is the argument, the expected the parameter.
1021        assert!(s
1022            .unify(
1023                &Ty::fun_eff(vec![Ty::int()], Ty::int(), Row::of([Effect::Nondet])),
1024                &params[0],
1025            )
1026            .is_ok());
1027        // The trailing variable is deliberate: subsumption leaves room for a *later* call site to
1028        // widen the same row. What matters is that the atom arrived.
1029        assert_eq!(
1030            s.resolve_row(&latent).atoms,
1031            BTreeSet::from([Effect::Nondet])
1032        );
1033    }
1034
1035    #[test]
1036    fn a_recursive_row_resolves_to_its_least_fixed_point_rather_than_diverging() {
1037        // Two mutually recursive effectful functions: `r_f = {dom} ∪ r_g`, `r_g = {durable} ∪ r_f`.
1038        let s = Subst::new();
1039        let (f, g) = (s.fresh_row_var(), s.fresh_row_var());
1040        s.bind_row(f, Row::of([Effect::Dom]).union(&Row::var(g)));
1041        s.bind_row(g, Row::of([Effect::Durable]).union(&Row::var(f)));
1042        let resolved = s.resolve_row(&Row::var(f));
1043        assert_eq!(resolved, Row::of([Effect::Dom, Effect::Durable]));
1044        assert!(resolved.is_closed());
1045    }
1046
1047    #[test]
1048    fn a_pure_function_prints_without_a_row_and_an_effectful_one_with_it() {
1049        assert_eq!(
1050            Ty::fun(vec![Ty::int()], Ty::int()).to_string(),
1051            "(Int) -> Int"
1052        );
1053        assert_eq!(
1054            Ty::fun_eff(vec![], Ty::unit(), Row::of([Effect::Durable])).to_string(),
1055            "() -> Unit ! {durable}"
1056        );
1057        // Ambient effects are elided from the signature, exactly as §3.2 says.
1058        assert_eq!(
1059            Ty::fun_eff(
1060                vec![],
1061                Ty::unit(),
1062                Row::of([Effect::Ambient(Ambient::Log), Effect::Dom])
1063            )
1064            .to_string(),
1065            "() -> Unit ! {dom}"
1066        );
1067    }
1068}