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 ¶ms[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}