Modes
Modes track circumstantial facts about values — locality, uniqueness, multiplicity, sendability — alongside types. They are not types: x: String @ local is type String, mode local.
Settled spine (June 2026 catalog). Staging: questions/100-mode-implementation-order.md, SHIP-5. Builds on GC-first semantics, immutable-by-default, explicit mut (unchanged — unique proves destructive update is unobservable, not a mut replacement), one @ marker, and not Rust ownership.
Model
Four axes, one @, value-level inference:
| Axis | Lattice | Meaning |
|---|---|---|
| Locality | global ≤ linked ≤ local | Where a value may live / be retained |
| Uniqueness | aliased ≤ unique | Provably sole live reference (past) |
| Multiplicity | many, once, required | How often a closure/Code may run (future) |
| Sendability | confined ≤ sendable | May cross worker/actor boundary |
Call sites never write modes. Ordinary code writes @ global to retain, @ local to accept a value that must be neither kept nor returned, and return! to build a local result in the caller. An unmarked parameter is @ linked: not kept, but the result may hold it. .shc may promise @ once or demand @ required / @ sendable.
Syntax
let push: fn!(event: Event @ global sendable) -> Unit, eff Bus;
let dispatch: fn!(
worker: Worker,
message: Message @ global sendable,
) -> Unit, eff ActorSystem;
let send: pure fn!(w: Worker, buf: Builder @ unique sendable) -> Unit;
One @ per slot; modes space-separated; canonical order enforced: locality, uniqueness, multiplicity, sendability. Out-of-order / duplicate-axis are errors. @ binds tighter than ->: fn() -> Builder @ unique modes the return; (fn() -> Unit) @ once sendable modes the function value.
Crossing claims on abstracts: abstract type t @ +local +unique +sendable; (empty @ = crosses nothing; bare abstract type t without claims is rejected once that checker lands — claims are always verified against the companion).
Builtin aliases (adjectives only — no domain nouns):
@ spawnable // = once sendable
@ transferable // = unique sendable
Aliases stand alone; mixing with raw modes errors; hovers show expansion. User-defined aliases deferred — 101-mode-syntax.md.
When mode syntax hits lexer/parser for real surfaces, follow ship-syntax-change (@ is lexed today but unparsed on type slots in some paths).
Ordinary Code: Zero Annotations
let create_todo = fn(req: Request) -> Response,
exn [ :auth(AuthError) | :db(DbError) ],
eff Db
{
...
}
Modes re-infer across refactors. First contact is often an error about invisible inference (a private helper keeps a value via Stash.keep) — provenance traces in diagnostics are mandatory.
Locality
Executable behavior: Locality. Loop iteration regions and tail calls: 175.
Retention Must Be Visible
.shc hybrid (decided):
Contracted modules: unannotated slots = restrictive defaults (params
linked, returnsglobal,aliased/many).@ globalmust be written. Dependent checking never needs body analysis.Fn-typed slots are re-callable and capture-silent by default (see Multiplicity). Demands/promises written only where real.
Modes are the one axis where contract omission carries meaning (
exn/effmust be chosen explicitly — 02-type-inference.md).Contract-less modules: fully inferred modes cross-module.
Tooling hints under-promise gaps;
#infer let name;opts a whole header out of self-containedness (no mode-only#infer).Boundary-only annotations (chosen) over uniform enforcement: retention infection through private chains stays inferred until a boundary or local collision; provenance traces close the blame gap. Prefer
copy/ id keys over forcing@ globalup a chain.
Uniqueness
let push: pure fn!(b: Builder @ unique, s: String) -> Builder @ unique;
let finish: pure fn!(b: Builder @ unique) -> String;
let b = Builder.new();
let b = Builder.push(b, "hello");
let s = Builder.finish(b);
Builder.push(b, "!") // error: consumed by finish
Reads borrow implicitly via @ local params — uniqueness holds after. Consume- return / |> threading is the ergonomic target; hot loops may use let mut. Statement-position discard consumes (accepted sharp edge). Explicit borrow-into-binding deferred.
Multiplicity
Closures/Code join captures on every axis (unique capture → once). Interval lattice: once = [0..1], required = [1..1], many = [0..∞]. Arithmetic: sequencing adds, nesting multiplies; insertion counts future uses.
// bare fn type = re-callable (common case)
let each: fn('a)(v: Vector('a), ~f: fn('a) -> Unit) -> Unit;
// @ once = promise "I call this ≤1"
let map: fn('a, 'b)(o: Option('a), ~f: (fn('a) -> 'b) @ once) -> Option('b);
Capture axes never appear on bare fn types — only the value knows. @ once / @ sendable / @ global written at real demands. Until the multiplicity checker lands, @ once may be an unenforced promise for API shape exploration.
required is named; mechanics open — 105-required-multiplicity-mechanics.md. Erased @ 0 deferred. Lazy as once→many promotion — 106-lazy-and-mode-promotion.md.
Sendability
Actor.send(worker, task); // immutable data: free by structure
Actor.send(worker, buf); // Builder @ unique: transfer OK
Actor.send(worker, shared_buf) // aliased mutable: error
Kind-derived for plain data; per-value for closures/unique handoffs. Records are mode-polymorphic over closure fields (type carries no verdict). Low pole is confined (tied to home context). Durable workflows persist typed step inputs and module identity rather than transporting effectful callables; see question 240.
Crossing, Fields, copy
Boring types (immediates, static literals, deeply immutable) cross axes — crossing ships before checking noise. Opaque abstracts must claim crossed axes explicitly (@ +…); verified, never trusted.
A stored slot may cut one axis off that deep walk. The cut is the same @ on a product field, a variant payload, or a type parameter when every stored occurrence of that parameter shares it. The executable Field modes chapter is that behavior. Unique fields in aggregates are sticky (destructure/rebuild). Module-wide defaults and mut beside a cut stay open in 102-modality-syntax.md.
copy(x) is the sanctioned mode-crossing (not Rust Clone pressure): returns fresh @ unique, region-free, sendable if allowed. Named copy; synthesized Copy(T) with !Copy opt-out. Resources and once closures refuse copy. Copy depth for mutable interiors undecided.
Promotion (No In-Place)
| Axis | Free / structural | Paid |
|---|---|---|
| Locality | Crossing; inference re-derives at birth | copy; params never promote |
| Uniqueness | Unique by birth | copy once aliased |
| Multiplicity | Share result; capture less | copy; lazy (open) |
| Sendability | Unique transfer; effects are not captures | copy; handles never |
Runtime Payoff
local→ region/stack allocation (LocalStackhooks)unique→ in-place builders / zero-copyfinishsendable+unique→ zero-copy message passingModes erase entirely on JS — compile-time checking only
Value-level modes (not kinds, not arrows): one
Listfor every mode; records mode-polymorphic per value; HM-shaped prenex inference matching 02-type-inference.md. A mode variable ('m) names a region each call picks from its arguments: written on function parameters and results, never inferred, never on stored slots (Locality).
Compiler Status
Parsing, vocabulary, order/alias/crossing checks, mut enforcement, abstract crossing verification, @ global retention contracts, locality checking with unmarked-parameter inference, return!, mode variables, owned @ unique fields, contract and behavior-member conformance, JS erasure, and related goldens exist. Staged until each lands (annotations may error as unchecked): uniqueness+multiplicity affine checker, Copy(T), sendability on real boundary APIs, LocalStack + elaborations — see 100-mode-implementation-order.md.
Vocabulary
| Mode | Axis | Phrase |
|---|---|---|
local | locality | belongs to a region; must not be kept or returned |
linked | locality | not kept; may come back in the result |
global | locality | may be kept |
aliased | uniqueness | other references may exist |
unique | uniqueness | sole reference; safe in-place |
once | multiplicity | at most once |
required | multiplicity | exactly once (mechanics open) |
many | multiplicity | any number of times |
confined | sendability | tied to home context |
sendable | sendability | safe across a boundary |
spawnable | alias | once sendable |
transferable | alias | unique sendable |
Diagnostics use friendly phrases and programmer actions, never lattice jargon.
Open Questions
101-mode-syntax.md— user aliases102-modality-syntax.md— module defaults,mutbeside a cutCopy depth for mutable interiors — undecided