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