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