Skip to content

§7 Meaning by translation

Bynk has no operational semantics. A program’s dynamic meaning is defined by translation: it is the behaviour of the TypeScript the compiler emits for it, executed against the runtime library (§7.4). This chapter defines that translation per construct, and the two emission targets it varies over.

For every well-formed program (§5) the compiler emits TypeScript. The meaning of the program is the meaning of that TypeScript run against the runtime library. There is no second, independent definition of behaviour to reconcile against; where this chapter and the emitter disagree, that is a defect (§1.1). The emitted output is deterministic and each file is headed // Generated by bynkc — do not edit by hand.

A program is emitted for one of two targets, selected by --target (§8). A commons emits the same plain TypeScript — types and functions — on either target; the targets differ in how a context, its agents, and cross-context calls are realised.

bundle (default)workers
Layouta flat .ts tree mirroring the sourceone Worker directory per context
Cross-context calla direct, in-process callJSON over a Cloudflare Service Binding, validated at the boundary
Agentbacked by an in-process StateRegistrya Cloudflare Durable Object keyed by the agent key
Consumed adapterin-process, via its bindingin-process, via its binding — no Service Binding, no Env entry

An adapter is not a deployment unit: consuming one is in-process on both targets (§7.3.6). Only consumed contexts become Service Bindings under workers.

On the workers target a context with handlers additionally emits a router and boundary plumbing (index.ts), the handler logic (handlers.ts), the composition root (compose.ts), and a wrangler.toml; records that cross a boundary gain serialise_* / deserialise_* helpers. Cross-context data MUST be structurally validated as it crosses (§6.5).

Float at boundaries (v0.21, ADR 0040). Boundary Float values are finite, even though in-language arithmetic is host-defined (§5.2):

  • deserialise_ of a Float field requires typeof v === "number" && Number.isFinite(v), with no integer check (decimals are the point). JSON.parse("1e999") yields Infinity, which is rejected as a StructuralMismatch expecting a finite number — never admitted from the wire.
  • serialise_ of a Float field throws on a non-finite value (a contract violation): JSON.stringify(NaN) would otherwise silently produce null and break the round-trip.
  • v0.22b (ADR 0049): a bare Int field additionally requires Number.isInteger — at record/nested fields and workers handler params. A previously-accepted {qty: 1.5} now fails with a StructuralMismatch expecting an integer; with Float in the language there is no excuse for fractional Ints, and codec and .of agree (as 0040 aligned them for Float).
ConstructEmits
alias (type Id = Int)a branded base type with a .of constructor
refined typea branded base with .of (a runtime predicate check returning Result)
opaque typea branded base with .of and .unsafe constructors; no structural access to the representation
recordan interface with readonly fields, constructed from an object literal
sum / enuma discriminated union over a tag field, plus a constructor namespace

Only an opaque type emits a public .unsafe constructor (ADR 0182) — its representation is hidden, so its defining commons needs one; a refined or alias type emits none, so no host or adapter code can brand a value past the refinement predicate. A refined type is constructed through .of and compile-time literal admission (type-system §6.4); an admitted literal lowers to an inline brand cast (§7.3.2, below), not a constructor call.

A brand is a compile-time TypeScript intersection (a phantom field); it is erased after type-checking and has no runtime representation. A sum type lowers as:

export type Status =
{ readonly tag: "Pending" }
| { readonly tag: "Shipped"; readonly tracking: string };
export const Status = {
Pending: { tag: "Pending" } as Status,
Shipped: (tracking: string): Status => ({ tag: "Shipped", tracking }),
};

All four base types lower to TypeScript primitives: Int and Float both emit number (v0.21 — the distinction is checker-side only and erased), String emits string, Bool emits boolean. A refined Int’s .of includes a Number.isInteger check; a refined Float’s .of includes Number.isFinite instead — validated Float values are finite (0040).

ConstructEmits
if … else …a conditional / if
matcha switch on .tag, payload fields bound as const
admitted literalan inline brand cast (literal as T) (§6.4; ADR 0182)
float literalthe source lexeme verbatim (1e10 does not normalise)
interpolated stringa template literal: chunks as escaped text (backslash, backtick, and $ escaped), each \(e) hole as ${String(<e>)} (ADR 0075)
Int / IntMath.trunc(a / b) — truncating, unchanged
Float / Floata / b — true division (v0.21, operand-typed)
numeric kerneli.toFloat() → the receiver (erased identity); f.round()/floor/ceil/truncateMath.round(f) / Math.floor(f) / Math.ceil(f) / Math.trunc(f); x.toString()String(x) (host number→string, ADR 0074)
?a check-and-early-return: on a Result, if (r.tag === "Err") return r; (or, when a declared embedding converts the error (v0.154), return Err(F.V(r.error));); on an Option in an HttpResult handler (v0.153), if (o.tag === "None") return HttpResult.NotFound; — then the expression is the Ok/Some payload
<-await
~>ctx.__exec.waitUntil(<effect>) — dispatched, not awaited

An Effect[T] is realised as a Promise<T>; it has no runtime constructor. Effect.pure(x) lowers to x directly where an async context absorbs it, and to Promise.resolve(x) in a synchronous or tail position. The <- bind is therefore await and needs no runtime support.

An asynchronous send (~>, §4.8.5) lowers on the Workers target to deps.__exec.waitUntil(<effect>): the effect’s Promise is handed to the execution context’s waitUntil rather than awaited, so it settles after the handler returns its response instead of being cancelled with it. The execution context is threaded from the entry point (fetch/scheduled/queue’s third argument) through compose(env, ctx) into the handler’s deps.__exec — but only for contexts that contain a send, so a context that never uses ~> emits byte-for-byte as before. This is the immediate delivery tier; a buffered/at-commit tier is reserved for the events channel and is not yet emitted.

An agent emits a state interface, a zero-value factory that realises the field zeros and initialisers of §5.4, and a class whose handlers read through loadState and write through commitState:

function __zeroOfCounterState(): CounterState { return { count: 0 }; }
// loadState(): return stored ?? __zeroOfCounterState();

On bundle an agent is constructed against a per-agent StateRegistry (a serialised-key-to-state map, reset between tests); on workers it is a Durable Object addressed by the agent key. A single makeAgent helper selects the path from whether a Durable Object binding is present, so call sites are identical across targets (§7.4).

Invariants (v0.80). When an agent declares invariants (§5.4.1), commitState(s) gates on each predicate (lowered as a pure expression over the proposed state s, with state fields read as s.<field> and implies as (!(P) || Q)) before storage.put. A failed predicate console.error-logs the agent type and invariant name — never the key value (ADR 0107) — and throws the dedicated invariantViolation(agent, invariant) fault, so the offending state is never written. The fault rides the existing uncaught-fault channel and surfaces to the caller as a 500-class fault, not an outcome:

private async commitState(s: OrderState): Promise<void> {
if (!((!(s.status === OrderStatus.Paid) || (s.paymentRef.tag === "Some")))) {
console.error("InvariantViolation Order.paid_has_payment_ref",
{ agent: "Order", invariant: "paid_has_payment_ref" });
throw invariantViolation("Order", "paid_has_payment_ref");
}
await this.state.storage.put("state", s);
}

Transitions (v0.116). When an agent declares transitions (§5.4.1-i), commitState(s) also gates on each step predicate, after the snapshot invariants and before storage.put. The old state is read from storage inside the gate — the method performs the write, so storage still holds the pre-commit value there; undefined is the genesis commit and the whole block is skipped. The bindings lower to __old/__new (new is a JS reserved word), and a failed predicate throws the same invariantViolation(agent, transition) fault:

const __prior = await this.state.storage.get<OrderState>("state");
if (__prior !== undefined) {
const __old = { ...__zeroOfOrderState(), ...__prior };
const __new = s;
if (!((!(__old.status.tag === "Paid") || __new.status.tag === "Paid"))) {
console.error("InvariantViolation Order.paid_is_terminal",
{ agent: "Order", invariant: "paid_is_terminal" });
throw invariantViolation("Order", "paid_is_terminal");
}
}

On the workers target, each context with HTTP handlers emits handlers.ts (the handler logic), index.ts (the router and boundary validation), compose.ts (the wiring), and a wrangler.toml. A handler’s HttpResult[T] (§5.7) determines the HTTP status and body of the Response; records crossing the boundary are serialised and deserialised through the generated serialise_* / deserialise_* helpers.

The router MUST answer the method contract derived from the declared routes. For each path, the allowed-method set is the union of the methods declared on it, plus HEAD when GET is declared, plus OPTIONS. A GET dispatch block also accepts HEAD: the GET handler runs and its Response is returned with the body stripped (headResponse — same status and headers, empty body, the source body never read). When no dispatch block matches, the router MUST test the request path against each known path: on a match, a plain OPTIONS (one that does not carry Access-Control-Request-Method) is a 204 and any other method is a 405, both carrying Allow = the path’s allowed-method set; on no match, the response is 404. The synthesised 405/OPTIONS answers are produced before the handler authentication seam (method discovery and rejection are credential-less, as with the CORS preflight); a HEAD runs GET’s seam unchanged. These are synthesised router responses and do not touch the HttpResult sum.

A service that declares a cors { } policy (§5.7.1) additionally emits, in index.ts, a synthesised CorsPolicy constant plus two behaviours. First, a preflight branch — an OPTIONS that carries Access-Control-Request-Method and matches any of that service’s route paths — returns corsPreflightResponse(policy, origin), a 204 carrying the Access-Control-* headers; this branch MUST precede the route dispatch, so a preflight is answered before the handler authentication seam (a preflight carries no credentials and MUST NOT be rejected by a by/Bearer check). A bare OPTIONS (without that header) is not a preflight and is answered by the generic method-contract fall-through above. Second, each of that service’s route responses — and its synthesised 405/OPTIONS — is wrapped in applyCors(response, policy, origin), which stamps Access-Control-Allow-Origin (reflecting a matched allowlist origin with Vary: Origin, or emitting * for a wildcard policy; omitting the header — fail closed — when the origin does not match). Allowed methods are derived from the service’s routes (the same derivation as the Allow header — so HEAD appears when GET is declared); allowed headers default to content-type (plus Authorization when the service has a Bearer route). A service without a cors { } policy emits neither CORS behaviour and, apart from the always-on method contract above, is otherwise unchanged.

§7.3.4a Actors & the verification seam (v0.45)

Section titled “§7.3.4a Actors & the verification seam (v0.45)”

An actor declaration emits no TypeScript — like a brand, it is a compile-time contract. The handler by clause (§5.7a) lowers through a per-scheme verification seam that mirrors the protocol descriptor: a scheme contributes its verification codegen, identity shape, and failure mapping behind one interface. The two zero-crypto schemes add no topology: None always admits, and Internal reuses the channel-trust assertion already implicit in the service-binding and platform-dispatch entry points — so a handler with a by clause emits byte-identically to one without. The bound identity (<binder>.identity) is minted at the seam; for the zero-crypto schemes it is the sealed unit value. Bearer (v0.47) extends the seam with real verification: the compose wrapper extracts Authorization: Bearer …, HS256-verifies the JWT (verifyBearerJwtHs256 in the runtime) against a secret sourced from env, enforces exp/nbf, and mints the identity by constructing the declared type from the sub claim — returning HttpResult.Unauthorized (401) fail-closed on any failure, before the body. The minted identity threads through the handler’s deps, so <binder>.identity reads the verified value. Signature (v0.51) verifies an HMAC over the request body for inbound webhooks. Because the signature is over the raw bytes, its seam sits in the entry dispatch (where the body is read): it reads the body once as text (await request.text()), sources the secret from env, recomputes HMAC-SHA256 and constant-time-compares (verifySignatureHmacSha256 in the runtime, via crypto.subtle.verify) against the configured signature header (accepting a bare hex digest or a sha256=<hex> prefix), optionally checks a signed timestamp is within tolerance of now (binding <timestamp>.<body> as the signed string), then hands the same text to body deserialisation — never a re-read or a re-serialisation. Any failure → HttpResult.Unauthorized (401), fail-closed, before the body. A Signature actor carries no identity.

Oidc (v0.151) extends the seam like Bearer, but verifies against a provider’s public key set instead of a shared secret. The compose wrapper extracts Authorization: Bearer …, calls verifyOidcJwt (in the runtime) with the declared issuer/audience/jwks literals, and mints the identity by constructing the declared type from the verified sub claim — returning HttpResult.Unauthorized (401) fail-closed on any failure, before the body. The verifier fetches the JWKS (cached, refetched on a kid miss for key rotation — rate-limited by a cooldown so an attacker-chosen kid cannot amplify into fetches), imports the matching RS256/ES256 public key, verifies the signature (crypto.subtle.verify), and enforces the trust contract — iss equals the declared issuer, aud contains the declared audience, exp is in the future, nbf (if present) has passed. Because no secret is named the wrapper sources nothing from env; the trust parameters are the actor declaration’s public literals. The minted identity threads through deps exactly as Bearer’s does.

A multi-actor sum (by who: A | B, v0.52) composes these seams under first-wins resolution in a single boundary wrapper, which owns the whole boundary so the request is read once. When any member verifies over the body (a Signature peer) or the handler takes a body, it reads the raw body once as text; it then tries each member’s scheme in declared order — a Bearer peer against the Authorization header, a Signature peer against those held bytes, a None peer accepting unconditionally — and binds the first success into a tagged { tag: "<Actor>", identity?: … } value threaded through deps.who, which the body matches. If no member verifies, the wrapper returns HttpResult.Unauthorized (401) fail-closed, before the body; otherwise it parses the body param from the same bytes and dispatches. No member re-reads the request — composing a header member with a body member never re-reads or re-serialises.

A refinement actor (actor Admin = User where <claim predicate>, v0.53) adds an authorisation check to its base’s seam. The Bearer base verifies as above (failure → 401); the seam then surfaces the verified claims (verifyBearerJwtHs256 now returns them alongside sub) and evaluates the lowered claim predicate against them — a failed invariant returns HttpResult.Forbidden (403, distinct from the 401 authentication channel), before the identity mints or the body runs. The claims are an authorisation-time input only: the body still sees just the sealed base identity (<binder>.identity), so claims are not threaded into deps.

The Caller actor (on call … by c: Caller, v0.54) mints a live CallerId over the cross-context Service Binding. The call site (callService) stamps the calling context’s qualified name — a compile-time constant — into a reserved X-Bynk-Caller header beside the (unchanged) args body. The callee’s /_bynk/call/<service> dispatch reads the header, rejects fail-closed if it is absent or empty (the internal analogue of 401), and threads the name into the handler’s deps as the CallerId identity, so <binder>.identity lowers to deps.identity (replacing the undefined placeholder). Verification is channel-based — no crypto — and a binder-less on call reads no header and is byte-unchanged.

§7.3.4b The cross-context boundary codec (v0.176)

Section titled “§7.3.4b The cross-context boundary codec (v0.176)”

Every value crossing a workers cross-context boundary is encoded and decoded by a generated codec — the same serialise_* / deserialise_* helpers §7.2 requires, monomorphised per instantiation. No wire position asserts a value with an as JsonValue cast, and no return type decodes through an unvalidated identity function.

The boundary is therefore symmetric by construction: the same dispatch names the serialiser and the deserialiser, so a type cannot be encoded one way and decoded another. This is what admits a bare Bytes in a cross-context signature — it base64-encodes outbound and base64-decodes (with validation) inbound. Before v0.176 the boundary carried its own codec dispatch, which cast a Bytes outbound while decoding it inbound; the resulting mis-encode was diagnosed rather than emitted (ADR 0142 D8). With one dispatch, the asymmetry — and so the restriction — is gone.

Each Worker is self-contained: a context generates its own codecs for the contracts it participates in and imports no sibling context’s module as a value (#661, discharging ADR 0199 Decision G). A caller reaches its callee’s codecs through local serialise_* / deserialise_* helpers it emits itself; the callee module is imported for types only (import type * as <ns>), which is erased outright, so the caller’s bundle never carries the callee’s provider implementation. Only the callee’s own exported types reachable from the services the caller actually calls are generated — commons types the caller already holds are left alone, and an uncalled service contributes nothing.

A caller-side codec for a callee-owned type cannot route through the owner’s .of constructor (it lives in the owner’s module), so validation follows the export visibility: an opaque type validates its base and casts — its refinement is the owner’s secret and is not re-checked, which is sound because the value came from the owner’s typed code and a skewed owner is caught by the §7.3.4c contract hash — while a transparent refined type inlines its predicate, since the consumer knows the shape by declaration. This applies to consumed contexts under workers only: a consumed adapter’s binding namespace is a real value import used by the composition root, and on bundle the contexts compile together.

One position remains deliberately not codec-checked, stated here rather than left to be discovered: the runtime-owned error types (ValidationError, JsonError, HttpResult, QueueResult) pass through uncoded. They are declared by the runtime rather than by a type declaration the emitter can walk, so there is no helper to generate; their JSON shape is fixed by the runtime, so the pass-through is unchecked rather than wrong.

A cross-context call carries a contract hash beside the caller identity: a reserved X-Bynk-Contract header holding a compile-time constant, exactly as §7.3.4a’s X-Bynk-Caller does. The args body is unchanged.

The constant is the canonical normal form of the callee’s on call contract, hashed. The form is deterministic and order-insensitive where the wire is: refinement predicates canonicalise as a set, and record fields and sum variants sort by name — a JSON object is unordered and a sum carries a kind discriminant, so their order is not wire-observable and MUST NOT change the hash. Field presence, field types, parameter names, parameter order, and the return type all MUST change it. An opaque type contributes its representation but not its predicate: a consumer cannot observe that predicate, so it is not part of the contract between them.

Caller and callee canonicalise the callee’s contract in the callee’s own namespace, from the callee’s own type table. A caller MUST NOT canonicalise a consumed type in its own namespace, where rebranding would render the same type differently.

The callee compares the header against its own constant before reading the request body — and so before the caller check — and on mismatch answers 409 with a ContractMismatch body naming the service, the expected hash, and what arrived. Once the contracts disagree the body’s interpretation is precisely what is in doubt, so validating it first would misreport the fault. An absent or empty header is a mismatch: a Bynk caller always stamps one.

ContractMismatch is not a BoundaryError — a codec cannot produce it — so the call surface is typed CallError = BoundaryError | ContractMismatch (§7.4).

Each Worker also emits bynk-contracts.json beside its wrangler.toml, carrying what the context provides per service and what it expects of each dependency. This is a driver artifact, not part of the wire.

Each test unit emits a per-target test module; an aggregating runner (tests/main.ts) collects the module results. An assert (§5.9) lowers to a runtime check that throws on failure, which the runner records as a failing case. bynkc test emits these modules, compiles them with tsc, and runs the aggregated runner on Node (§8.4).

A contracted pure function (requires/ensures, §5.4.1a) emits behind a build-profile switch (ADR 0150). In the dev/test profile (bynkc test, --inspect) the function is wrapped by a call-site guard: each requires is checked on entry and each ensures on exit (over the bound result), throwing a contract failure that names the clause and the offending arguments/result. The guard is emitted once around the definition (O(1) in code size), not at each call site. In the release profile (bynkc compile) the guard is stripped entirely — the emitted function is identical to an uncontracted one, so contracts add no production cost or behaviour. The runner attack (§7.4.10) is emitted only alongside the guard.

A case that observes a capability (expect Cap.op called … or trace(Cap.op), §5.9b) emits behind the same build-profile switch as contracts (ADR 0150). In the test profile the case’s deps object is wrapped by a recording proxy — for each observed operation, the seam function is replaced by a wrapper that appends { args, order } to a per-op log (__obs) and then delegates to whatever stands behind the seam (a stub or the real provider), so the return value is unchanged. The sugar lowers to reads over that log (called → length checks; with <pred> → a filter over the recorded args using the lowered predicate; before → an order-index comparison); trace(Cap.op) lowers to the recorded list mapped to per-op records, with a synthetic type __Cap_op_Call = { … } alias emitted so the records type-check. The proxy, the log, and the alias are emitted only in the test build; a module with no observation emits byte-for-byte unchanged, and the deploy build calls the seam directly — observation adds no production cost or behaviour.

A case’s tier (§5.9c) resolves how each seam is provided, per build, behind the ADR-0147 build strip — a suite is a test-only declaration, so all of this machinery is test-build-only and never reaches the deploy build.

For each capability seam the unit under test consumes, the emitter resolves the provision in precedence order case provides > suite provides > the tier default (§5.9d):

  • The tier default. unit and integration emit in process with the real provider for any un-overridden seam (full unit auto-stubbing is a named follow-on); system stands the inferred participants up as Workers and wires them across the real serialise → JSON → deserialise boundary (§7.4.5), the participant set being the target’s transitive consumes closure.
  • A stub override lowers to a stub object behind the recording proxy (§7.3.5c), so a case can both stub a return and observe the call. The method’s clauses become an ordered match over the recorded arguments (the predicate surface lowered as in observation’s with), first match wins:
    • returns <value> lowers to a constant return of the lowered value;
    • fails lowers to a thrown/Err capability fault;
    • returns each [<outcome>, …] lowers to a per-call cursor over the lowered outcomes with last-outcome-repeat exhaustion (see §7.4.12).

Provision is resolved once per case; the stub, the cursor, and the match table are emitted only under bynkc test. A module with no suite emits byte-for-byte unchanged.

ConstructEmits
function type A -> Bthe TS function type (a0: A) => B (an Effect return is Promise via the ordinary lowering)
lambdaa TS arrow — async exactly when its checked type is effectful; written annotations transcribe, omitted ones rely on TS contextual typing in argument position
lambda block bodythe same statement lowering as a function body
named function as a valuethe function’s TS identifier, unchanged
value applicationa TS call
generic functionan erased TS generic (function name<A, B>(…)) — no runtime type-argument dispatch

An adapter’s contract emits like a context’s: each capability becomes a TypeScript interface plus an injection token, and its types emit per §7.3.1 into the adapter’s module (<adapter>.ts). An external provider emits no class — its implementation is the class of the same name that the binding module MUST export, and implements <Interface> against the generated interface is the contract between the two halves, checked by the tsc --strict gate (§8.4).

A capability operation’s own type parameter (v0.235, ADR 0281) emits as a genuine generic interface methoddedup<T>(key: string): Promise<Option<T>> — not an erased/monomorphised one, since the implementing class is hand-authored TypeScript, not compiler-generated. Unlike a generic function (above) or Json.decode[T] (which specialises a runtime codec per call and needs no TS-level generic at all), a capability operation’s T is a pure type-level parameter with no runtime codec to specialise, so nothing at the call site lets tsc infer a return-position-only parameter — the call site therefore names the type argument explicitly too (deps.Cap.op<SomeType>(…)).

The binding module is copied verbatim into the output beside the adapter’s emitted module, so its imports resolve and the gate checks it. Its declared requires dependencies are folded into a generated package.json.

The composition root instantiates an external provider from the binding module’s namespace. A provider with a given clause receives a by-name deps object: each key is the given name, each value the recursively instantiated provider of that capability — a bare name resolving through the provider’s own unit’s flattened consumes (§5.8), so an adapter’s dependency on another adapter pulls that adapter’s binding into the same compose, transitively:

const Jwt = new tokens__binding.JoseJwt({
Secrets: new bynk__binding.SecretsProvider(),
});

Provider selection is per build — a test-scoped stub overrides a local provides, which overrides the adapter default — but instances are per-compose: each consuming context constructs its own.

The first-party bynk adapter — the ambient surface: Clock, Random, Logger, Fetch, Secrets — is injected as a synthetic unit when any unit consumes it, and flows through this same pipeline. It has no binding clause; the toolchain supplies one per platform (bynk-<platform>.ts, selected by --platform, §8.5). Because the contract names canonical provider symbols, the emitted compose is platform-identical — only the imported binding module differs. On the workers target the compose passes the Worker env to the first-party providers that take it (Secrets); on bundle the binding falls back to a globalThis probe of process.env.

A platform adapter (v0.19: bynk.cloudflare, exporting Kv; v0.23 extends it — get/put/putTtl/delete/list) is injected the same way, with one toolchain-supplied binding copied to bynk/cloudflare.binding.ts. putTtl passes { expirationTtl } to the namespace (0051). list(prefix: Option[String]) -> Effect[List[String]] is a binding-side drain (0050): the cursor loops inside the host binding (env.KV.list({ prefix, cursor }) until list_complete, projecting keys[].name) because no Bynk routine can both recurse and hold a capability — the given-on-free-functions gap, recorded for a future increment. The drain is eager and unbounded, normatively: a very large namespace loads every matching key; cursor-paging is deferred until the language can consume it. The runtime KVNamespace interface (§7.4) carries the matching list page shape and put options parameter. Its resources exist only on the Worker env — there is no globalThis path — so its binding reads env.KV explicitly and throws a clear error when the binding is absent. The compiler’s work is derived, not injected: when a deployment unit’s closure reaches the adapter, the Worker’s Env gains a typed KV: KVNamespace field and its wrangler.toml a [[kv_namespaces]] stanza (one fixed KV binding name; the namespace id is a deploy-time placeholder). On the bundle target, composeApp gains an optional env?: unknown parameter — threaded to env-taking providers — only when a platform-native resource is consumed; native-free programs emit the parameterless signature unchanged. Consuming a platform adapter locks the deployment unit to its platform (§5.8).

(v0.20b) The collection types lower to immutable TypeScript shapes:

ConstructEmits
List[T]readonly T[]
Map[K, V]ReadonlyMap<K, V>
[a, b, c]the array literal [a, b, c]
List.empty() / Map.empty()[] as readonly T[] / new Map<K, V>(), with the checked type arguments written out

Kernel operations emit inline — typed IIFEs and spreads, no runtime imports — so a module that never touches collections emits byte-identically to v0.20a. prepend is the spread [x, ...xs]; insert copies (new Map(m).set(k, v)) — the emitted value is never mutated in place. fold, foldEff, forEach, parTraverse, traverseAll, and parTraverseAll emit as a single loop (an IIFE; async for the effectful ones) — iteration is the kernel’s, so no user-visible recursion or stack growth exists. forEach (v0.146) is the for…await analogue of the Query.forEach terminal, awaiting each step in sequence; parTraverse (v0.147) is the await Promise.all(xs.map(f)) analogue of Query.parTraverse, issuing every element’s effect concurrently — both yield Promise<void>. The collect-all pair (v0.148) return the gathered Results: traverseAll awaits each into a typed Result<U, E>[] (const __out: Result<…>[] = []; … __out.push(await f(x))), parTraverseAll is await Promise.all(xs.map(f)) keeping the resolved array (a Result Err is a value, so nothing rejects) — both yield Promise<Result<…>[]>. The short-circuit pair (v0.150) instead thread the Result: traverseTry awaits each and return Err(__r.error) on the first Err, else pushes __r.value, finally return Ok(__out); parTraverseTry Promise.alls first, then scans the resolved Results in input order for the first Err — both yield Promise<Result<U[], E>>. Local mutation inside these loops is permitted; it never escapes.

A do e statement emits as a bare await <e>; — the binder-free form of let _ <- e (which emits const <fresh> = await <e>;), dropping the throwaway binding. A block that ends with no explicit tail (or an if with no else) returns the unit value, which erases to undefined — the same output an explicit () / Effect.pure(()) tail already produced, so no existing emission changes.

At boundaries, a List[T] serialises element-wise as a JSON array; a Map[K, V] serialises as an entries array [[k, v], …] — uniform across String and Int keys (a JSON object could not carry Int keys), and insertion-ordered, normatively. Per-instantiation helpers (serialise_List_<T>, deserialise_Map_<K>_<V>) follow the existing Result/Option pattern: element and entry deserialisation validates structurally, re-validates refined types, and reports StructuralMismatch with an indexed path ($.orders[3].tags[0][1]).

The v0.22a kernel methods lower inline, like the collection kernel — no new runtime imports beyond the Some/None/Ok/Err constructors every module already has:

ConstructEmits
string opsthe TS string method (trim, split, includes, startsWith, …); s.length().length; toUpper/toLowertoUpperCase/toLowerCase
s.replace(a, b)replaceAll(a, b) — replace-all, normatively
s.chars()[...s] — code points, normatively
s.slice(lo, hi)slice(Math.max(0, lo), Math.max(0, hi)) — negatives clamp, no wrap
s.indexOf(sub)a typed IIFE turning -1 into None, else Some(i)
Option/Result combinatorstyped IIFEs branching on .tag; the miss branch returns the narrowed receiver or None
Effect[Result] combinators (v0.152)an async IIFE that awaits the receiver Promise<Result<…>> and rebuilds: mapOk/mapErr branch on .tag and re-wrap the mapped side (Ok(__f(__r.value)) / Err(__f(__r.error))), else return the narrowed receiver; flatMapOk/flatMapErr await __f(…) on the matching tag, else return the receiver — yielding Promise<Result<…>>. No runtime import
numeric abs/min/maxMath.*
x.clamp(lo, hi)Math.min(Math.max(x, lo), hi)
f.isNaN()/f.isFinite()Number.isNaN/Number.isFinite
Int.parse/Float.parsea typed IIFE over Number(s) (full-string), rejecting empty/whitespace input, non-safe-integers (Int) and non-finite values (Float)

Json.encode(v) lowers to JSON.stringify(serialise_<T>(v)) for the value’s checked type; Json.decode[T](s) to a typed IIFE that JSON.parses (a throw becomes a Malformed JsonError), dispatches to deserialise_<T>, and maps a BoundaryError into the uniform kind/path/message record (ADR 0047). The per-type serialise_/deserialise_ helpers and any generic instantiations (deserialise_List_Order, …) are emitted module-locally into each module whose code calls the codec — the same closure machinery as the workers boundary path, deduped against helpers that path already emitted. The codec runtime types (JsonError, JsonValue, BoundaryError) are imported only by modules that use the codec, so non-codec modules emit byte-identically to v0.22a.

§7.3.10 Streams and WebSockets (v0.100, v0.102+)

Section titled “§7.3.10 Streams and WebSockets (v0.100, v0.102+)”

A Stream[T] lowers to a host AsyncIterable<T>, emitted inline as async-generator IIFEs with no runtime import, so non-stream modules stay byte-identical: Stream.of(xs) becomes a generator over the list, map/take wrap it lazily, and the terminal collect() drains it into an array. A streamed HTTP responseStreaming(stream) — lowers to a Response whose body is the stream encoded as a Server-Sent-Events (SSE) byte stream consuming a Stream[String].

A Connection[F] lowers against the runtime Connection<F> interface (§7.4.9): send JSON-encodes a frame, close ends the socket. The from websocket protocol lowers per target:

  • bundle — the on open/on message/on close handlers become callable surface methods (Service.open(conn, …)) taking a TestConnection — a capture-and-inspect channel recording every frame sent — so the service runs under Node with no Durable Object.
  • Workers — the Worker authenticates the upgrade at the edge (the same JWT verifier the HTTP seam uses, reading the Bearer token from the Sec-WebSocket-Protocol subprotocol) and forwards it to the addressed Durable Object, which accepts the socket via the hibernatable-WebSocket API (acceptWebSocket/serializeAttachment/getWebSockets). A held Map[K, Connection] cannot be JSON-persisted (a live socket), so it lives in an in-memory side-table split out of the durable record. parTraverse over the map lowers to await Promise.all(xs.map(f)). Only modules that use streams or WebSockets emit this machinery.

Every emitted project ships a single runtime module that the per-context and per-test modules import. It is the normative contract the emitted code depends on, defined in §7.4 The runtime library.