§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.
§7.1 The model
Section titled “§7.1 The model”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.
§7.2 Targets
Section titled “§7.2 Targets”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 | |
|---|---|---|
| Layout | a flat .ts tree mirroring the source | one Worker directory per context |
| Cross-context call | a direct, in-process call | JSON over a Cloudflare Service Binding, validated at the boundary |
| Agent | backed by an in-process StateRegistry | a Cloudflare Durable Object keyed by the agent key |
| Consumed adapter | in-process, via its binding | in-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 aFloatfield requirestypeof v === "number" && Number.isFinite(v), with no integer check (decimals are the point).JSON.parse("1e999")yieldsInfinity, which is rejected as aStructuralMismatchexpecting afinite number— never admitted from the wire.serialise_of aFloatfield throws on a non-finite value (a contract violation):JSON.stringify(NaN)would otherwise silently producenulland break the round-trip.- v0.22b (ADR 0049): a bare
Intfield additionally requiresNumber.isInteger— at record/nested fields and workers handler params. A previously-accepted{qty: 1.5}now fails with aStructuralMismatchexpecting aninteger; withFloatin the language there is no excuse for fractionalInts, and codec and.ofagree (as 0040 aligned them forFloat).
§7.3 Construct emission
Section titled “§7.3 Construct emission”§7.3.1 Types
Section titled “§7.3.1 Types”| Construct | Emits |
|---|---|
alias (type Id = Int) | a branded base type with a .of constructor |
| refined type | a branded base with .of (a runtime predicate check returning Result) |
| opaque type | a branded base with .of and .unsafe constructors; no structural access to the representation |
| record | an interface with readonly fields, constructed from an object literal |
| sum / enum | a 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).
§7.3.2 Expressions
Section titled “§7.3.2 Expressions”| Construct | Emits |
|---|---|
if … else … | a conditional / if |
match | a switch on .tag, payload fields bound as const |
| admitted literal | an inline brand cast (literal as T) (§6.4; ADR 0182) |
| float literal | the source lexeme verbatim (1e10 does not normalise) |
| interpolated string | a template literal: chunks as escaped text (backslash, backtick, and $ escaped), each \(e) hole as ${String(<e>)} (ADR 0075) |
Int / Int | Math.trunc(a / b) — truncating, unchanged |
Float / Float | a / b — true division (v0.21, operand-typed) |
| numeric kernel | i.toFloat() → the receiver (erased identity); f.round()/floor/ceil/truncate → Math.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.
§7.3.3 Agents
Section titled “§7.3.3 Agents”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"); }}§7.3.4 HTTP services
Section titled “§7.3.4 HTTP services”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.
§7.3.4c The contract seam (v0.177)
Section titled “§7.3.4c The contract seam (v0.177)”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.
§7.3.5 Tests
Section titled “§7.3.5 Tests”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).
§7.3.5b Function contracts (v0.115)
Section titled “§7.3.5b Function contracts (v0.115)”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.
§7.3.5c Observation (v0.117)
Section titled “§7.3.5c Observation (v0.117)”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.
§7.3.5d Tiers and stub (v0.118)
Section titled “§7.3.5d Tiers and stub (v0.118)”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.
unitandintegrationemit in process with the real provider for any un-overridden seam (fullunitauto-stubbing is a named follow-on);systemstands 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 transitiveconsumesclosure. - A
stuboverride 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’swith), first match wins:returns <value>lowers to a constant return of the lowered value;failslowers to a thrown/Errcapability 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.
§7.3.5a Functions as values (v0.20a)
Section titled “§7.3.5a Functions as values (v0.20a)”| Construct | Emits |
|---|---|
function type A -> B | the TS function type (a0: A) => B (an Effect return is Promise via the ordinary lowering) |
| lambda | a 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 body | the same statement lowering as a function body |
| named function as a value | the function’s TS identifier, unchanged |
| value application | a TS call |
| generic function | an erased TS generic (function name<A, B>(…)) — no runtime type-argument dispatch |
§7.3.6 Adapters
Section titled “§7.3.6 Adapters”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 method — dedup<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).
§7.3.7 Collections
Section titled “§7.3.7 Collections”(v0.20b) The collection types lower to immutable TypeScript shapes:
| Construct | Emits |
|---|---|
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]).
§7.3.8 The stdlib kernels (v0.22a)
Section titled “§7.3.8 The stdlib kernels (v0.22a)”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:
| Construct | Emits |
|---|---|
| string ops | the TS string method (trim, split, includes, startsWith, …); s.length() → .length; toUpper/toLower → toUpperCase/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 combinators | typed 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/max | Math.* |
x.clamp(lo, hi) | Math.min(Math.max(x, lo), hi) |
f.isNaN()/f.isFinite() | Number.isNaN/Number.isFinite |
Int.parse/Float.parse | a typed IIFE over Number(s) (full-string), rejecting empty/whitespace input, non-safe-integers (Int) and non-finite values (Float) |
§7.3.9 The typed JSON codec (v0.22b)
Section titled “§7.3.9 The typed JSON codec (v0.22b)”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 response — Streaming(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 closehandlers become callable surface methods (Service.open(conn, …)) taking aTestConnection— 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-Protocolsubprotocol) and forwards it to the addressed Durable Object, which accepts the socket via the hibernatable-WebSocket API (acceptWebSocket/serializeAttachment/getWebSockets). A heldMap[K, Connection]cannot be JSON-persisted (a live socket), so it lives in an in-memory side-table split out of the durable record.parTraverseover the map lowers toawait Promise.all(xs.map(f)). Only modules that use streams or WebSockets emit this machinery.
§7.4 The runtime library
Section titled “§7.4 The runtime library”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.