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 prefix an `EdgeCache` instance's binding name would carry (first-party
485/// registry L7, #2414). Workers Cache is a Worker-wide `[cache]` setting with
486/// no `env` binding, so the name is never emitted: the instance is built from
487/// the entry's `ctx` instead.
488pub const EDGE_CACHE_BINDING_PREFIX: &str = "EDGE_CACHE";
489
490/// Whether a first-party item is under the 1.0 promise (track D2).
491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum Stability {
493    /// Under the promise: a stable L0/L1 item changes only at a major release; a
494    /// stable L2/L3 item may gain operations and, if extensible, variants.
495    Stable,
496    /// No promise: it may change or disappear in any release, and using it needs
497    /// an `[unstable] allow` entry in `bynk.toml`.
498    Experimental,
499}
500
501impl Unit {
502    /// The binding module to emit for this unit on `platform`, as
503    /// `(output path, source)`, or `None` for a commons.
504    pub fn binding_for(&self, platform: Platform) -> Option<(&'static str, &'static str)> {
505        match self.binding {
506            Binding::None => None,
507            Binding::PerPlatform => Some((
508                platform.bynk_binding_filename(),
509                platform.bynk_binding_source(),
510            )),
511            Binding::Fixed { filename, source } => Some((filename, source)),
512            Binding::PerPlatformFiles {
513                cloudflare,
514                node,
515                browser,
516            } => Some(match platform {
517                Platform::Cloudflare => cloudflare,
518                Platform::Node => node,
519                Platform::Browser => browser,
520            }),
521        }
522    }
523}
524
525/// The standard library's first-party units. Dependencies between them are not
526/// listed here: they are the `uses`/`consumes` lines of each unit's own source,
527/// which the checker follows when it injects. The table's order is
528/// [`FIRSTPARTY_SOURCES`]' order; injection follows [`Unit::id_block`].
529pub const UNITS: &[Unit] = &[
530    Unit {
531        name: BYNK_UNIT,
532        kind: UnitKind::Adapter,
533        identity_path: "bynk.bynk",
534        source: BYNK_ADAPTER_SRC,
535        binding: Binding::PerPlatform,
536        id_block: 0,
537        platform: None,
538        env_providers: &["SecretsProvider"],
539        stability: Stability::Stable,
540        // K5a (#2586): the new operations, until a service has used them.
541        experimental: &["Fetch.sendWith", "Logger.log"],
542        extensible: &["FetchError", "Method", "Redirect"],
543        instantiable: &[],
544    },
545    Unit {
546        name: CLOUDFLARE_UNIT,
547        kind: UnitKind::Adapter,
548        identity_path: "bynk/cloudflare.bynk",
549        source: CLOUDFLARE_ADAPTER_SRC,
550        binding: Binding::Fixed {
551            filename: CLOUDFLARE_BINDING_FILENAME,
552            source: CLOUDFLARE_BINDING,
553        },
554        id_block: 1,
555        platform: Some(Platform::Cloudflare),
556        env_providers: &[
557            "WorkersKv",
558            "WorkersService",
559            "WorkersR2",
560            "WorkersRateLimit",
561            "WorkersQueue",
562            "WorkersAnalytics",
563        ],
564        stability: Stability::Stable,
565        // Interop I5 (#1972): a Service Binding to a non-Bynk Worker is new
566        // and its surface is untyped, so it lands outside the 1.0 promise.
567        // P5 (#2323): R2 ships experimental until the first-party registry has
568        // run on it (#2308 R6); its conditions are extensible meanwhile.
569        // P3 (#2342): RateLimit too, until the registry has run on it.
570        // L7 (#2414): EdgeCache likewise.
571        // P2 (#2578): Queue too, until a first-party service has sent on it.
572        // P3b (#2582): Analytics too, until a first-party service writes to it.
573        // P1 (#2590): the new operations of the stable `Kv`, until a service
574        // has used them.
575        experimental: &[
576            "Service",
577            "R2",
578            "RateLimit",
579            "EdgeCache",
580            "Queue",
581            "Analytics",
582            "Kv.getWith",
583            "Kv.putWith",
584            "Kv.getBytes",
585            "Kv.putBytes",
586            "Kv.listPage",
587        ],
588        extensible: &["R2Condition", "KvExpiration"],
589        instantiable: &[
590            Instantiable {
591                capability: "Kv",
592                binding_prefix: KV_BINDING_NAME,
593                options: &[],
594                required: &[],
595                bare: true,
596            },
597            Instantiable {
598                capability: "Service",
599                binding_prefix: SERVICE_BINDING_PREFIX,
600                options: &[(SERVICE_WORKER_OPTION, OptionKind::Str)],
601                required: &[SERVICE_WORKER_OPTION],
602                bare: false,
603            },
604            Instantiable {
605                capability: "R2",
606                binding_prefix: R2_BINDING_PREFIX,
607                options: &[(R2_JURISDICTION_OPTION, OptionKind::Str)],
608                required: &[],
609                bare: false,
610            },
611            Instantiable {
612                capability: "RateLimit",
613                binding_prefix: RATELIMIT_BINDING_PREFIX,
614                options: &[
615                    (RATELIMIT_LIMIT_OPTION, OptionKind::PositiveInt),
616                    (RATELIMIT_PERIOD_OPTION, OptionKind::Duration),
617                ],
618                required: &[RATELIMIT_LIMIT_OPTION, RATELIMIT_PERIOD_OPTION],
619                bare: false,
620            },
621            Instantiable {
622                capability: "Queue",
623                binding_prefix: QUEUE_BINDING_PREFIX,
624                options: &[(QUEUE_NAME_OPTION, OptionKind::Str)],
625                required: &[QUEUE_NAME_OPTION],
626                bare: false,
627            },
628            Instantiable {
629                capability: "Analytics",
630                binding_prefix: ANALYTICS_BINDING_PREFIX,
631                options: &[],
632                required: &[],
633                bare: false,
634            },
635            Instantiable {
636                capability: "EdgeCache",
637                binding_prefix: EDGE_CACHE_BINDING_PREFIX,
638                options: &[],
639                required: &[],
640                bare: false,
641            },
642        ],
643    },
644    Unit {
645        name: LIST_UNIT,
646        kind: UnitKind::Commons,
647        identity_path: "bynk/list.bynk",
648        source: BYNK_LIST_SRC,
649        binding: Binding::None,
650        id_block: 3,
651        platform: None,
652        env_providers: &[],
653        stability: Stability::Stable,
654        experimental: &[],
655        extensible: &[],
656        instantiable: &[],
657    },
658    Unit {
659        name: MAP_UNIT,
660        kind: UnitKind::Commons,
661        identity_path: "bynk/map.bynk",
662        source: BYNK_MAP_SRC,
663        binding: Binding::None,
664        id_block: 2,
665        platform: None,
666        env_providers: &[],
667        stability: Stability::Stable,
668        experimental: &[],
669        extensible: &[],
670        instantiable: &[],
671    },
672    Unit {
673        name: STRING_UNIT,
674        kind: UnitKind::Commons,
675        identity_path: "bynk/string.bynk",
676        source: BYNK_STRING_SRC,
677        binding: Binding::None,
678        id_block: 4,
679        platform: None,
680        env_providers: &[],
681        stability: Stability::Stable,
682        experimental: &[],
683        extensible: &[],
684        instantiable: &[],
685    },
686    Unit {
687        name: LOCALE_UNIT,
688        kind: UnitKind::Commons,
689        identity_path: "bynk/locale.bynk",
690        source: BYNK_LOCALE_SRC,
691        binding: Binding::None,
692        id_block: 6,
693        platform: None,
694        env_providers: &[],
695        stability: Stability::Stable,
696        experimental: &[],
697        extensible: &[],
698        instantiable: &[],
699    },
700    Unit {
701        name: LOCALE_TYPES_UNIT,
702        kind: UnitKind::Commons,
703        identity_path: "bynk/locale/types.bynk",
704        source: BYNK_LOCALE_TYPES_SRC,
705        binding: Binding::None,
706        id_block: 5,
707        platform: None,
708        env_providers: &[],
709        stability: Stability::Stable,
710        experimental: &[],
711        extensible: &[],
712        instantiable: &[],
713    },
714    Unit {
715        name: CRYPTO_UNIT,
716        kind: UnitKind::Adapter,
717        identity_path: "bynk/crypto.bynk",
718        source: BYNK_CRYPTO_SRC,
719        binding: Binding::PerPlatformFiles {
720            cloudflare: (
721                "bynk/crypto-cloudflare.binding.ts",
722                CRYPTO_CLOUDFLARE_BINDING,
723            ),
724            node: ("bynk/crypto-node.binding.ts", CRYPTO_NODE_BINDING),
725            browser: ("bynk/crypto-browser.binding.ts", CRYPTO_BROWSER_BINDING),
726        },
727        id_block: 7,
728        platform: None,
729        env_providers: &[],
730        // Experimental until the first-party registry has signed with it in
731        // production (#2308 R6), as P5's R2 does.
732        stability: Stability::Experimental,
733        experimental: &[],
734        extensible: &["Digest", "Signature", "CryptoError"],
735        instantiable: &[],
736    },
737    Unit {
738        name: URL_UNIT,
739        kind: UnitKind::Commons,
740        identity_path: "bynk/url.bynk",
741        source: BYNK_URL_SRC,
742        binding: Binding::None,
743        id_block: 8,
744        platform: None,
745        env_providers: &[],
746        stability: Stability::Stable,
747        experimental: &[],
748        extensible: &[],
749        instantiable: &[],
750    },
751    Unit {
752        name: SET_UNIT,
753        kind: UnitKind::Commons,
754        identity_path: "bynk/set.bynk",
755        source: BYNK_SET_SRC,
756        binding: Binding::None,
757        // 9, not 8: K2's `bynk.url` (#2594) takes 8 in parallel.
758        id_block: 9,
759        platform: None,
760        env_providers: &[],
761        stability: Stability::Stable,
762        experimental: &[],
763        extensible: &[],
764        instantiable: &[],
765    },
766];
767
768/// The registry entry for `name`, if it is a first-party unit.
769pub fn unit(name: &str) -> Option<&'static Unit> {
770    UNITS.iter().find(|u| u.name == name)
771}
772
773/// The unit a dotted item path lies in, and the rest of the path relative to it
774/// (empty for the unit itself). Unit names contain dots, so the longest unit
775/// name that is a whole-segment prefix wins: `bynk.cloudflare.Kv` is in
776/// `bynk.cloudflare`, not in `bynk`.
777pub fn split_item_path(path: &str) -> Option<(&'static Unit, &str)> {
778    split_item_path_in(UNITS, path)
779}
780
781/// [`split_item_path`] over a given table (tests pass a synthetic one).
782pub fn split_item_path_in<'u, 'p>(units: &'u [Unit], path: &'p str) -> Option<(&'u Unit, &'p str)> {
783    units
784        .iter()
785        .filter_map(|u| {
786            if path == u.name {
787                Some((u, ""))
788            } else {
789                path.strip_prefix(u.name)?
790                    .strip_prefix('.')
791                    .map(|rest| (u, rest))
792            }
793        })
794        .max_by_key(|(u, _)| u.name.len())
795}
796
797/// Whether `path` is `prefix` or lies under it, by whole dotted segments.
798pub fn covers(prefix: &str, path: &str) -> bool {
799    path == prefix
800        || path
801            .strip_prefix(prefix)
802            .is_some_and(|rest| rest.starts_with('.'))
803}
804
805/// The stability of a first-party item: a unit (`bynk.cloudflare`), an
806/// exported item (`bynk.cloudflare.Kv`) or a capability operation
807/// (`bynk.cloudflare.Kv.list`). An item in an experimental unit, or under an
808/// experimental item, is experimental. `None` when no first-party unit holds
809/// the path.
810pub fn stability_of(path: &str) -> Option<Stability> {
811    stability_of_in(UNITS, path)
812}
813
814/// [`stability_of`] over a given table.
815pub fn stability_of_in(units: &[Unit], path: &str) -> Option<Stability> {
816    let (unit, rest) = split_item_path_in(units, path)?;
817    if unit.stability == Stability::Experimental
818        || unit.experimental.iter().any(|item| covers(item, rest))
819    {
820        Some(Stability::Experimental)
821    } else {
822        Some(Stability::Stable)
823    }
824}
825
826/// Every experimental item, as a full path: each experimental unit, and each
827/// listed item of a stable one.
828pub fn experimental_items() -> Vec<String> {
829    experimental_items_in(UNITS)
830}
831
832/// [`experimental_items`] over a given table.
833pub fn experimental_items_in(units: &[Unit]) -> Vec<String> {
834    let mut out = Vec::new();
835    for u in units {
836        if u.stability == Stability::Experimental {
837            out.push(u.name.to_string());
838        }
839        out.extend(
840            u.experimental
841                .iter()
842                .map(|item| format!("{}.{item}", u.name)),
843        );
844    }
845    out
846}
847
848/// Whether an `[unstable] allow` list covers `path`: an entry covers itself and
849/// everything under it.
850pub fn allows(allow: &[String], path: &str) -> bool {
851    allow.iter().any(|entry| covers(entry, path))
852}
853
854/// Whether `entry`, an `[unstable] allow` entry, reaches anything in
855/// `experimental` (a list of experimental item paths, such as
856/// [`experimental_items`]): it names an experimental item, lies under one, or
857/// contains one. An entry that reaches nothing is stale.
858pub fn allow_entry_is_live(experimental: &[String], entry: &str) -> bool {
859    experimental
860        .iter()
861        .any(|item| covers(item, entry) || covers(entry, item))
862}
863
864/// The instance kind `capability` of `unit` is, if a context may declare named
865/// instances of it (S4a, #1966).
866pub fn instantiable(unit: &str, capability: &str) -> Option<&'static Instantiable> {
867    self::unit(unit)?
868        .instantiable
869        .iter()
870        .find(|i| i.capability == capability)
871}
872
873/// Whether `name`, an enum exported by `unit`, is extensible.
874pub fn is_extensible(unit: &str, name: &str) -> bool {
875    is_extensible_in(UNITS, unit, name)
876}
877
878/// [`is_extensible`] over a given table.
879pub fn is_extensible_in(units: &[Unit], unit: &str, name: &str) -> bool {
880    units
881        .iter()
882        .any(|u| u.name == unit && u.extensible.contains(&name))
883}
884
885/// The names a first-party unit exports: those on its `exports … { … }` lines
886/// (an adapter) and its own `fn`s and `type`s (a commons). Read from the
887/// source, so the list cannot fall behind it.
888pub fn exported_names(unit: &Unit) -> Vec<&'static str> {
889    let mut out = Vec::new();
890    let source = unit.source;
891    let mut offset = 0;
892    for line in source.lines() {
893        let start = offset;
894        offset += line.len() + 1;
895        let trimmed = line.trim_start();
896        // The list may run over several lines (`bynk-fmt` wraps a long one,
897        // one name a line), so it is read from the source up to its `}`.
898        if trimmed.starts_with("exports ")
899            && let Some(open) = source[start..].find('{').map(|i| start + i)
900            && let Some(close) = source[open..].find('}').map(|i| open + i)
901        {
902            out.extend(
903                source[open + 1..close]
904                    .split(',')
905                    .map(str::trim)
906                    .filter(|n| !n.is_empty()),
907            );
908        } else if unit.kind == UnitKind::Commons
909            // A commons' own declarations: at column 0 in the flat form, one tab
910            // in the braced `commons bynk.list { … }` form the first-party
911            // sources use. Nothing deeper is a declaration.
912            && (line == trimmed || line.strip_prefix('\t') == Some(trimmed))
913        {
914            for keyword in ["fn ", "type "] {
915                if let Some(rest) = trimmed.strip_prefix(keyword) {
916                    let end = rest
917                        .find(|c: char| !(c.is_alphanumeric() || c == '_'))
918                        .unwrap_or(rest.len());
919                    if end > 0 {
920                        out.push(&rest[..end]);
921                    }
922                }
923            }
924        }
925    }
926    out
927}
928
929/// Whether `path` names a first-party unit or an item it exports. Operation
930/// paths (`bynk.cloudflare.Kv.list`) are accepted when their item exists.
931pub fn names_first_party_item(path: &str) -> bool {
932    names_first_party_item_in(UNITS, path)
933}
934
935/// [`names_first_party_item`] over a given table.
936pub fn names_first_party_item_in(units: &[Unit], path: &str) -> bool {
937    match split_item_path_in(units, path) {
938        None => false,
939        Some((_, "")) => true,
940        Some((unit, rest)) => {
941            let item = rest.split('.').next().unwrap_or(rest);
942            exported_names(unit).contains(&item)
943        }
944    }
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950
951    /// A synthetic registry with an experimental unit, an experimental item
952    /// and an experimental operation inside stable units.
953    const SYNTH: &[Unit] = &[
954        Unit {
955            name: "bynk",
956            kind: UnitKind::Adapter,
957            identity_path: "bynk.bynk",
958            source: "adapter bynk {\n\texports capability { Clock, Fetch }\n\texports transparent { FetchError }\n}\n",
959            binding: Binding::PerPlatform,
960            id_block: 0,
961            platform: None,
962            env_providers: &[],
963            stability: Stability::Stable,
964            experimental: &["Fetch.stream"],
965            extensible: &["FetchError"],
966            instantiable: &[],
967        },
968        Unit {
969            name: "bynk.cloudflare",
970            kind: UnitKind::Adapter,
971            identity_path: "bynk/cloudflare.bynk",
972            source: "adapter bynk.cloudflare {\n\texports capability { Kv, Ai }\n}\n",
973            binding: Binding::None,
974            id_block: 1,
975            platform: Some(Platform::Cloudflare),
976            env_providers: &[],
977            stability: Stability::Stable,
978            experimental: &["Ai"],
979            extensible: &[],
980            instantiable: &[],
981        },
982        Unit {
983            name: "bynk.cloudflare.d1",
984            kind: UnitKind::Adapter,
985            identity_path: "bynk/cloudflare/d1.bynk",
986            source: "adapter bynk.cloudflare.d1 {\n\texports capability { D1 }\n}\n",
987            binding: Binding::None,
988            id_block: 2,
989            platform: Some(Platform::Cloudflare),
990            env_providers: &[],
991            stability: Stability::Experimental,
992            experimental: &[],
993            extensible: &[],
994            instantiable: &[],
995        },
996        Unit {
997            name: "bynk.text",
998            kind: UnitKind::Commons,
999            identity_path: "bynk/text.bynk",
1000            source: "commons bynk.text\n\ntype Slug = String\n\nfn shout(s: String) -> String {\n\ts\n}\n",
1001            binding: Binding::None,
1002            id_block: 3,
1003            platform: None,
1004            env_providers: &[],
1005            stability: Stability::Stable,
1006            experimental: &[],
1007            extensible: &[],
1008            instantiable: &[],
1009        },
1010    ];
1011
1012    #[test]
1013    fn item_paths_resolve_to_the_longest_unit() {
1014        let unit_of = |p| split_item_path_in(SYNTH, p).map(|(u, rest)| (u.name, rest));
1015        assert_eq!(unit_of("bynk"), Some(("bynk", "")));
1016        assert_eq!(unit_of("bynk.Fetch.send"), Some(("bynk", "Fetch.send")));
1017        assert_eq!(
1018            unit_of("bynk.cloudflare.Kv"),
1019            Some(("bynk.cloudflare", "Kv"))
1020        );
1021        assert_eq!(
1022            unit_of("bynk.cloudflare.d1.D1"),
1023            Some(("bynk.cloudflare.d1", "D1"))
1024        );
1025        assert_eq!(unit_of("bynkx"), None, "a whole-segment prefix only");
1026        assert_eq!(unit_of("user.unit"), None);
1027    }
1028
1029    #[test]
1030    fn stability_follows_unit_item_and_operation() {
1031        let st = |p| stability_of_in(SYNTH, p);
1032        use Stability::*;
1033        assert_eq!(st("bynk.Clock"), Some(Stable));
1034        assert_eq!(st("bynk.Fetch.send"), Some(Stable));
1035        assert_eq!(
1036            st("bynk.Fetch.stream"),
1037            Some(Experimental),
1038            "an experimental operation"
1039        );
1040        assert_eq!(
1041            st("bynk.Fetch"),
1042            Some(Stable),
1043            "its capability stays stable"
1044        );
1045        assert_eq!(
1046            st("bynk.cloudflare.Ai"),
1047            Some(Experimental),
1048            "an experimental item"
1049        );
1050        assert_eq!(
1051            st("bynk.cloudflare.Ai.run"),
1052            Some(Experimental),
1053            "its operations too"
1054        );
1055        assert_eq!(st("bynk.cloudflare.Kv"), Some(Stable));
1056        assert_eq!(
1057            st("bynk.cloudflare.d1"),
1058            Some(Experimental),
1059            "an experimental unit"
1060        );
1061        assert_eq!(st("bynk.cloudflare.d1.D1.query"), Some(Experimental));
1062        assert_eq!(st("user.unit"), None);
1063    }
1064
1065    #[test]
1066    fn allow_entries_cover_by_whole_segment_prefix() {
1067        let allow = vec![
1068            "bynk.cloudflare.Ai".to_string(),
1069            "bynk.cloudflare.d1".to_string(),
1070        ];
1071        assert!(allows(&allow, "bynk.cloudflare.Ai"));
1072        assert!(allows(&allow, "bynk.cloudflare.Ai.run"));
1073        assert!(allows(&allow, "bynk.cloudflare.d1.D1.query"));
1074        assert!(
1075            !allows(&allow, "bynk.cloudflare.AiX"),
1076            "not a segment prefix"
1077        );
1078        assert!(!allows(&allow, "bynk.Fetch.stream"));
1079        assert!(!allows(&[], "bynk.cloudflare.Ai"));
1080    }
1081
1082    #[test]
1083    fn an_allow_entry_is_live_while_it_reaches_something_experimental() {
1084        let synth = experimental_items_in(SYNTH);
1085        let live = |e| allow_entry_is_live(&synth, e);
1086        assert!(live("bynk.cloudflare.Ai"));
1087        assert!(live("bynk.cloudflare.Ai.run"), "under an experimental item");
1088        assert!(live("bynk.cloudflare"), "contains an experimental item");
1089        assert!(live("bynk.Fetch.stream"));
1090        assert!(live("bynk"), "contains an experimental operation");
1091        assert!(!live("bynk.cloudflare.Kv"), "stable: a stale entry");
1092        assert!(!live("bynk.text"));
1093        assert_eq!(
1094            experimental_items_in(SYNTH),
1095            [
1096                "bynk.Fetch.stream",
1097                "bynk.cloudflare.Ai",
1098                "bynk.cloudflare.d1"
1099            ]
1100        );
1101    }
1102
1103    #[test]
1104    fn exported_names_and_item_existence_come_from_the_sources() {
1105        assert_eq!(exported_names(&SYNTH[0]), ["Clock", "Fetch", "FetchError"]);
1106        // K5a (#2586): a list `bynk-fmt` wrapped, one name a line.
1107        let wrapped = Unit {
1108            source: "adapter bynk {\n\texports transparent {\n\t\tMethod,\n\t\tRedirect,\n\t}\n\texports capability { Fetch }\n}\n",
1109            ..SYNTH[0]
1110        };
1111        assert_eq!(exported_names(&wrapped), ["Method", "Redirect", "Fetch"]);
1112        assert_eq!(exported_names(&SYNTH[3]), ["Slug", "shout"]);
1113        let names = |p| names_first_party_item_in(SYNTH, p);
1114        assert!(names("bynk.cloudflare"));
1115        assert!(names("bynk.cloudflare.Ai"));
1116        assert!(
1117            names("bynk.cloudflare.Ai.run"),
1118            "an operation of an existing item"
1119        );
1120        assert!(names("bynk.text.shout"));
1121        assert!(!names("bynk.cloudflare.Aii"), "a typo");
1122        assert!(!names("user.unit"));
1123    }
1124
1125    #[test]
1126    fn extensibility_is_by_unit_and_name() {
1127        assert!(is_extensible_in(SYNTH, "bynk", "FetchError"));
1128        assert!(
1129            !is_extensible_in(SYNTH, "user.app", "FetchError"),
1130            "a user's own FetchError"
1131        );
1132        assert!(!is_extensible_in(SYNTH, "bynk", "Method"));
1133    }
1134
1135    /// The shipped instantiable kinds: `bynk.cloudflare`'s `Kv` (S4a, #1966),
1136    /// binding as `KV_<NAME>` with no creation options, and its `Service`
1137    /// (interop I5, #1972), binding as `SERVICE_<NAME>` and naming its Worker
1138    /// with a required `@worker`, never usable bare. Each one is a capability
1139    /// its unit exports.
1140    #[test]
1141    fn the_shipped_instantiable_kinds_are_pinned() {
1142        let kinds: Vec<(&str, &str, &str)> = UNITS
1143            .iter()
1144            .flat_map(|u| {
1145                u.instantiable
1146                    .iter()
1147                    .map(move |i| (u.name, i.capability, i.binding_prefix))
1148            })
1149            .collect();
1150        assert_eq!(
1151            kinds,
1152            [
1153                ("bynk.cloudflare", "Kv", "KV"),
1154                ("bynk.cloudflare", "Service", "SERVICE"),
1155                ("bynk.cloudflare", "R2", "R2"),
1156                ("bynk.cloudflare", "RateLimit", "RATELIMIT"),
1157                ("bynk.cloudflare", "Queue", "QUEUE"),
1158                ("bynk.cloudflare", "Analytics", "ANALYTICS"),
1159                ("bynk.cloudflare", "EdgeCache", "EDGE_CACHE")
1160            ]
1161        );
1162        let kv = instantiable("bynk.cloudflare", "Kv").expect("Kv is instantiable");
1163        assert!(kv.options.is_empty() && kv.required.is_empty() && kv.bare);
1164        let service = instantiable("bynk.cloudflare", "Service").expect("Service is instantiable");
1165        assert_eq!(service.option_names().collect::<Vec<_>>(), ["worker"]);
1166        assert_eq!(service.required, ["worker"]);
1167        assert!(!service.bare);
1168        let r2 = instantiable("bynk.cloudflare", "R2").expect("R2 is instantiable");
1169        assert_eq!(r2.option_names().collect::<Vec<_>>(), ["jurisdiction"]);
1170        assert!(r2.required.is_empty() && !r2.bare);
1171        let rl = instantiable("bynk.cloudflare", "RateLimit").expect("RateLimit is instantiable");
1172        assert_eq!(rl.option_kind("limit"), Some(OptionKind::PositiveInt));
1173        assert_eq!(rl.option_kind("period"), Some(OptionKind::Duration));
1174        assert_eq!(rl.required, ["limit", "period"]);
1175        assert!(!rl.bare);
1176        let queue = instantiable("bynk.cloudflare", "Queue").expect("Queue is instantiable");
1177        assert_eq!(queue.option_kind("name"), Some(OptionKind::Str));
1178        assert_eq!(queue.required, ["name"]);
1179        assert!(!queue.bare);
1180        let analytics =
1181            instantiable("bynk.cloudflare", "Analytics").expect("Analytics is instantiable");
1182        assert!(analytics.options.is_empty() && !analytics.bare);
1183        let edge = instantiable("bynk.cloudflare", "EdgeCache").expect("EdgeCache is instantiable");
1184        assert!(edge.options.is_empty() && !edge.bare);
1185        for u in UNITS {
1186            for i in u.instantiable {
1187                assert!(
1188                    i.required.iter().all(|r| i.option_kind(r).is_some()),
1189                    "{}.{}: a required option must be an option",
1190                    u.name,
1191                    i.capability
1192                );
1193            }
1194        }
1195        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Kv"));
1196        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Service"));
1197        assert_eq!(instantiable("bynk", "Clock"), None);
1198        assert_eq!(instantiable("bynk.cloudflare", "Nope"), None);
1199    }
1200
1201    /// The shipped registry: every unit is stable (S3 ships the mechanism,
1202    /// #1963 Decision C) but `bynk.crypto` (K4), which is experimental until the
1203    /// first-party registry has signed with it in production. The one
1204    /// experimental item inside a stable unit is `bynk.cloudflare`'s `Service`
1205    /// (interop I5, #1972), and the growing enums are extensible. A slice that
1206    /// marks an item experimental updates this test, so the change is visible
1207    /// in review.
1208    #[test]
1209    fn the_shipped_stability_is_pinned() {
1210        for u in UNITS {
1211            let expected = if u.name == CRYPTO_UNIT {
1212                Stability::Experimental
1213            } else {
1214                Stability::Stable
1215            };
1216            assert_eq!(u.stability, expected, "{}", u.name);
1217        }
1218        assert_eq!(
1219            experimental_items(),
1220            [
1221                "bynk.Fetch.sendWith",
1222                "bynk.Logger.log",
1223                "bynk.cloudflare.Service",
1224                "bynk.cloudflare.R2",
1225                "bynk.cloudflare.RateLimit",
1226                "bynk.cloudflare.EdgeCache",
1227                "bynk.cloudflare.Queue",
1228                "bynk.cloudflare.Analytics",
1229                "bynk.cloudflare.Kv.getWith",
1230                "bynk.cloudflare.Kv.putWith",
1231                "bynk.cloudflare.Kv.getBytes",
1232                "bynk.cloudflare.Kv.putBytes",
1233                "bynk.cloudflare.Kv.listPage",
1234                "bynk.crypto"
1235            ]
1236        );
1237        let extensible: Vec<(&str, &str)> = UNITS
1238            .iter()
1239            .flat_map(|u| u.extensible.iter().map(move |e| (u.name, *e)))
1240            .collect();
1241        assert_eq!(
1242            extensible,
1243            [
1244                ("bynk", "FetchError"),
1245                ("bynk", "Method"),
1246                ("bynk", "Redirect"),
1247                ("bynk.cloudflare", "R2Condition"),
1248                ("bynk.cloudflare", "KvExpiration"),
1249                ("bynk.crypto", "Digest"),
1250                ("bynk.crypto", "Signature"),
1251                ("bynk.crypto", "CryptoError"),
1252            ]
1253        );
1254        for (unit, name) in extensible {
1255            assert!(
1256                exported_names(self::unit(unit).unwrap()).contains(&name),
1257                "{unit} lists {name} as extensible but does not export it"
1258            );
1259        }
1260        assert!(names_first_party_item("bynk.cloudflare.Kv.list"));
1261        assert!(names_first_party_item("bynk.list.reverse"));
1262        assert!(!names_first_party_item("bynk.cloudflare.Kvv"));
1263    }
1264
1265    /// #1958: `WorkersKv` reads the namespace at the name every generated
1266    /// Worker declares it under (`[[kv_namespaces]] binding`, its `Env`), the
1267    /// one TypeScript literal no Rust constructor can supply. Since S4a (#1966)
1268    /// the name is a constructor parameter, so a derived capability's instance
1269    /// can pass its own; the bare `Kv`'s is the parameter's default.
1270    ///
1271    /// Matched as a whole identifier, then by its neighbours, so the test
1272    /// ignores formatting but not a rename: `KVX` is not `KV` (review of
1273    /// #1962).
1274    #[test]
1275    fn the_cloudflare_binding_reads_kv_binding_name() {
1276        let ident = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
1277        let src = CLOUDFLARE_BINDING;
1278        let uses: Vec<(&str, &str)> = src
1279            .match_indices(KV_BINDING_NAME)
1280            .map(|(at, name)| (&src[..at], &src[at + name.len()..]))
1281            .filter(|(before, after)| !before.ends_with(ident) && !after.starts_with(ident))
1282            .collect();
1283        assert!(
1284            uses.iter().any(|(b, a)| {
1285                b.trim_end().ends_with("binding: string = \"") && a.starts_with('"')
1286            }),
1287            "the constructor's `binding` no longer defaults to \"{KV_BINDING_NAME}\""
1288        );
1289        assert!(
1290            src.contains("?.[this.binding]"),
1291            "the namespace is no longer read at `env[binding]`"
1292        );
1293    }
1294
1295    /// Each unit's `ExprId` block is permanent: emitted output and the
1296    /// process-wide parse cache depend on it. These are the blocks the
1297    /// hand-written injection used before the registry (S1, #1945); a new
1298    /// unit takes a new block and never moves one of these.
1299    #[test]
1300    fn id_blocks_are_pinned_and_distinct() {
1301        let blocks: Vec<(&str, u32)> = UNITS.iter().map(|u| (u.name, u.id_block)).collect();
1302        assert_eq!(
1303            blocks,
1304            [
1305                ("bynk", 0),
1306                ("bynk.cloudflare", 1),
1307                ("bynk.list", 3),
1308                ("bynk.map", 2),
1309                ("bynk.string", 4),
1310                ("bynk.locale", 6),
1311                ("bynk.locale.types", 5),
1312                ("bynk.crypto", 7),
1313                ("bynk.url", 8),
1314                ("bynk.set", 9),
1315            ]
1316        );
1317        let mut sorted: Vec<u32> = UNITS.iter().map(|u| u.id_block).collect();
1318        sorted.sort_unstable();
1319        sorted.dedup();
1320        assert_eq!(sorted.len(), UNITS.len(), "two units share an id block");
1321    }
1322
1323    /// Hover and completion read [`FIRSTPARTY_SOURCES`] in order, so it is
1324    /// pinned to the order the hand-written list had.
1325    #[test]
1326    fn firstparty_sources_keep_their_order() {
1327        let names: Vec<&str> = FIRSTPARTY_SOURCES.iter().map(|(n, _)| *n).collect();
1328        assert_eq!(
1329            names,
1330            [
1331                "bynk",
1332                "bynk.cloudflare",
1333                "bynk.list",
1334                "bynk.map",
1335                "bynk.string",
1336                "bynk.locale",
1337                "bynk.locale.types",
1338                "bynk.crypto",
1339                "bynk.url",
1340                "bynk.set",
1341            ]
1342        );
1343        for (unit, (name, source)) in UNITS.iter().zip(FIRSTPARTY_SOURCES) {
1344            assert_eq!((unit.name, unit.source), (*name, *source));
1345        }
1346    }
1347
1348    /// The views over the table answer as the hand-written matches did.
1349    #[test]
1350    fn metadata_views_match_the_previous_answers() {
1351        assert_eq!(platform_of("bynk.cloudflare"), Some(Platform::Cloudflare));
1352        for name in [
1353            "bynk",
1354            "bynk.list",
1355            "bynk.map",
1356            "bynk.string",
1357            "bynk.locale",
1358            "user.unit",
1359        ] {
1360            assert_eq!(platform_of(name), None, "{name}");
1361        }
1362        assert!(provider_takes_env("bynk", "SecretsProvider"));
1363        assert!(provider_takes_env("bynk.cloudflare", "WorkersKv"));
1364        assert!(!provider_takes_env("bynk", "WorkersKv"));
1365        assert!(!provider_takes_env("bynk.cloudflare", "SecretsProvider"));
1366        assert!(!provider_takes_env("bynk", "ClockProvider"));
1367        assert!(!provider_takes_env("user.unit", "SecretsProvider"));
1368    }
1369
1370    /// An adapter carries a binding and a commons does not, and every name and
1371    /// identity path is unique, every name inside the reserved prefix.
1372    #[test]
1373    fn entries_are_well_formed() {
1374        for u in UNITS {
1375            assert!(
1376                u.name == "bynk" || u.name.starts_with("bynk."),
1377                "{}",
1378                u.name
1379            );
1380            assert_eq!(
1381                UNITS.iter().filter(|o| o.name == u.name).count(),
1382                1,
1383                "{}",
1384                u.name
1385            );
1386            // Two entries under one identity path would be two parsed units
1387            // that diagnostics and emission cannot tell apart.
1388            assert_eq!(
1389                UNITS
1390                    .iter()
1391                    .filter(|o| o.identity_path == u.identity_path)
1392                    .count(),
1393                1,
1394                "{}",
1395                u.identity_path
1396            );
1397            match u.kind {
1398                UnitKind::Adapter => assert_ne!(u.binding, Binding::None, "{}", u.name),
1399                UnitKind::Commons => {
1400                    assert_eq!(u.binding, Binding::None, "{}", u.name);
1401                    assert!(u.env_providers.is_empty(), "{}", u.name);
1402                }
1403            }
1404        }
1405        assert_eq!(
1406            unit("bynk.cloudflare").and_then(|u| u.binding_for(Platform::Node)),
1407            Some((CLOUDFLARE_BINDING_FILENAME, CLOUDFLARE_BINDING))
1408        );
1409        assert_eq!(
1410            unit("bynk").and_then(|u| u.binding_for(Platform::Browser)),
1411            Some(("bynk-browser.ts", BYNK_BROWSER_BINDING))
1412        );
1413        assert_eq!(
1414            unit("bynk.list").and_then(|u| u.binding_for(Platform::Node)),
1415            None
1416        );
1417    }
1418
1419    /// The three `bynk.crypto` bindings are one body with one marked region of
1420    /// platform difference (the constant-time comparison primitive). They are
1421    /// separate files because each ships alone, so this keeps them from drifting.
1422    #[test]
1423    fn the_crypto_bindings_differ_only_in_their_platform_region() {
1424        const START: &str = "// --- platform: constant-time comparison primitive";
1425        const END: &str = "// --- end platform";
1426        fn shared(src: &str) -> String {
1427            let (head, rest) = src.split_once(START).expect("platform region start");
1428            let (_, tail) = rest.split_once(END).expect("platform region end");
1429            // The second header line names the platform.
1430            let head = head.lines().skip(2).collect::<Vec<_>>().join("\n");
1431            format!("{head}\n{tail}")
1432        }
1433        let cloudflare = shared(CRYPTO_CLOUDFLARE_BINDING);
1434        assert_eq!(cloudflare, shared(CRYPTO_NODE_BINDING));
1435        assert_eq!(cloudflare, shared(CRYPTO_BROWSER_BINDING));
1436    }
1437
1438    /// The hardcoded [`BYNK_SURFACE_CAPABILITIES`] must match the adapter's
1439    /// `exports capability { … }` line — otherwise the unknown-name quick-fix
1440    /// (#852) would offer a stale `consumes bynk { … }` set.
1441    #[test]
1442    fn bynk_surface_capabilities_match_the_adapter() {
1443        let src = BYNK_ADAPTER_SRC;
1444        let line = src
1445            .lines()
1446            .find(|l| l.trim_start().starts_with("exports capability {"))
1447            .expect("adapter declares `exports capability`");
1448        let inner = line
1449            .split_once('{')
1450            .and_then(|(_, r)| r.split_once('}'))
1451            .map(|(names, _)| names)
1452            .expect("braced capability list");
1453        let declared: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1454        assert_eq!(
1455            declared, BYNK_SURFACE_CAPABILITIES,
1456            "BYNK_SURFACE_CAPABILITIES is out of sync with bynk.bynk"
1457        );
1458    }
1459}