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}