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