Ship

Old Docs · Modes
On this page · 14 sections

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:

AxisLatticeMeaning
Localityglobal ≤ linked ≤ localWhere a value may live / be retained
Uniquenessaliased ≤ uniqueProvably sole live reference (past)
Multiplicitymany, once, requiredHow often a closure/Code may run (future)
Sendabilityconfined ≤ sendableMay 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, returns global, aliased/many). @ global must 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 / eff must 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 @ global up 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)

AxisFree / structuralPaid
LocalityCrossing; inference re-derives at birthcopy; params never promote
UniquenessUnique by birthcopy once aliased
MultiplicityShare result; capture lesscopy; lazy (open)
SendabilityUnique transfer; effects are not capturescopy; handles never

Runtime Payoff

  • local → region/stack allocation (LocalStack hooks)

  • unique → in-place builders / zero-copy finish

  • sendable + unique → zero-copy message passing

  • Modes erase entirely on JS — compile-time checking only

    Value-level modes (not kinds, not arrows): one List for 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

ModeAxisPhrase
locallocalitybelongs to a region; must not be kept or returned
linkedlocalitynot kept; may come back in the result
globallocalitymay be kept
aliaseduniquenessother references may exist
uniqueuniquenesssole reference; safe in-place
oncemultiplicityat most once
requiredmultiplicityexactly once (mechanics open)
manymultiplicityany number of times
confinedsendabilitytied to home context
sendablesendabilitysafe across a boundary
spawnablealiasonce sendable
transferablealiasunique sendable

Diagnostics use friendly phrases and programmer actions, never lattice jargon.

Open Questions