Skip to main content

bynk_std/
lib.rs

1//! The Bynk standard library's first-party sources, as data.
2//!
3//! The `bynk` conformance surface, the Bynk-written commons (`bynk.list`,
4//! `bynk.map`, `bynk.string`, `bynk.locale`, `bynk.locale.types`) and the
5//! `bynk.cloudflare` platform adapter ship with the compiler rather than being
6//! authored by the user. This crate holds their `.bynk` sources, their
7//! per-platform TypeScript bindings and the metadata the toolchain reads about
8//! them, embedded with `include_str!`. It holds no logic: `bynk-check` parses and
9//! injects these sources when a project `uses` or `consumes` them, and provides
10//! each binding for the selected [`Platform`]. The `bynk` root namespace is
11//! reserved (`bynk.namespace.reserved`) so user code can never collide with it.
12//!
13//! The sources live under `lib/`: the portable layers in `lib/core/` (with the
14//! `bynk` surface's bindings in `lib/core/bindings/`) and the platform adapter in
15//! `lib/cloudflare/`. Nothing in `lib/core/` depends on `lib/cloudflare/`, so the
16//! platform layer can be split out later without renaming a unit.
17
18/// The deploy platform — a selection axis distinct from the `--target
19/// {bundle,workers}` emit mode (§6.2). It chooses which `bynk-<platform>.ts`
20/// binding is linked for the `bynk` surface. v0.17 shipped `cloudflare`;
21/// v0.18 adds `node`, making the axis observable (and giving v0.19's
22/// platform-lock enforcement a second platform to fire against).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
24pub enum Platform {
25    #[default]
26    Cloudflare,
27    Node,
28    /// The browser (in-browser track, slice 1 — the REPL/playground host). A
29    /// Tier-3 binding exposing the `bynk` surface over Web APIs, composed with
30    /// `BuildTarget::Bundle` only (a browser cannot do the Workers wire model).
31    Browser,
32}
33
34impl Platform {
35    /// The output filename of this platform's `bynk` binding module.
36    pub fn bynk_binding_filename(self) -> &'static str {
37        match self {
38            Platform::Cloudflare => "bynk-cloudflare.ts",
39            Platform::Node => "bynk-node.ts",
40            Platform::Browser => "bynk-browser.ts",
41        }
42    }
43
44    /// The TypeScript source of this platform's `bynk` binding.
45    pub fn bynk_binding_source(self) -> &'static str {
46        match self {
47            Platform::Cloudflare => BYNK_CLOUDFLARE_BINDING,
48            Platform::Node => BYNK_NODE_BINDING,
49            Platform::Browser => BYNK_BROWSER_BINDING,
50        }
51    }
52
53    /// The platform's stable name (for the `--platform` flag and diagnostics).
54    pub fn as_str(self) -> &'static str {
55        match self {
56            Platform::Cloudflare => "cloudflare",
57            Platform::Node => "node",
58            Platform::Browser => "browser",
59        }
60    }
61}
62
63/// The unit name of the reserved first-party surface adapter.
64pub const BYNK_UNIT: &str = "bynk";
65
66/// The capabilities the env-free `bynk` surface exports (its `exports
67/// capability { … }` line in `bynk.bynk`). First-party symbols live on
68/// synthetic files and are excluded from the binding index, so the
69/// unknown-name quick-fix (#852) offering `consumes bynk { … }` needs this
70/// explicit list. Kept in sync with the adapter source by
71/// `bynk_surface_capabilities_match_the_adapter`.
72pub const BYNK_SURFACE_CAPABILITIES: &[&str] = &[
73    "Clock",
74    "Random",
75    "Logger",
76    "Fetch",
77    "Secrets",
78    "Locale",
79    "Idempotency",
80    "Events",
81];
82
83/// The unit name of the first-party Cloudflare platform adapter (v0.19,
84/// decision 0026): inside the reserved `bynk.*` prefix, so no separate
85/// reservation rule is needed. The surface unit `bynk` stays the portability
86/// marker; `bynk.<platform>` units are the platform-locked ones.
87pub const CLOUDFLARE_UNIT: &str = "bynk.cloudflare";
88
89/// The fixed Worker binding name for the Kv namespace (decision C1, v0.19):
90/// one namespace, one `[[kv_namespaces]]` stanza, one `env.KV` field.
91pub const KV_BINDING_NAME: &str = "KV";
92
93/// v0.18 decision 0021 / v0.19 decision 0025: which first-party provider
94/// classes take the Worker `env` as a constructor argument, keyed by
95/// (unit, provider class). `SecretsProvider` accepts an optional env with a
96/// `globalThis` probe fallback; `WorkersKv` *requires* env on use — KV
97/// namespaces exist only on the Worker `env`, never on `globalThis`.
98///
99/// A view over [`UNITS`]: each entry's [`Unit::env_providers`].
100pub fn provider_takes_env(unit: &str, provider: &str) -> bool {
101    self::unit(unit).is_some_and(|u| u.env_providers.contains(&provider))
102}
103
104/// v0.19 decision 0024: which first-party units are **platform-native** —
105/// consuming one locks its deployment unit to the returned platform. This is
106/// the metadata that drives effective-platform computation, `env` resource
107/// typing, and `wrangler.toml` stanza derivation; no user-facing marker
108/// syntax exists (additive later, when third-party platform adapters become
109/// a goal).
110///
111/// A view over [`UNITS`]: each entry's [`Unit::platform`].
112pub fn platform_of(unit: &str) -> Option<Platform> {
113    self::unit(unit).and_then(|u| u.platform)
114}
115
116/// The unit names of the first-party collection commons (v0.20b): the
117/// Bynk-written combinator stdlib over the built-in `List`/`Map` kernel.
118/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported.
119pub const LIST_UNIT: &str = "bynk.list";
120pub const MAP_UNIT: &str = "bynk.map";
121
122/// `bynk.list` — combinators over the `List` kernel (`fold`, `prepend`,
123/// `length`, `get`, `foldEff`), written in ordinary Bynk (decision 0034):
124/// the first real consumer of v0.20a generics, lambdas, and effectful
125/// traversal. Order-preserving combinators build with `fold` + `prepend`
126/// and a final `reverse` — O(n) builds, never `append` (which would be
127/// O(n²) over the array lowering).
128pub const BYNK_LIST_SRC: &str = include_str!("../lib/core/bynk.list.bynk");
129
130/// `bynk.map` — combinators over the `Map` kernel (`empty`, `insert`, `get`,
131/// `keys`, `length`). `fromList` is deliberately absent: Bynk has no pair
132/// type to spell a `List[(K, V)]` with, so map construction is `Map.empty()`
133/// + `insert` (revisit with tuples or generic records).
134pub const BYNK_MAP_SRC: &str = include_str!("../lib/core/bynk.map.bynk");
135
136/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported.
137pub const STRING_UNIT: &str = "bynk.string";
138
139/// `bynk.string` — Bynk-written helpers over the v0.22a string kernel
140/// (`concat`, the `List` `fold`, and the `Option` kernel methods). The
141/// kernel itself is compiler built-in (ADR 0046); only derived helpers
142/// live here. `join` folds to `Option[String]` so empty-string *elements*
143/// are joined faithfully (a bare `""` accumulator could not tell "nothing
144/// yet" from "first element was empty").
145pub const BYNK_STRING_SRC: &str = include_str!("../lib/core/bynk.string.bynk");
146
147/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported.
148pub const URL_UNIT: &str = "bynk.url";
149
150/// `bynk.url` (standard-library K2, #2594) — a `Url` record with RFC 3986
151/// `parse`/`format` and query-string helpers, written in Bynk over the
152/// `String` kernel. Only percent-encoding is kernel (`percentEncode`,
153/// `percentDecode`): it needs a string's UTF-8 bytes. `Url` is a record here,
154/// not a base type, because a base type's name is a lexer token, and `Url`
155/// as a keyword would break every user `type Url`.
156pub const BYNK_URL_SRC: &str = include_str!("../lib/core/bynk.url.bynk");
157
158pub const SET_UNIT: &str = "bynk.set";
159
160/// `bynk.set` (standard-library K6a, #2596) — Bynk-written combinators over
161/// the built-in `Set` kernel (`length`, `contains`, `insert`, `remove`,
162/// `toList`, `Set.of`): `union`, `intersect`, `difference`, `isSubsetOf`,
163/// `filter`, `map`.
164pub const BYNK_SET_SRC: &str = include_str!("../lib/core/bynk.set.bynk");
165
166/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported by
167/// application code (a message-bundle commons, ADR 0272) — never directly by
168/// the `bynk` adapter itself any more (see [`LOCALE_TYPES_UNIT`]).
169pub const LOCALE_UNIT: &str = "bynk.locale";
170
171/// `bynk.locale` — the bundle-free `render`/`renderArg` helpers and the
172/// `message`/`withText`/`withWhole`/`withNum`/`withMoment` builder API
173/// (slice 1: `render` formats `code` plus a deterministic sorted-by-key
174/// rendering of `params`; no lookup, no locale-dependent behaviour). `uses
175/// bynk.locale.types` for the types its own signatures name.
176pub const BYNK_LOCALE_SRC: &str = include_str!("../lib/core/bynk.locale.bynk");
177
178/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported. Also
179/// `uses`-imported by the `bynk` adapter itself, to give `capability Locale`'s
180/// `current() -> Effect[LocaleTag]` a type to name (an adapter may `uses` a
181/// commons; a commons cannot in turn reference an adapter). Split out of
182/// `bynk.locale` (locale-negotiation-slice-2 follow-up, #886) specifically so
183/// a context can reach `LocaleTag` without also pulling in `bynk.locale`'s
184/// own `render` — which previously collided with a message-bundle commons's
185/// own synthesised `render` (ADR 0272) the moment a context needed both.
186pub const LOCALE_TYPES_UNIT: &str = "bynk.locale.types";
187
188/// `bynk.locale.types` — `LocaleTag`, `MessageArg`, `Message`: a
189/// dependency-free leaf (no `uses` of its own) so any consumer, including
190/// the `bynk` adapter itself, can reach these types without also reaching
191/// `bynk.locale`'s value-level API.
192pub const BYNK_LOCALE_TYPES_SRC: &str = include_str!("../lib/core/bynk.locale.types.bynk");
193
194/// The reserved `bynk` conformance-surface adapter (env-free core). It has no
195/// `binding` clause — the toolchain supplies one per platform (see
196/// [`Platform::bynk_binding_source`]).
197pub const BYNK_ADAPTER_SRC: &str = include_str!("../lib/core/bynk.bynk");
198
199/// The Cloudflare binding for the `bynk` surface. Implements the canonical
200/// provider symbols against the platform host API. The refined `Uuid` is built
201/// through its emitted validating `.of` constructor (§4.4), treating the
202/// unreachable `Err` as a bug rather than trusting the value away.
203const BYNK_CLOUDFLARE_BINDING: &str = include_str!("../lib/core/bindings/bynk-cloudflare.ts");
204
205/// The Node (≥ the toolchain's `NODE_MAJOR_FLOOR`, in `bynk-emit`) binding for
206/// the `bynk` surface (v0.18). Deliberately
207/// near-identical to the cloudflare binding: `Date.now`, the global
208/// `crypto`/`fetch`, and `console` are the same host API on both runtimes —
209/// which is exactly the ambient-surface portability claim (spec §4.2). The
210/// `SecretsProvider` reads `process.env` through the same `globalThis` probe
211/// (never bare `process`, which would demand @types/node at the tsc gate).
212const BYNK_NODE_BINDING: &str = include_str!("../lib/core/bindings/bynk-node.ts");
213
214/// The Browser binding for the `bynk` surface (in-browser track, slice 1). Tier-3:
215/// the same capability surface over Web APIs. `Clock`/`Random`/`Logger` are
216/// byte-identical to the Node binding (`Date.now`, Web Crypto `crypto.randomUUID`,
217/// `console` — all Web standards); the two substitutions are the playground safety
218/// boundary — `Fetch` is **withheld** (throws; no outbound egress from the
219/// playground origin) and `Secrets` is **unavailable** (throws; a browser has no
220/// secret store), both failing loudly rather than silently degrading.
221const BYNK_BROWSER_BINDING: &str = include_str!("../lib/core/bindings/bynk-browser.ts");
222
223/// The first-party Cloudflare platform adapter (v0.19): the platform's real
224/// infrastructure capabilities, as they are — no portable intersection
225/// (decision 0016). The v0.19 surface was the minimal, collection-free `Kv`
226/// (decision 0023); v0.23 adds the `list` drain and `putTtl` (0050/0051);
227/// structured values are v0.22-codec composition, and `Queue` remains its
228/// own future increment. Like the `bynk` surface it has no `binding`
229/// clause — the toolchain supplies the binding.
230pub const CLOUDFLARE_ADAPTER_SRC: &str = include_str!("../lib/cloudflare/bynk.cloudflare.bynk");
231
232/// Every first-party `.bynk` source, paired with its unit name — the single
233/// list every consumer iterates: hover (`describe_firstparty_symbol`),
234/// completion (`EMBEDDED_UNITS`), and the parse guard
235/// (`every_first_party_source_parses`). Before this list each consumer kept its
236/// own hand-maintained copy, and `bynk.locale`/`bynk.locale.types` were added to
237/// none of them — so the whole locale surface hovered as nothing and was absent
238/// from completion (#901).
239///
240/// A view over [`UNITS`], in its order, which is observable: hover takes the
241/// first source declaring a name, and completion lists units in this order. The
242/// `firstparty_sources_cover_every_src_const` drift guard
243/// (`bynkc/tests/firstparty_sources.rs`) fails if a `*_SRC` const is left out of
244/// [`UNITS`].
245pub const FIRSTPARTY_SOURCES: &[(&str, &str)] = &SOURCES;
246
247/// [`FIRSTPARTY_SOURCES`]' backing array, built from [`UNITS`] at compile time.
248const SOURCES: [(&str, &str); UNITS.len()] = {
249    let mut out = [("", ""); UNITS.len()];
250    let mut i = 0;
251    while i < UNITS.len() {
252        out[i] = (UNITS[i].name, UNITS[i].source);
253        i += 1;
254    }
255    out
256};
257
258/// The output path of the Cloudflare platform adapter's binding module,
259/// beside the adapter's emitted `bynk/cloudflare.ts` (distinct from the
260/// `bynk` *surface*'s per-platform `bynk-cloudflare.ts`).
261pub const CLOUDFLARE_BINDING_FILENAME: &str = "bynk/cloudflare.binding.ts";
262
263/// The Cloudflare platform adapter's binding. `WorkersKv` reads the Worker
264/// `env` explicitly (decision 0025): KV namespaces exist only on `env` —
265/// there is no `globalThis` path — so a missing binding is a clear runtime
266/// error rather than a silent fallback.
267const CLOUDFLARE_BINDING: &str = include_str!("../lib/cloudflare/cloudflare.binding.ts");
268
269/// The toolchain-supplied binding for the Cloudflare platform adapter.
270pub fn cloudflare_binding_source() -> &'static str {
271    CLOUDFLARE_BINDING
272}
273
274/// The unit name of the first-party cryptography adapter (standard-library K4,
275/// D5/D6): a flat sibling of `bynk`, platform-free, with a binding per platform.
276pub const CRYPTO_UNIT: &str = "bynk.crypto";
277
278/// `bynk.crypto` — digests, HMAC, constant-time comparison, and signing and
279/// verification with imported keys, as `Effect`s over Web Crypto.
280pub const BYNK_CRYPTO_SRC: &str = include_str!("../lib/crypto/bynk.crypto.bynk");
281
282/// The Cloudflare Workers binding for `bynk.crypto`. The three differ only where
283/// the platforms do: the constant-time comparison primitive.
284const CRYPTO_CLOUDFLARE_BINDING: &str = include_str!("../lib/crypto/crypto-cloudflare.binding.ts");
285
286/// The Node binding for `bynk.crypto`.
287const CRYPTO_NODE_BINDING: &str = include_str!("../lib/crypto/crypto-node.binding.ts");
288
289/// The browser binding for `bynk.crypto`. Web Crypto exists only in a secure
290/// context (R14.5): outside one it throws, loudly, rather than degrade.
291const CRYPTO_BROWSER_BINDING: &str = include_str!("../lib/crypto/crypto-browser.binding.ts");
292
293/// The opaque key types `bynk.crypto` exports. A value of one holds a
294/// non-extractable platform key, so it is never stored, persisted, logged or
295/// sent (`bynk.types.key_at_boundary`), as a `Stream` is not. The checker finds
296/// them by the unit's reserved span file, so a user type of the same name is not
297/// mistaken for one.
298pub const CRYPTO_KEY_TYPES: &[&str] = &["SigningKey", "VerifyKey"];
299
300/// What a first-party unit is, and so how a project brings it in: an adapter is
301/// injected when a unit `consumes` it, a commons when a unit `uses` it. Local to
302/// this crate, which has no dependencies; `bynk-check` maps it onto its own
303/// unit kind.
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum UnitKind {
306    Adapter,
307    Commons,
308}
309
310/// The TypeScript binding the toolchain supplies for a first-party unit.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum Binding {
313    /// A commons: Bynk-written, so nothing to bind.
314    None,
315    /// One binding per [`Platform`] ([`Platform::bynk_binding_filename`] and
316    /// [`Platform::bynk_binding_source`]): the portable `bynk` surface.
317    PerPlatform,
318    /// A single binding, for a platform-native adapter that runs only on its own
319    /// platform (the platform lock rejects every other `--platform`).
320    Fixed {
321        filename: &'static str,
322        source: &'static str,
323    },
324    /// One binding file per [`Platform`], each with its own name and source, for a
325    /// platform-free unit whose binding differs where the platforms do
326    /// (`bynk.crypto`). [`Binding::PerPlatform`] stays the `bynk` surface's.
327    PerPlatformFiles {
328        cloudflare: (&'static str, &'static str),
329        node: (&'static str, &'static str),
330        browser: (&'static str, &'static str),
331    },
332}
333
334/// One first-party unit, as data (standard-library track, S1 #1945). Adding a
335/// unit to the standard library is an entry in [`UNITS`] plus its files; the
336/// checker's injection reads this table and needs no change.
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub struct Unit {
339    /// The unit's dotted name, inside the reserved `bynk.*` prefix.
340    pub name: &'static str,
341    /// Whether a project `consumes` it (an adapter) or `uses` it (a commons).
342    pub kind: UnitKind,
343    /// The synthetic path the injected unit is parsed under. Diagnostics and
344    /// emission name the unit by it, so it never changes.
345    pub identity_path: &'static str,
346    /// The unit's `.bynk` source.
347    pub source: &'static str,
348    /// The binding the toolchain supplies for it.
349    pub binding: Binding,
350    /// Its permanent `ExprId` block: the checker parses it at
351    /// `FIRSTPARTY_ID_BASE + id_block * FIRSTPARTY_ID_BLOCK`, and injects units
352    /// in ascending `id_block` order. A block is never reused or moved, so ids
353    /// and injection order stay stable as units are added; a new unit takes the
354    /// next free block.
355    pub id_block: u32,
356    /// The platform that consuming it locks a deployment unit to (decision
357    /// 0024), if it is platform-native.
358    pub platform: Option<Platform>,
359    /// The provider classes whose constructor takes the Worker `env` (decisions
360    /// 0021/0025).
361    pub env_providers: &'static [&'static str],
362    /// The unit's stability (track D2, S3 #1963): a stable item is under the
363    /// 1.0 promise, an experimental one carries none and needs an explicit
364    /// `[unstable] allow` to use. Required, so no entry is added without a
365    /// choice.
366    pub stability: Stability,
367    /// Items inside a stable unit that are experimental, as paths relative to
368    /// the unit: an exported item (`"Kv"`) or a capability operation
369    /// (`"Kv.list"`). An experimental item makes its operations experimental.
370    pub experimental: &'static [&'static str],
371    /// The exported enums that may gain variants in a minor release, so a
372    /// `match` covering one needs a wildcard (track §11 Q5).
373    pub extensible: &'static [&'static str],
374    /// The capabilities a context may declare named instances of with
375    /// `capability Sessions = Kv` (S4a, #1966): each its own platform resource.
376    /// A capability not listed (Workers AI's single `[ai]` binding, say) has no
377    /// instances.
378    pub instantiable: &'static [Instantiable],
379}
380
381/// A capability a context may declare instances of (S4a, #1966, track D3).
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub struct Instantiable {
384    /// The capability's name in its unit (`Kv`).
385    pub capability: &'static str,
386    /// The prefix of each instance's Worker binding name: `KV` makes
387    /// `capability Sessions = Kv` bind as `KV_SESSIONS`.
388    pub binding_prefix: &'static str,
389    /// The creation options (`@name(…)`) an instance may carry, each with the
390    /// one literal its value is (standard-library P3, #2342: `@worker("…")`
391    /// takes a string, `@limit(100)` an `Int`, `@period(60.seconds)` a
392    /// `Duration`); any other is refused. Empty when the kind takes none yet.
393    pub options: &'static [(&'static str, OptionKind)],
394    /// The options every instance must carry (interop I5, #1972): a
395    /// `Service` names the Worker it reaches, so it has no default. A subset
396    /// of [`Instantiable::options`].
397    pub required: &'static [&'static str],
398    /// Whether the base capability is usable bare, through `given Kv`, as
399    /// well as through instances. `false` for a kind whose every use needs an
400    /// option: a bare `Service` would name no Worker.
401    pub bare: bool,
402}
403
404impl Instantiable {
405    /// The kind of literal option `name` takes, or `None` when it is not one
406    /// of this kind's options.
407    pub fn option_kind(&self, name: &str) -> Option<OptionKind> {
408        self.options
409            .iter()
410            .find(|(o, _)| *o == name)
411            .map(|(_, k)| *k)
412    }
413
414    /// The names of this kind's options, in declaration order.
415    pub fn option_names(&self) -> impl Iterator<Item = &'static str> + '_ {
416        self.options.iter().map(|(o, _)| *o)
417    }
418}
419
420/// The literal an instance option takes (standard-library P3, #2342).
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum OptionKind {
423    /// A string literal: `@worker("legacy-billing")`.
424    Str,
425    /// A positive `Int` literal: `@limit(100)`.
426    PositiveInt,
427    /// A `Duration` literal: `@period(60.seconds)`.
428    Duration,
429}
430
431/// The Worker binding prefix of a `Service` instance (interop I5, #1972):
432/// `capability Billing = Service @worker("legacy-billing")` binds as
433/// `SERVICE_BILLING`, a `[[services]]` entry naming `legacy-billing`.
434pub const SERVICE_BINDING_PREFIX: &str = "SERVICE";
435
436/// The option of a `Service` instance that names the Worker it reaches.
437/// `worker`, not D4's `service`: `service` is a keyword, so `@service(…)`
438/// does not parse, and widening the annotation grammar is language work
439/// (ADR 0023).
440pub const SERVICE_WORKER_OPTION: &str = "worker";
441
442/// The Worker binding prefix of an `R2` instance (standard-library P5,
443/// #2323): `capability Archives = R2` binds as `R2_ARCHIVES`, an
444/// `[[r2_buckets]]` entry.
445pub const R2_BINDING_PREFIX: &str = "R2";
446
447/// The one creation option of an `R2` instance: the bucket's jurisdiction.
448pub const R2_JURISDICTION_OPTION: &str = "jurisdiction";
449
450/// The jurisdictions an `R2` bucket may be pinned to, as `@jurisdiction(…)`
451/// takes them and `wrangler r2 bucket create --jurisdiction` accepts them.
452pub const R2_JURISDICTIONS: &[&str] = &["eu", "fedramp"];
453
454/// The Worker binding prefix of a `RateLimit` instance (standard-library P3,
455/// #2342): `capability LoginLimit = RateLimit @limit(100) @period(60.seconds)`
456/// binds as `RATELIMIT_LOGIN_LIMIT`, a `[[ratelimits]]` entry.
457pub const RATELIMIT_BINDING_PREFIX: &str = "RATELIMIT";
458
459/// A `RateLimit` instance's count: the calls per key allowed in one period.
460pub const RATELIMIT_LIMIT_OPTION: &str = "limit";
461
462/// A `RateLimit` instance's window.
463pub const RATELIMIT_PERIOD_OPTION: &str = "period";
464
465/// The windows the Workers Rate Limiting binding offers, in seconds.
466pub const RATELIMIT_PERIODS: &[i64] = &[10, 60];
467
468/// The Worker binding prefix of a `Queue` instance (standard-library P2,
469/// #2578): `capability OrderEvents = Queue @name("order-events")` binds as
470/// `QUEUE_ORDER_EVENTS`, a `[[queues.producers]]` entry.
471pub const QUEUE_BINDING_PREFIX: &str = "QUEUE";
472
473/// The option of a `Queue` instance that names the queue it sends to.
474/// `name`, not the track's `queue`: `queue` is a keyword, so `@queue(…)` does
475/// not parse (as `Service`'s `@worker`, ADR 0023).
476pub const QUEUE_NAME_OPTION: &str = "name";
477
478/// The Worker binding prefix of an `Analytics` instance (standard-library
479/// P3b, #2582): `capability Signups = Analytics` binds as
480/// `ANALYTICS_SIGNUPS`, an `[[analytics_engine_datasets]]` entry. It takes no
481/// options: `bynk deploy` derives the dataset's name (track A12).
482pub const ANALYTICS_BINDING_PREFIX: &str = "ANALYTICS";
483
484/// The Worker binding prefix of a `D1` instance (standard-library P4a,
485/// #2602): `capability Orders = D1` binds as `D1_ORDERS`, a
486/// `[[d1_databases]]` entry whose `database_id` `bynk deploy` mints.
487pub const D1_BINDING_PREFIX: &str = "D1";
488
489/// The prefix an `EdgeCache` instance's binding name would carry (first-party
490/// registry L7, #2414). Workers Cache is a Worker-wide `[cache]` setting with
491/// no `env` binding, so the name is never emitted: the instance is built from
492/// the entry's `ctx` instead.
493pub const EDGE_CACHE_BINDING_PREFIX: &str = "EDGE_CACHE";
494
495/// Whether a first-party item is under the 1.0 promise (track D2).
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub enum Stability {
498    /// Under the promise: a stable L0/L1 item changes only at a major release; a
499    /// stable L2/L3 item may gain operations and, if extensible, variants.
500    Stable,
501    /// No promise: it may change or disappear in any release, and using it needs
502    /// an `[unstable] allow` entry in `bynk.toml`.
503    Experimental,
504}
505
506impl Unit {
507    /// The binding module to emit for this unit on `platform`, as
508    /// `(output path, source)`, or `None` for a commons.
509    pub fn binding_for(&self, platform: Platform) -> Option<(&'static str, &'static str)> {
510        match self.binding {
511            Binding::None => None,
512            Binding::PerPlatform => Some((
513                platform.bynk_binding_filename(),
514                platform.bynk_binding_source(),
515            )),
516            Binding::Fixed { filename, source } => Some((filename, source)),
517            Binding::PerPlatformFiles {
518                cloudflare,
519                node,
520                browser,
521            } => Some(match platform {
522                Platform::Cloudflare => cloudflare,
523                Platform::Node => node,
524                Platform::Browser => browser,
525            }),
526        }
527    }
528}
529
530/// The standard library's first-party units. Dependencies between them are not
531/// listed here: they are the `uses`/`consumes` lines of each unit's own source,
532/// which the checker follows when it injects. The table's order is
533/// [`FIRSTPARTY_SOURCES`]' order; injection follows [`Unit::id_block`].
534pub const UNITS: &[Unit] = &[
535    Unit {
536        name: BYNK_UNIT,
537        kind: UnitKind::Adapter,
538        identity_path: "bynk.bynk",
539        source: BYNK_ADAPTER_SRC,
540        binding: Binding::PerPlatform,
541        id_block: 0,
542        platform: None,
543        env_providers: &["SecretsProvider"],
544        stability: Stability::Stable,
545        // K5a (#2586), K5b (#2604), K5c (#2627): the new operations, until a
546        // service has used them.
547        experimental: &[
548            "Fetch.sendWith",
549            "Fetch.receiveBytes",
550            "Fetch.sendBytes",
551            "Fetch.receiveStream",
552            "Fetch.sendStream",
553            "Logger.log",
554        ],
555        extensible: &["FetchError", "Method", "Redirect"],
556        instantiable: &[],
557    },
558    Unit {
559        name: CLOUDFLARE_UNIT,
560        kind: UnitKind::Adapter,
561        identity_path: "bynk/cloudflare.bynk",
562        source: CLOUDFLARE_ADAPTER_SRC,
563        binding: Binding::Fixed {
564            filename: CLOUDFLARE_BINDING_FILENAME,
565            source: CLOUDFLARE_BINDING,
566        },
567        id_block: 1,
568        platform: Some(Platform::Cloudflare),
569        env_providers: &[
570            "WorkersKv",
571            "WorkersService",
572            "WorkersR2",
573            "WorkersRateLimit",
574            "WorkersQueue",
575            "WorkersAnalytics",
576            "WorkersD1",
577        ],
578        stability: Stability::Stable,
579        // Interop I5 (#1972): a Service Binding to a non-Bynk Worker is new
580        // and its surface is untyped, so it lands outside the 1.0 promise.
581        // P5 (#2323): R2 ships experimental until the first-party registry has
582        // run on it (#2308 R6); its conditions are extensible meanwhile.
583        // P3 (#2342): RateLimit too, until the registry has run on it.
584        // L7 (#2414): EdgeCache likewise.
585        // P2 (#2578): Queue too, until a first-party service has sent on it.
586        // P3b (#2582): Analytics too, until a first-party service writes to it.
587        // P1 (#2590): the new operations of the stable `Kv`, until a service
588        // has used them.
589        // D1 shipped experimental (P4a, #2602) and graduated in P4d (#2652)
590        // once its migrations and call-site shape were settled (P4b, P4c),
591        // by its own gate rather than the registry's.
592        experimental: &[
593            "Service",
594            "R2",
595            "RateLimit",
596            "EdgeCache",
597            "Queue",
598            "Analytics",
599            "Kv.getWith",
600            "Kv.putWith",
601            "Kv.getBytes",
602            "Kv.putBytes",
603            "Kv.listPage",
604        ],
605        extensible: &["R2Condition", "KvExpiration", "SqlError"],
606        instantiable: &[
607            Instantiable {
608                capability: "Kv",
609                binding_prefix: KV_BINDING_NAME,
610                options: &[],
611                required: &[],
612                bare: true,
613            },
614            Instantiable {
615                capability: "Service",
616                binding_prefix: SERVICE_BINDING_PREFIX,
617                options: &[(SERVICE_WORKER_OPTION, OptionKind::Str)],
618                required: &[SERVICE_WORKER_OPTION],
619                bare: false,
620            },
621            Instantiable {
622                capability: "R2",
623                binding_prefix: R2_BINDING_PREFIX,
624                options: &[(R2_JURISDICTION_OPTION, OptionKind::Str)],
625                required: &[],
626                bare: false,
627            },
628            Instantiable {
629                capability: "RateLimit",
630                binding_prefix: RATELIMIT_BINDING_PREFIX,
631                options: &[
632                    (RATELIMIT_LIMIT_OPTION, OptionKind::PositiveInt),
633                    (RATELIMIT_PERIOD_OPTION, OptionKind::Duration),
634                ],
635                required: &[RATELIMIT_LIMIT_OPTION, RATELIMIT_PERIOD_OPTION],
636                bare: false,
637            },
638            Instantiable {
639                capability: "Queue",
640                binding_prefix: QUEUE_BINDING_PREFIX,
641                options: &[(QUEUE_NAME_OPTION, OptionKind::Str)],
642                required: &[QUEUE_NAME_OPTION],
643                bare: false,
644            },
645            Instantiable {
646                capability: "Analytics",
647                binding_prefix: ANALYTICS_BINDING_PREFIX,
648                options: &[],
649                required: &[],
650                bare: false,
651            },
652            Instantiable {
653                capability: "D1",
654                binding_prefix: D1_BINDING_PREFIX,
655                options: &[],
656                required: &[],
657                bare: false,
658            },
659            Instantiable {
660                capability: "EdgeCache",
661                binding_prefix: EDGE_CACHE_BINDING_PREFIX,
662                options: &[],
663                required: &[],
664                bare: false,
665            },
666        ],
667    },
668    Unit {
669        name: LIST_UNIT,
670        kind: UnitKind::Commons,
671        identity_path: "bynk/list.bynk",
672        source: BYNK_LIST_SRC,
673        binding: Binding::None,
674        id_block: 3,
675        platform: None,
676        env_providers: &[],
677        stability: Stability::Stable,
678        experimental: &[],
679        extensible: &[],
680        instantiable: &[],
681    },
682    Unit {
683        name: MAP_UNIT,
684        kind: UnitKind::Commons,
685        identity_path: "bynk/map.bynk",
686        source: BYNK_MAP_SRC,
687        binding: Binding::None,
688        id_block: 2,
689        platform: None,
690        env_providers: &[],
691        stability: Stability::Stable,
692        experimental: &[],
693        extensible: &[],
694        instantiable: &[],
695    },
696    Unit {
697        name: STRING_UNIT,
698        kind: UnitKind::Commons,
699        identity_path: "bynk/string.bynk",
700        source: BYNK_STRING_SRC,
701        binding: Binding::None,
702        id_block: 4,
703        platform: None,
704        env_providers: &[],
705        stability: Stability::Stable,
706        experimental: &[],
707        extensible: &[],
708        instantiable: &[],
709    },
710    Unit {
711        name: LOCALE_UNIT,
712        kind: UnitKind::Commons,
713        identity_path: "bynk/locale.bynk",
714        source: BYNK_LOCALE_SRC,
715        binding: Binding::None,
716        id_block: 6,
717        platform: None,
718        env_providers: &[],
719        stability: Stability::Stable,
720        experimental: &[],
721        extensible: &[],
722        instantiable: &[],
723    },
724    Unit {
725        name: LOCALE_TYPES_UNIT,
726        kind: UnitKind::Commons,
727        identity_path: "bynk/locale/types.bynk",
728        source: BYNK_LOCALE_TYPES_SRC,
729        binding: Binding::None,
730        id_block: 5,
731        platform: None,
732        env_providers: &[],
733        stability: Stability::Stable,
734        experimental: &[],
735        extensible: &[],
736        instantiable: &[],
737    },
738    Unit {
739        name: CRYPTO_UNIT,
740        kind: UnitKind::Adapter,
741        identity_path: "bynk/crypto.bynk",
742        source: BYNK_CRYPTO_SRC,
743        binding: Binding::PerPlatformFiles {
744            cloudflare: (
745                "bynk/crypto-cloudflare.binding.ts",
746                CRYPTO_CLOUDFLARE_BINDING,
747            ),
748            node: ("bynk/crypto-node.binding.ts", CRYPTO_NODE_BINDING),
749            browser: ("bynk/crypto-browser.binding.ts", CRYPTO_BROWSER_BINDING),
750        },
751        id_block: 7,
752        platform: None,
753        env_providers: &[],
754        // Experimental until the first-party registry has signed with it in
755        // production (#2308 R6), as P5's R2 does.
756        stability: Stability::Experimental,
757        experimental: &[],
758        extensible: &["Digest", "Signature", "CryptoError"],
759        instantiable: &[],
760    },
761    Unit {
762        name: URL_UNIT,
763        kind: UnitKind::Commons,
764        identity_path: "bynk/url.bynk",
765        source: BYNK_URL_SRC,
766        binding: Binding::None,
767        id_block: 8,
768        platform: None,
769        env_providers: &[],
770        stability: Stability::Stable,
771        experimental: &[],
772        extensible: &[],
773        instantiable: &[],
774    },
775    Unit {
776        name: SET_UNIT,
777        kind: UnitKind::Commons,
778        identity_path: "bynk/set.bynk",
779        source: BYNK_SET_SRC,
780        binding: Binding::None,
781        // 9, not 8: K2's `bynk.url` (#2594) takes 8 in parallel.
782        id_block: 9,
783        platform: None,
784        env_providers: &[],
785        stability: Stability::Stable,
786        experimental: &[],
787        extensible: &[],
788        instantiable: &[],
789    },
790];
791
792/// The registry entry for `name`, if it is a first-party unit.
793pub fn unit(name: &str) -> Option<&'static Unit> {
794    UNITS.iter().find(|u| u.name == name)
795}
796
797/// The unit a dotted item path lies in, and the rest of the path relative to it
798/// (empty for the unit itself). Unit names contain dots, so the longest unit
799/// name that is a whole-segment prefix wins: `bynk.cloudflare.Kv` is in
800/// `bynk.cloudflare`, not in `bynk`.
801pub fn split_item_path(path: &str) -> Option<(&'static Unit, &str)> {
802    split_item_path_in(UNITS, path)
803}
804
805/// [`split_item_path`] over a given table (tests pass a synthetic one).
806pub fn split_item_path_in<'u, 'p>(units: &'u [Unit], path: &'p str) -> Option<(&'u Unit, &'p str)> {
807    units
808        .iter()
809        .filter_map(|u| {
810            if path == u.name {
811                Some((u, ""))
812            } else {
813                path.strip_prefix(u.name)?
814                    .strip_prefix('.')
815                    .map(|rest| (u, rest))
816            }
817        })
818        .max_by_key(|(u, _)| u.name.len())
819}
820
821/// Whether `path` is `prefix` or lies under it, by whole dotted segments.
822pub fn covers(prefix: &str, path: &str) -> bool {
823    path == prefix
824        || path
825            .strip_prefix(prefix)
826            .is_some_and(|rest| rest.starts_with('.'))
827}
828
829/// The stability of a first-party item: a unit (`bynk.cloudflare`), an
830/// exported item (`bynk.cloudflare.Kv`) or a capability operation
831/// (`bynk.cloudflare.Kv.list`). An item in an experimental unit, or under an
832/// experimental item, is experimental. `None` when no first-party unit holds
833/// the path.
834pub fn stability_of(path: &str) -> Option<Stability> {
835    stability_of_in(UNITS, path)
836}
837
838/// [`stability_of`] over a given table.
839pub fn stability_of_in(units: &[Unit], path: &str) -> Option<Stability> {
840    let (unit, rest) = split_item_path_in(units, path)?;
841    if unit.stability == Stability::Experimental
842        || unit.experimental.iter().any(|item| covers(item, rest))
843    {
844        Some(Stability::Experimental)
845    } else {
846        Some(Stability::Stable)
847    }
848}
849
850/// Every experimental item, as a full path: each experimental unit, and each
851/// listed item of a stable one.
852pub fn experimental_items() -> Vec<String> {
853    experimental_items_in(UNITS)
854}
855
856/// [`experimental_items`] over a given table.
857pub fn experimental_items_in(units: &[Unit]) -> Vec<String> {
858    let mut out = Vec::new();
859    for u in units {
860        if u.stability == Stability::Experimental {
861            out.push(u.name.to_string());
862        }
863        out.extend(
864            u.experimental
865                .iter()
866                .map(|item| format!("{}.{item}", u.name)),
867        );
868    }
869    out
870}
871
872/// Whether an `[unstable] allow` list covers `path`: an entry covers itself and
873/// everything under it.
874pub fn allows(allow: &[String], path: &str) -> bool {
875    allow.iter().any(|entry| covers(entry, path))
876}
877
878/// Whether `entry`, an `[unstable] allow` entry, reaches anything in
879/// `experimental` (a list of experimental item paths, such as
880/// [`experimental_items`]): it names an experimental item, lies under one, or
881/// contains one. An entry that reaches nothing is stale.
882pub fn allow_entry_is_live(experimental: &[String], entry: &str) -> bool {
883    experimental
884        .iter()
885        .any(|item| covers(item, entry) || covers(entry, item))
886}
887
888/// The instance kind `capability` of `unit` is, if a context may declare named
889/// instances of it (S4a, #1966).
890pub fn instantiable(unit: &str, capability: &str) -> Option<&'static Instantiable> {
891    self::unit(unit)?
892        .instantiable
893        .iter()
894        .find(|i| i.capability == capability)
895}
896
897/// Whether `name`, an enum exported by `unit`, is extensible.
898pub fn is_extensible(unit: &str, name: &str) -> bool {
899    is_extensible_in(UNITS, unit, name)
900}
901
902/// [`is_extensible`] over a given table.
903pub fn is_extensible_in(units: &[Unit], unit: &str, name: &str) -> bool {
904    units
905        .iter()
906        .any(|u| u.name == unit && u.extensible.contains(&name))
907}
908
909/// The names a first-party unit exports: those on its `exports … { … }` lines
910/// (an adapter) and its own `fn`s and `type`s (a commons). Read from the
911/// source, so the list cannot fall behind it.
912pub fn exported_names(unit: &Unit) -> Vec<&'static str> {
913    let mut out = Vec::new();
914    let source = unit.source;
915    let mut offset = 0;
916    for line in source.lines() {
917        let start = offset;
918        offset += line.len() + 1;
919        let trimmed = line.trim_start();
920        // The list may run over several lines (`bynk-fmt` wraps a long one,
921        // one name a line), so it is read from the source up to its `}`.
922        if trimmed.starts_with("exports ")
923            && let Some(open) = source[start..].find('{').map(|i| start + i)
924            && let Some(close) = source[open..].find('}').map(|i| open + i)
925        {
926            out.extend(
927                source[open + 1..close]
928                    .split(',')
929                    .map(str::trim)
930                    .filter(|n| !n.is_empty()),
931            );
932        } else if unit.kind == UnitKind::Commons
933            // A commons' own declarations: at column 0 in the flat form, one tab
934            // in the braced `commons bynk.list { … }` form the first-party
935            // sources use. Nothing deeper is a declaration.
936            && (line == trimmed || line.strip_prefix('\t') == Some(trimmed))
937        {
938            for keyword in ["fn ", "type "] {
939                if let Some(rest) = trimmed.strip_prefix(keyword) {
940                    let end = rest
941                        .find(|c: char| !(c.is_alphanumeric() || c == '_'))
942                        .unwrap_or(rest.len());
943                    if end > 0 {
944                        out.push(&rest[..end]);
945                    }
946                }
947            }
948        }
949    }
950    out
951}
952
953/// Whether `path` names a first-party unit or an item it exports. Operation
954/// paths (`bynk.cloudflare.Kv.list`) are accepted when their item exists.
955pub fn names_first_party_item(path: &str) -> bool {
956    names_first_party_item_in(UNITS, path)
957}
958
959/// [`names_first_party_item`] over a given table.
960pub fn names_first_party_item_in(units: &[Unit], path: &str) -> bool {
961    match split_item_path_in(units, path) {
962        None => false,
963        Some((_, "")) => true,
964        Some((unit, rest)) => {
965            let item = rest.split('.').next().unwrap_or(rest);
966            exported_names(unit).contains(&item)
967        }
968    }
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974
975    /// A synthetic registry with an experimental unit, an experimental item
976    /// and an experimental operation inside stable units.
977    const SYNTH: &[Unit] = &[
978        Unit {
979            name: "bynk",
980            kind: UnitKind::Adapter,
981            identity_path: "bynk.bynk",
982            source: "adapter bynk {\n\texports capability { Clock, Fetch }\n\texports transparent { FetchError }\n}\n",
983            binding: Binding::PerPlatform,
984            id_block: 0,
985            platform: None,
986            env_providers: &[],
987            stability: Stability::Stable,
988            experimental: &["Fetch.stream"],
989            extensible: &["FetchError"],
990            instantiable: &[],
991        },
992        Unit {
993            name: "bynk.cloudflare",
994            kind: UnitKind::Adapter,
995            identity_path: "bynk/cloudflare.bynk",
996            source: "adapter bynk.cloudflare {\n\texports capability { Kv, Ai }\n}\n",
997            binding: Binding::None,
998            id_block: 1,
999            platform: Some(Platform::Cloudflare),
1000            env_providers: &[],
1001            stability: Stability::Stable,
1002            experimental: &["Ai"],
1003            extensible: &[],
1004            instantiable: &[],
1005        },
1006        Unit {
1007            name: "bynk.cloudflare.d1",
1008            kind: UnitKind::Adapter,
1009            identity_path: "bynk/cloudflare/d1.bynk",
1010            source: "adapter bynk.cloudflare.d1 {\n\texports capability { D1 }\n}\n",
1011            binding: Binding::None,
1012            id_block: 2,
1013            platform: Some(Platform::Cloudflare),
1014            env_providers: &[],
1015            stability: Stability::Experimental,
1016            experimental: &[],
1017            extensible: &[],
1018            instantiable: &[],
1019        },
1020        Unit {
1021            name: "bynk.text",
1022            kind: UnitKind::Commons,
1023            identity_path: "bynk/text.bynk",
1024            source: "commons bynk.text\n\ntype Slug = String\n\nfn shout(s: String) -> String {\n\ts\n}\n",
1025            binding: Binding::None,
1026            id_block: 3,
1027            platform: None,
1028            env_providers: &[],
1029            stability: Stability::Stable,
1030            experimental: &[],
1031            extensible: &[],
1032            instantiable: &[],
1033        },
1034    ];
1035
1036    #[test]
1037    fn item_paths_resolve_to_the_longest_unit() {
1038        let unit_of = |p| split_item_path_in(SYNTH, p).map(|(u, rest)| (u.name, rest));
1039        assert_eq!(unit_of("bynk"), Some(("bynk", "")));
1040        assert_eq!(unit_of("bynk.Fetch.send"), Some(("bynk", "Fetch.send")));
1041        assert_eq!(
1042            unit_of("bynk.cloudflare.Kv"),
1043            Some(("bynk.cloudflare", "Kv"))
1044        );
1045        assert_eq!(
1046            unit_of("bynk.cloudflare.d1.D1"),
1047            Some(("bynk.cloudflare.d1", "D1"))
1048        );
1049        assert_eq!(unit_of("bynkx"), None, "a whole-segment prefix only");
1050        assert_eq!(unit_of("user.unit"), None);
1051    }
1052
1053    #[test]
1054    fn stability_follows_unit_item_and_operation() {
1055        let st = |p| stability_of_in(SYNTH, p);
1056        use Stability::*;
1057        assert_eq!(st("bynk.Clock"), Some(Stable));
1058        assert_eq!(st("bynk.Fetch.send"), Some(Stable));
1059        assert_eq!(
1060            st("bynk.Fetch.stream"),
1061            Some(Experimental),
1062            "an experimental operation"
1063        );
1064        assert_eq!(
1065            st("bynk.Fetch"),
1066            Some(Stable),
1067            "its capability stays stable"
1068        );
1069        assert_eq!(
1070            st("bynk.cloudflare.Ai"),
1071            Some(Experimental),
1072            "an experimental item"
1073        );
1074        assert_eq!(
1075            st("bynk.cloudflare.Ai.run"),
1076            Some(Experimental),
1077            "its operations too"
1078        );
1079        assert_eq!(st("bynk.cloudflare.Kv"), Some(Stable));
1080        assert_eq!(
1081            st("bynk.cloudflare.d1"),
1082            Some(Experimental),
1083            "an experimental unit"
1084        );
1085        assert_eq!(st("bynk.cloudflare.d1.D1.query"), Some(Experimental));
1086        assert_eq!(st("user.unit"), None);
1087    }
1088
1089    #[test]
1090    fn allow_entries_cover_by_whole_segment_prefix() {
1091        let allow = vec![
1092            "bynk.cloudflare.Ai".to_string(),
1093            "bynk.cloudflare.d1".to_string(),
1094        ];
1095        assert!(allows(&allow, "bynk.cloudflare.Ai"));
1096        assert!(allows(&allow, "bynk.cloudflare.Ai.run"));
1097        assert!(allows(&allow, "bynk.cloudflare.d1.D1.query"));
1098        assert!(
1099            !allows(&allow, "bynk.cloudflare.AiX"),
1100            "not a segment prefix"
1101        );
1102        assert!(!allows(&allow, "bynk.Fetch.stream"));
1103        assert!(!allows(&[], "bynk.cloudflare.Ai"));
1104    }
1105
1106    #[test]
1107    fn an_allow_entry_is_live_while_it_reaches_something_experimental() {
1108        let synth = experimental_items_in(SYNTH);
1109        let live = |e| allow_entry_is_live(&synth, e);
1110        assert!(live("bynk.cloudflare.Ai"));
1111        assert!(live("bynk.cloudflare.Ai.run"), "under an experimental item");
1112        assert!(live("bynk.cloudflare"), "contains an experimental item");
1113        assert!(live("bynk.Fetch.stream"));
1114        assert!(live("bynk"), "contains an experimental operation");
1115        assert!(!live("bynk.cloudflare.Kv"), "stable: a stale entry");
1116        assert!(!live("bynk.text"));
1117        assert_eq!(
1118            experimental_items_in(SYNTH),
1119            [
1120                "bynk.Fetch.stream",
1121                "bynk.cloudflare.Ai",
1122                "bynk.cloudflare.d1"
1123            ]
1124        );
1125    }
1126
1127    #[test]
1128    fn exported_names_and_item_existence_come_from_the_sources() {
1129        assert_eq!(exported_names(&SYNTH[0]), ["Clock", "Fetch", "FetchError"]);
1130        // K5a (#2586): a list `bynk-fmt` wrapped, one name a line.
1131        let wrapped = Unit {
1132            source: "adapter bynk {\n\texports transparent {\n\t\tMethod,\n\t\tRedirect,\n\t}\n\texports capability { Fetch }\n}\n",
1133            ..SYNTH[0]
1134        };
1135        assert_eq!(exported_names(&wrapped), ["Method", "Redirect", "Fetch"]);
1136        assert_eq!(exported_names(&SYNTH[3]), ["Slug", "shout"]);
1137        let names = |p| names_first_party_item_in(SYNTH, p);
1138        assert!(names("bynk.cloudflare"));
1139        assert!(names("bynk.cloudflare.Ai"));
1140        assert!(
1141            names("bynk.cloudflare.Ai.run"),
1142            "an operation of an existing item"
1143        );
1144        assert!(names("bynk.text.shout"));
1145        assert!(!names("bynk.cloudflare.Aii"), "a typo");
1146        assert!(!names("user.unit"));
1147    }
1148
1149    #[test]
1150    fn extensibility_is_by_unit_and_name() {
1151        assert!(is_extensible_in(SYNTH, "bynk", "FetchError"));
1152        assert!(
1153            !is_extensible_in(SYNTH, "user.app", "FetchError"),
1154            "a user's own FetchError"
1155        );
1156        assert!(!is_extensible_in(SYNTH, "bynk", "Method"));
1157    }
1158
1159    /// The shipped instantiable kinds: `bynk.cloudflare`'s `Kv` (S4a, #1966),
1160    /// binding as `KV_<NAME>` with no creation options, and its `Service`
1161    /// (interop I5, #1972), binding as `SERVICE_<NAME>` and naming its Worker
1162    /// with a required `@worker`, never usable bare. Each one is a capability
1163    /// its unit exports.
1164    #[test]
1165    fn the_shipped_instantiable_kinds_are_pinned() {
1166        let kinds: Vec<(&str, &str, &str)> = UNITS
1167            .iter()
1168            .flat_map(|u| {
1169                u.instantiable
1170                    .iter()
1171                    .map(move |i| (u.name, i.capability, i.binding_prefix))
1172            })
1173            .collect();
1174        assert_eq!(
1175            kinds,
1176            [
1177                ("bynk.cloudflare", "Kv", "KV"),
1178                ("bynk.cloudflare", "Service", "SERVICE"),
1179                ("bynk.cloudflare", "R2", "R2"),
1180                ("bynk.cloudflare", "RateLimit", "RATELIMIT"),
1181                ("bynk.cloudflare", "Queue", "QUEUE"),
1182                ("bynk.cloudflare", "Analytics", "ANALYTICS"),
1183                ("bynk.cloudflare", "D1", "D1"),
1184                ("bynk.cloudflare", "EdgeCache", "EDGE_CACHE")
1185            ]
1186        );
1187        let kv = instantiable("bynk.cloudflare", "Kv").expect("Kv is instantiable");
1188        assert!(kv.options.is_empty() && kv.required.is_empty() && kv.bare);
1189        let service = instantiable("bynk.cloudflare", "Service").expect("Service is instantiable");
1190        assert_eq!(service.option_names().collect::<Vec<_>>(), ["worker"]);
1191        assert_eq!(service.required, ["worker"]);
1192        assert!(!service.bare);
1193        let r2 = instantiable("bynk.cloudflare", "R2").expect("R2 is instantiable");
1194        assert_eq!(r2.option_names().collect::<Vec<_>>(), ["jurisdiction"]);
1195        assert!(r2.required.is_empty() && !r2.bare);
1196        let rl = instantiable("bynk.cloudflare", "RateLimit").expect("RateLimit is instantiable");
1197        assert_eq!(rl.option_kind("limit"), Some(OptionKind::PositiveInt));
1198        assert_eq!(rl.option_kind("period"), Some(OptionKind::Duration));
1199        assert_eq!(rl.required, ["limit", "period"]);
1200        assert!(!rl.bare);
1201        let queue = instantiable("bynk.cloudflare", "Queue").expect("Queue is instantiable");
1202        assert_eq!(queue.option_kind("name"), Some(OptionKind::Str));
1203        assert_eq!(queue.required, ["name"]);
1204        assert!(!queue.bare);
1205        let analytics =
1206            instantiable("bynk.cloudflare", "Analytics").expect("Analytics is instantiable");
1207        assert!(analytics.options.is_empty() && !analytics.bare);
1208        let edge = instantiable("bynk.cloudflare", "EdgeCache").expect("EdgeCache is instantiable");
1209        assert!(edge.options.is_empty() && !edge.bare);
1210        for u in UNITS {
1211            for i in u.instantiable {
1212                assert!(
1213                    i.required.iter().all(|r| i.option_kind(r).is_some()),
1214                    "{}.{}: a required option must be an option",
1215                    u.name,
1216                    i.capability
1217                );
1218            }
1219        }
1220        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Kv"));
1221        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Service"));
1222        assert_eq!(instantiable("bynk", "Clock"), None);
1223        assert_eq!(instantiable("bynk.cloudflare", "Nope"), None);
1224    }
1225
1226    /// The shipped registry: every unit is stable (S3 ships the mechanism,
1227    /// #1963 Decision C) but `bynk.crypto` (K4), which is experimental until the
1228    /// first-party registry has signed with it in production. The one
1229    /// experimental item inside a stable unit is `bynk.cloudflare`'s `Service`
1230    /// (interop I5, #1972), and the growing enums are extensible. A slice that
1231    /// marks an item experimental updates this test, so the change is visible
1232    /// in review.
1233    #[test]
1234    fn the_shipped_stability_is_pinned() {
1235        for u in UNITS {
1236            let expected = if u.name == CRYPTO_UNIT {
1237                Stability::Experimental
1238            } else {
1239                Stability::Stable
1240            };
1241            assert_eq!(u.stability, expected, "{}", u.name);
1242        }
1243        assert_eq!(
1244            experimental_items(),
1245            [
1246                "bynk.Fetch.sendWith",
1247                "bynk.Fetch.receiveBytes",
1248                "bynk.Fetch.sendBytes",
1249                "bynk.Fetch.receiveStream",
1250                "bynk.Fetch.sendStream",
1251                "bynk.Logger.log",
1252                "bynk.cloudflare.Service",
1253                "bynk.cloudflare.R2",
1254                "bynk.cloudflare.RateLimit",
1255                "bynk.cloudflare.EdgeCache",
1256                "bynk.cloudflare.Queue",
1257                "bynk.cloudflare.Analytics",
1258                "bynk.cloudflare.Kv.getWith",
1259                "bynk.cloudflare.Kv.putWith",
1260                "bynk.cloudflare.Kv.getBytes",
1261                "bynk.cloudflare.Kv.putBytes",
1262                "bynk.cloudflare.Kv.listPage",
1263                "bynk.crypto"
1264            ]
1265        );
1266        let extensible: Vec<(&str, &str)> = UNITS
1267            .iter()
1268            .flat_map(|u| u.extensible.iter().map(move |e| (u.name, *e)))
1269            .collect();
1270        assert_eq!(
1271            extensible,
1272            [
1273                ("bynk", "FetchError"),
1274                ("bynk", "Method"),
1275                ("bynk", "Redirect"),
1276                ("bynk.cloudflare", "R2Condition"),
1277                ("bynk.cloudflare", "KvExpiration"),
1278                ("bynk.cloudflare", "SqlError"),
1279                ("bynk.crypto", "Digest"),
1280                ("bynk.crypto", "Signature"),
1281                ("bynk.crypto", "CryptoError"),
1282            ]
1283        );
1284        for (unit, name) in extensible {
1285            assert!(
1286                exported_names(self::unit(unit).unwrap()).contains(&name),
1287                "{unit} lists {name} as extensible but does not export it"
1288            );
1289        }
1290        assert!(names_first_party_item("bynk.cloudflare.Kv.list"));
1291        assert!(names_first_party_item("bynk.list.reverse"));
1292        assert!(!names_first_party_item("bynk.cloudflare.Kvv"));
1293    }
1294
1295    /// #1958: `WorkersKv` reads the namespace at the name every generated
1296    /// Worker declares it under (`[[kv_namespaces]] binding`, its `Env`), the
1297    /// one TypeScript literal no Rust constructor can supply. Since S4a (#1966)
1298    /// the name is a constructor parameter, so a derived capability's instance
1299    /// can pass its own; the bare `Kv`'s is the parameter's default.
1300    ///
1301    /// Matched as a whole identifier, then by its neighbours, so the test
1302    /// ignores formatting but not a rename: `KVX` is not `KV` (review of
1303    /// #1962).
1304    #[test]
1305    fn the_cloudflare_binding_reads_kv_binding_name() {
1306        let ident = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
1307        let src = CLOUDFLARE_BINDING;
1308        let uses: Vec<(&str, &str)> = src
1309            .match_indices(KV_BINDING_NAME)
1310            .map(|(at, name)| (&src[..at], &src[at + name.len()..]))
1311            .filter(|(before, after)| !before.ends_with(ident) && !after.starts_with(ident))
1312            .collect();
1313        assert!(
1314            uses.iter().any(|(b, a)| {
1315                b.trim_end().ends_with("binding: string = \"") && a.starts_with('"')
1316            }),
1317            "the constructor's `binding` no longer defaults to \"{KV_BINDING_NAME}\""
1318        );
1319        assert!(
1320            src.contains("?.[this.binding]"),
1321            "the namespace is no longer read at `env[binding]`"
1322        );
1323    }
1324
1325    /// Each unit's `ExprId` block is permanent: emitted output and the
1326    /// process-wide parse cache depend on it. These are the blocks the
1327    /// hand-written injection used before the registry (S1, #1945); a new
1328    /// unit takes a new block and never moves one of these.
1329    #[test]
1330    fn id_blocks_are_pinned_and_distinct() {
1331        let blocks: Vec<(&str, u32)> = UNITS.iter().map(|u| (u.name, u.id_block)).collect();
1332        assert_eq!(
1333            blocks,
1334            [
1335                ("bynk", 0),
1336                ("bynk.cloudflare", 1),
1337                ("bynk.list", 3),
1338                ("bynk.map", 2),
1339                ("bynk.string", 4),
1340                ("bynk.locale", 6),
1341                ("bynk.locale.types", 5),
1342                ("bynk.crypto", 7),
1343                ("bynk.url", 8),
1344                ("bynk.set", 9),
1345            ]
1346        );
1347        let mut sorted: Vec<u32> = UNITS.iter().map(|u| u.id_block).collect();
1348        sorted.sort_unstable();
1349        sorted.dedup();
1350        assert_eq!(sorted.len(), UNITS.len(), "two units share an id block");
1351    }
1352
1353    /// Hover and completion read [`FIRSTPARTY_SOURCES`] in order, so it is
1354    /// pinned to the order the hand-written list had.
1355    #[test]
1356    fn firstparty_sources_keep_their_order() {
1357        let names: Vec<&str> = FIRSTPARTY_SOURCES.iter().map(|(n, _)| *n).collect();
1358        assert_eq!(
1359            names,
1360            [
1361                "bynk",
1362                "bynk.cloudflare",
1363                "bynk.list",
1364                "bynk.map",
1365                "bynk.string",
1366                "bynk.locale",
1367                "bynk.locale.types",
1368                "bynk.crypto",
1369                "bynk.url",
1370                "bynk.set",
1371            ]
1372        );
1373        for (unit, (name, source)) in UNITS.iter().zip(FIRSTPARTY_SOURCES) {
1374            assert_eq!((unit.name, unit.source), (*name, *source));
1375        }
1376    }
1377
1378    /// The views over the table answer as the hand-written matches did.
1379    #[test]
1380    fn metadata_views_match_the_previous_answers() {
1381        assert_eq!(platform_of("bynk.cloudflare"), Some(Platform::Cloudflare));
1382        for name in [
1383            "bynk",
1384            "bynk.list",
1385            "bynk.map",
1386            "bynk.string",
1387            "bynk.locale",
1388            "user.unit",
1389        ] {
1390            assert_eq!(platform_of(name), None, "{name}");
1391        }
1392        assert!(provider_takes_env("bynk", "SecretsProvider"));
1393        assert!(provider_takes_env("bynk.cloudflare", "WorkersKv"));
1394        assert!(!provider_takes_env("bynk", "WorkersKv"));
1395        assert!(!provider_takes_env("bynk.cloudflare", "SecretsProvider"));
1396        assert!(!provider_takes_env("bynk", "ClockProvider"));
1397        assert!(!provider_takes_env("user.unit", "SecretsProvider"));
1398    }
1399
1400    /// An adapter carries a binding and a commons does not, and every name and
1401    /// identity path is unique, every name inside the reserved prefix.
1402    #[test]
1403    fn entries_are_well_formed() {
1404        for u in UNITS {
1405            assert!(
1406                u.name == "bynk" || u.name.starts_with("bynk."),
1407                "{}",
1408                u.name
1409            );
1410            assert_eq!(
1411                UNITS.iter().filter(|o| o.name == u.name).count(),
1412                1,
1413                "{}",
1414                u.name
1415            );
1416            // Two entries under one identity path would be two parsed units
1417            // that diagnostics and emission cannot tell apart.
1418            assert_eq!(
1419                UNITS
1420                    .iter()
1421                    .filter(|o| o.identity_path == u.identity_path)
1422                    .count(),
1423                1,
1424                "{}",
1425                u.identity_path
1426            );
1427            match u.kind {
1428                UnitKind::Adapter => assert_ne!(u.binding, Binding::None, "{}", u.name),
1429                UnitKind::Commons => {
1430                    assert_eq!(u.binding, Binding::None, "{}", u.name);
1431                    assert!(u.env_providers.is_empty(), "{}", u.name);
1432                }
1433            }
1434        }
1435        assert_eq!(
1436            unit("bynk.cloudflare").and_then(|u| u.binding_for(Platform::Node)),
1437            Some((CLOUDFLARE_BINDING_FILENAME, CLOUDFLARE_BINDING))
1438        );
1439        assert_eq!(
1440            unit("bynk").and_then(|u| u.binding_for(Platform::Browser)),
1441            Some(("bynk-browser.ts", BYNK_BROWSER_BINDING))
1442        );
1443        assert_eq!(
1444            unit("bynk.list").and_then(|u| u.binding_for(Platform::Node)),
1445            None
1446        );
1447    }
1448
1449    /// The three `bynk.crypto` bindings are one body with one marked region of
1450    /// platform difference (the constant-time comparison primitive). They are
1451    /// separate files because each ships alone, so this keeps them from drifting.
1452    #[test]
1453    fn the_crypto_bindings_differ_only_in_their_platform_region() {
1454        const START: &str = "// --- platform: constant-time comparison primitive";
1455        const END: &str = "// --- end platform";
1456        fn shared(src: &str) -> String {
1457            let (head, rest) = src.split_once(START).expect("platform region start");
1458            let (_, tail) = rest.split_once(END).expect("platform region end");
1459            // The second header line names the platform.
1460            let head = head.lines().skip(2).collect::<Vec<_>>().join("\n");
1461            format!("{head}\n{tail}")
1462        }
1463        let cloudflare = shared(CRYPTO_CLOUDFLARE_BINDING);
1464        assert_eq!(cloudflare, shared(CRYPTO_NODE_BINDING));
1465        assert_eq!(cloudflare, shared(CRYPTO_BROWSER_BINDING));
1466    }
1467
1468    /// The hardcoded [`BYNK_SURFACE_CAPABILITIES`] must match the adapter's
1469    /// `exports capability { … }` line — otherwise the unknown-name quick-fix
1470    /// (#852) would offer a stale `consumes bynk { … }` set.
1471    #[test]
1472    fn bynk_surface_capabilities_match_the_adapter() {
1473        let src = BYNK_ADAPTER_SRC;
1474        let line = src
1475            .lines()
1476            .find(|l| l.trim_start().starts_with("exports capability {"))
1477            .expect("adapter declares `exports capability`");
1478        let inner = line
1479            .split_once('{')
1480            .and_then(|(_, r)| r.split_once('}'))
1481            .map(|(names, _)| names)
1482            .expect("braced capability list");
1483        let declared: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1484        assert_eq!(
1485            declared, BYNK_SURFACE_CAPABILITIES,
1486            "BYNK_SURFACE_CAPABILITIES is out of sync with bynk.bynk"
1487        );
1488    }
1489}