Skip to main content

bynk_std/
lib.rs

1//! The Bynk standard library's first-party sources, as data.
2//!
3//! The `bynk` conformance surface, the Bynk-written commons (`bynk.list`,
4//! `bynk.map`, `bynk.string`, `bynk.locale`, `bynk.locale.types`) and the
5//! `bynk.cloudflare` platform adapter ship with the compiler rather than being
6//! authored by the user. This crate holds their `.bynk` sources, their
7//! per-platform TypeScript bindings and the metadata the toolchain reads about
8//! them, embedded with `include_str!`. It holds no logic: `bynk-check` parses and
9//! injects these sources when a project `uses` or `consumes` them, and provides
10//! each binding for the selected [`Platform`]. The `bynk` root namespace is
11//! reserved (`bynk.namespace.reserved`) so user code can never collide with it.
12//!
13//! The sources live under `lib/`: the portable layers in `lib/core/` (with the
14//! `bynk` surface's bindings in `lib/core/bindings/`) and the platform adapter in
15//! `lib/cloudflare/`. Nothing in `lib/core/` depends on `lib/cloudflare/`, so the
16//! platform layer can be split out later without renaming a unit.
17
18/// The deploy platform — a selection axis distinct from the `--target
19/// {bundle,workers}` emit mode (§6.2). It chooses which `bynk-<platform>.ts`
20/// binding is linked for the `bynk` surface. v0.17 shipped `cloudflare`;
21/// v0.18 adds `node`, making the axis observable (and giving v0.19's
22/// platform-lock enforcement a second platform to fire against).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
24pub enum Platform {
25    #[default]
26    Cloudflare,
27    Node,
28    /// The browser (in-browser track, slice 1 — the REPL/playground host). A
29    /// Tier-3 binding exposing the `bynk` surface over Web APIs, composed with
30    /// `BuildTarget::Bundle` only (a browser cannot do the Workers wire model).
31    Browser,
32}
33
34impl Platform {
35    /// The output filename of this platform's `bynk` binding module.
36    pub fn bynk_binding_filename(self) -> &'static str {
37        match self {
38            Platform::Cloudflare => "bynk-cloudflare.ts",
39            Platform::Node => "bynk-node.ts",
40            Platform::Browser => "bynk-browser.ts",
41        }
42    }
43
44    /// The TypeScript source of this platform's `bynk` binding.
45    pub fn bynk_binding_source(self) -> &'static str {
46        match self {
47            Platform::Cloudflare => BYNK_CLOUDFLARE_BINDING,
48            Platform::Node => BYNK_NODE_BINDING,
49            Platform::Browser => BYNK_BROWSER_BINDING,
50        }
51    }
52
53    /// The platform's stable name (for the `--platform` flag and diagnostics).
54    pub fn as_str(self) -> &'static str {
55        match self {
56            Platform::Cloudflare => "cloudflare",
57            Platform::Node => "node",
58            Platform::Browser => "browser",
59        }
60    }
61}
62
63/// The unit name of the reserved first-party surface adapter.
64pub const BYNK_UNIT: &str = "bynk";
65
66/// The capabilities the env-free `bynk` surface exports (its `exports
67/// capability { … }` line in `bynk.bynk`). First-party symbols live on
68/// synthetic files and are excluded from the binding index, so the
69/// unknown-name quick-fix (#852) offering `consumes bynk { … }` needs this
70/// explicit list. Kept in sync with the adapter source by
71/// `bynk_surface_capabilities_match_the_adapter`.
72pub const BYNK_SURFACE_CAPABILITIES: &[&str] = &[
73    "Clock",
74    "Random",
75    "Logger",
76    "Fetch",
77    "Secrets",
78    "Locale",
79    "Idempotency",
80    "Events",
81];
82
83/// The unit name of the first-party Cloudflare platform adapter (v0.19,
84/// decision 0026): inside the reserved `bynk.*` prefix, so no separate
85/// reservation rule is needed. The surface unit `bynk` stays the portability
86/// marker; `bynk.<platform>` units are the platform-locked ones.
87pub const CLOUDFLARE_UNIT: &str = "bynk.cloudflare";
88
89/// The fixed Worker binding name for the Kv namespace (decision C1, v0.19):
90/// one namespace, one `[[kv_namespaces]]` stanza, one `env.KV` field.
91pub const KV_BINDING_NAME: &str = "KV";
92
93/// v0.18 decision 0021 / v0.19 decision 0025: which first-party provider
94/// classes take the Worker `env` as a constructor argument, keyed by
95/// (unit, provider class). `SecretsProvider` accepts an optional env with a
96/// `globalThis` probe fallback; `WorkersKv` *requires* env on use — KV
97/// namespaces exist only on the Worker `env`, never on `globalThis`.
98///
99/// A view over [`UNITS`]: each entry's [`Unit::env_providers`].
100pub fn provider_takes_env(unit: &str, provider: &str) -> bool {
101    self::unit(unit).is_some_and(|u| u.env_providers.contains(&provider))
102}
103
104/// v0.19 decision 0024: which first-party units are **platform-native** —
105/// consuming one locks its deployment unit to the returned platform. This is
106/// the metadata that drives effective-platform computation, `env` resource
107/// typing, and `wrangler.toml` stanza derivation; no user-facing marker
108/// syntax exists (additive later, when third-party platform adapters become
109/// a goal).
110///
111/// A view over [`UNITS`]: each entry's [`Unit::platform`].
112pub fn platform_of(unit: &str) -> Option<Platform> {
113    self::unit(unit).and_then(|u| u.platform)
114}
115
116/// The unit names of the first-party collection commons (v0.20b): the
117/// Bynk-written combinator stdlib over the built-in `List`/`Map` kernel.
118/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported.
119pub const LIST_UNIT: &str = "bynk.list";
120pub const MAP_UNIT: &str = "bynk.map";
121
122/// `bynk.list` — combinators over the `List` kernel (`fold`, `prepend`,
123/// `length`, `get`, `foldEff`), written in ordinary Bynk (decision 0034):
124/// the first real consumer of v0.20a generics, lambdas, and effectful
125/// traversal. Order-preserving combinators build with `fold` + `prepend`
126/// and a final `reverse` — O(n) builds, never `append` (which would be
127/// O(n²) over the array lowering).
128pub const BYNK_LIST_SRC: &str = include_str!("../lib/core/bynk.list.bynk");
129
130/// `bynk.map` — combinators over the `Map` kernel (`empty`, `insert`, `get`,
131/// `keys`, `length`). `fromList` is deliberately absent: Bynk has no pair
132/// type to spell a `List[(K, V)]` with, so map construction is `Map.empty()`
133/// + `insert` (revisit with tuples or generic records).
134pub const BYNK_MAP_SRC: &str = include_str!("../lib/core/bynk.map.bynk");
135
136/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported.
137pub const STRING_UNIT: &str = "bynk.string";
138
139/// `bynk.string` — Bynk-written helpers over the v0.22a string kernel
140/// (`concat`, the `List` `fold`, and the `Option` kernel methods). The
141/// kernel itself is compiler built-in (ADR 0046); only derived helpers
142/// live here. `join` folds to `Option[String]` so empty-string *elements*
143/// are joined faithfully (a bare `""` accumulator could not tell "nothing
144/// yet" from "first element was empty").
145pub const BYNK_STRING_SRC: &str = include_str!("../lib/core/bynk.string.bynk");
146
147/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported by
148/// application code (a message-bundle commons, ADR 0272) — never directly by
149/// the `bynk` adapter itself any more (see [`LOCALE_TYPES_UNIT`]).
150pub const LOCALE_UNIT: &str = "bynk.locale";
151
152/// `bynk.locale` — the bundle-free `render`/`renderArg` helpers and the
153/// `message`/`withText`/`withWhole`/`withNum`/`withMoment` builder API
154/// (slice 1: `render` formats `code` plus a deterministic sorted-by-key
155/// rendering of `params`; no lookup, no locale-dependent behaviour). `uses
156/// bynk.locale.types` for the types its own signatures name.
157pub const BYNK_LOCALE_SRC: &str = include_str!("../lib/core/bynk.locale.bynk");
158
159/// Inside the reserved `bynk.*` prefix; injected when `uses`-imported. Also
160/// `uses`-imported by the `bynk` adapter itself, to give `capability Locale`'s
161/// `current() -> Effect[LocaleTag]` a type to name (an adapter may `uses` a
162/// commons; a commons cannot in turn reference an adapter). Split out of
163/// `bynk.locale` (locale-negotiation-slice-2 follow-up, #886) specifically so
164/// a context can reach `LocaleTag` without also pulling in `bynk.locale`'s
165/// own `render` — which previously collided with a message-bundle commons's
166/// own synthesised `render` (ADR 0272) the moment a context needed both.
167pub const LOCALE_TYPES_UNIT: &str = "bynk.locale.types";
168
169/// `bynk.locale.types` — `LocaleTag`, `MessageArg`, `Message`: a
170/// dependency-free leaf (no `uses` of its own) so any consumer, including
171/// the `bynk` adapter itself, can reach these types without also reaching
172/// `bynk.locale`'s value-level API.
173pub const BYNK_LOCALE_TYPES_SRC: &str = include_str!("../lib/core/bynk.locale.types.bynk");
174
175/// The reserved `bynk` conformance-surface adapter (env-free core). It has no
176/// `binding` clause — the toolchain supplies one per platform (see
177/// [`Platform::bynk_binding_source`]).
178pub const BYNK_ADAPTER_SRC: &str = include_str!("../lib/core/bynk.bynk");
179
180/// The Cloudflare binding for the `bynk` surface. Implements the canonical
181/// provider symbols against the platform host API. The refined `Uuid` is built
182/// through its emitted validating `.of` constructor (§4.4), treating the
183/// unreachable `Err` as a bug rather than trusting the value away.
184const BYNK_CLOUDFLARE_BINDING: &str = include_str!("../lib/core/bindings/bynk-cloudflare.ts");
185
186/// The Node (≥ the toolchain's `NODE_MAJOR_FLOOR`, in `bynk-emit`) binding for
187/// the `bynk` surface (v0.18). Deliberately
188/// near-identical to the cloudflare binding: `Date.now`, the global
189/// `crypto`/`fetch`, and `console` are the same host API on both runtimes —
190/// which is exactly the ambient-surface portability claim (spec §4.2). The
191/// `SecretsProvider` reads `process.env` through the same `globalThis` probe
192/// (never bare `process`, which would demand @types/node at the tsc gate).
193const BYNK_NODE_BINDING: &str = include_str!("../lib/core/bindings/bynk-node.ts");
194
195/// The Browser binding for the `bynk` surface (in-browser track, slice 1). Tier-3:
196/// the same capability surface over Web APIs. `Clock`/`Random`/`Logger` are
197/// byte-identical to the Node binding (`Date.now`, Web Crypto `crypto.randomUUID`,
198/// `console` — all Web standards); the two substitutions are the playground safety
199/// boundary — `Fetch` is **withheld** (throws; no outbound egress from the
200/// playground origin) and `Secrets` is **unavailable** (throws; a browser has no
201/// secret store), both failing loudly rather than silently degrading.
202const BYNK_BROWSER_BINDING: &str = include_str!("../lib/core/bindings/bynk-browser.ts");
203
204/// The first-party Cloudflare platform adapter (v0.19): the platform's real
205/// infrastructure capabilities, as they are — no portable intersection
206/// (decision 0016). The v0.19 surface was the minimal, collection-free `Kv`
207/// (decision 0023); v0.23 adds the `list` drain and `putTtl` (0050/0051);
208/// structured values are v0.22-codec composition, and `Queue` remains its
209/// own future increment. Like the `bynk` surface it has no `binding`
210/// clause — the toolchain supplies the binding.
211pub const CLOUDFLARE_ADAPTER_SRC: &str = include_str!("../lib/cloudflare/bynk.cloudflare.bynk");
212
213/// Every first-party `.bynk` source, paired with its unit name — the single
214/// list every consumer iterates: hover (`describe_firstparty_symbol`),
215/// completion (`EMBEDDED_UNITS`), and the parse guard
216/// (`every_first_party_source_parses`). Before this list each consumer kept its
217/// own hand-maintained copy, and `bynk.locale`/`bynk.locale.types` were added to
218/// none of them — so the whole locale surface hovered as nothing and was absent
219/// from completion (#901).
220///
221/// A view over [`UNITS`], in its order, which is observable: hover takes the
222/// first source declaring a name, and completion lists units in this order. The
223/// `firstparty_sources_cover_every_src_const` drift guard
224/// (`bynkc/tests/firstparty_sources.rs`) fails if a `*_SRC` const is left out of
225/// [`UNITS`].
226pub const FIRSTPARTY_SOURCES: &[(&str, &str)] = &SOURCES;
227
228/// [`FIRSTPARTY_SOURCES`]' backing array, built from [`UNITS`] at compile time.
229const SOURCES: [(&str, &str); UNITS.len()] = {
230    let mut out = [("", ""); UNITS.len()];
231    let mut i = 0;
232    while i < UNITS.len() {
233        out[i] = (UNITS[i].name, UNITS[i].source);
234        i += 1;
235    }
236    out
237};
238
239/// The output path of the Cloudflare platform adapter's binding module,
240/// beside the adapter's emitted `bynk/cloudflare.ts` (distinct from the
241/// `bynk` *surface*'s per-platform `bynk-cloudflare.ts`).
242pub const CLOUDFLARE_BINDING_FILENAME: &str = "bynk/cloudflare.binding.ts";
243
244/// The Cloudflare platform adapter's binding. `WorkersKv` reads the Worker
245/// `env` explicitly (decision 0025): KV namespaces exist only on `env` —
246/// there is no `globalThis` path — so a missing binding is a clear runtime
247/// error rather than a silent fallback.
248const CLOUDFLARE_BINDING: &str = include_str!("../lib/cloudflare/cloudflare.binding.ts");
249
250/// The toolchain-supplied binding for the Cloudflare platform adapter.
251pub fn cloudflare_binding_source() -> &'static str {
252    CLOUDFLARE_BINDING
253}
254
255/// What a first-party unit is, and so how a project brings it in: an adapter is
256/// injected when a unit `consumes` it, a commons when a unit `uses` it. Local to
257/// this crate, which has no dependencies; `bynk-check` maps it onto its own
258/// unit kind.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum UnitKind {
261    Adapter,
262    Commons,
263}
264
265/// The TypeScript binding the toolchain supplies for a first-party unit.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum Binding {
268    /// A commons: Bynk-written, so nothing to bind.
269    None,
270    /// One binding per [`Platform`] ([`Platform::bynk_binding_filename`] and
271    /// [`Platform::bynk_binding_source`]): the portable `bynk` surface.
272    PerPlatform,
273    /// A single binding, for a platform-native adapter that runs only on its own
274    /// platform (the platform lock rejects every other `--platform`).
275    Fixed {
276        filename: &'static str,
277        source: &'static str,
278    },
279}
280
281/// One first-party unit, as data (standard-library track, S1 #1945). Adding a
282/// unit to the standard library is an entry in [`UNITS`] plus its files; the
283/// checker's injection reads this table and needs no change.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct Unit {
286    /// The unit's dotted name, inside the reserved `bynk.*` prefix.
287    pub name: &'static str,
288    /// Whether a project `consumes` it (an adapter) or `uses` it (a commons).
289    pub kind: UnitKind,
290    /// The synthetic path the injected unit is parsed under. Diagnostics and
291    /// emission name the unit by it, so it never changes.
292    pub identity_path: &'static str,
293    /// The unit's `.bynk` source.
294    pub source: &'static str,
295    /// The binding the toolchain supplies for it.
296    pub binding: Binding,
297    /// Its permanent `ExprId` block: the checker parses it at
298    /// `FIRSTPARTY_ID_BASE + id_block * FIRSTPARTY_ID_BLOCK`, and injects units
299    /// in ascending `id_block` order. A block is never reused or moved, so ids
300    /// and injection order stay stable as units are added; a new unit takes the
301    /// next free block.
302    pub id_block: u32,
303    /// The platform that consuming it locks a deployment unit to (decision
304    /// 0024), if it is platform-native.
305    pub platform: Option<Platform>,
306    /// The provider classes whose constructor takes the Worker `env` (decisions
307    /// 0021/0025).
308    pub env_providers: &'static [&'static str],
309    /// The unit's stability (track D2, S3 #1963): a stable item is under the
310    /// 1.0 promise, an experimental one carries none and needs an explicit
311    /// `[unstable] allow` to use. Required, so no entry is added without a
312    /// choice.
313    pub stability: Stability,
314    /// Items inside a stable unit that are experimental, as paths relative to
315    /// the unit: an exported item (`"Kv"`) or a capability operation
316    /// (`"Kv.list"`). An experimental item makes its operations experimental.
317    pub experimental: &'static [&'static str],
318    /// The exported enums that may gain variants in a minor release, so a
319    /// `match` covering one needs a wildcard (track §11 Q5).
320    pub extensible: &'static [&'static str],
321    /// The capabilities a context may declare named instances of with
322    /// `capability Sessions = Kv` (S4a, #1966): each its own platform resource.
323    /// A capability not listed (Workers AI's single `[ai]` binding, say) has no
324    /// instances.
325    pub instantiable: &'static [Instantiable],
326}
327
328/// A capability a context may declare instances of (S4a, #1966, track D3).
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub struct Instantiable {
331    /// The capability's name in its unit (`Kv`).
332    pub capability: &'static str,
333    /// The prefix of each instance's Worker binding name: `KV` makes
334    /// `capability Sessions = Kv` bind as `KV_SESSIONS`.
335    pub binding_prefix: &'static str,
336    /// The creation options (`@name(…)`) an instance may carry; any other is
337    /// refused. Empty when the kind takes none yet. Each takes one string
338    /// literal (`@worker("legacy-billing")`).
339    pub options: &'static [&'static str],
340    /// The options every instance must carry (interop I5, #1972): a
341    /// `Service` names the Worker it reaches, so it has no default. A subset
342    /// of [`Instantiable::options`].
343    pub required: &'static [&'static str],
344    /// Whether the base capability is usable bare, through `given Kv`, as
345    /// well as through instances. `false` for a kind whose every use needs an
346    /// option: a bare `Service` would name no Worker.
347    pub bare: bool,
348}
349
350/// The Worker binding prefix of a `Service` instance (interop I5, #1972):
351/// `capability Billing = Service @worker("legacy-billing")` binds as
352/// `SERVICE_BILLING`, a `[[services]]` entry naming `legacy-billing`.
353pub const SERVICE_BINDING_PREFIX: &str = "SERVICE";
354
355/// The option of a `Service` instance that names the Worker it reaches.
356/// `worker`, not D4's `service`: `service` is a keyword, so `@service(…)`
357/// does not parse, and widening the annotation grammar is language work
358/// (ADR 0023).
359pub const SERVICE_WORKER_OPTION: &str = "worker";
360
361/// Whether a first-party item is under the 1.0 promise (track D2).
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub enum Stability {
364    /// Under the promise: a stable L0/L1 item changes only at a major release; a
365    /// stable L2/L3 item may gain operations and, if extensible, variants.
366    Stable,
367    /// No promise: it may change or disappear in any release, and using it needs
368    /// an `[unstable] allow` entry in `bynk.toml`.
369    Experimental,
370}
371
372impl Unit {
373    /// The binding module to emit for this unit on `platform`, as
374    /// `(output path, source)`, or `None` for a commons.
375    pub fn binding_for(&self, platform: Platform) -> Option<(&'static str, &'static str)> {
376        match self.binding {
377            Binding::None => None,
378            Binding::PerPlatform => Some((
379                platform.bynk_binding_filename(),
380                platform.bynk_binding_source(),
381            )),
382            Binding::Fixed { filename, source } => Some((filename, source)),
383        }
384    }
385}
386
387/// The standard library's first-party units. Dependencies between them are not
388/// listed here: they are the `uses`/`consumes` lines of each unit's own source,
389/// which the checker follows when it injects. The table's order is
390/// [`FIRSTPARTY_SOURCES`]' order; injection follows [`Unit::id_block`].
391pub const UNITS: &[Unit] = &[
392    Unit {
393        name: BYNK_UNIT,
394        kind: UnitKind::Adapter,
395        identity_path: "bynk.bynk",
396        source: BYNK_ADAPTER_SRC,
397        binding: Binding::PerPlatform,
398        id_block: 0,
399        platform: None,
400        env_providers: &["SecretsProvider"],
401        stability: Stability::Stable,
402        experimental: &[],
403        extensible: &["FetchError", "Method"],
404        instantiable: &[],
405    },
406    Unit {
407        name: CLOUDFLARE_UNIT,
408        kind: UnitKind::Adapter,
409        identity_path: "bynk/cloudflare.bynk",
410        source: CLOUDFLARE_ADAPTER_SRC,
411        binding: Binding::Fixed {
412            filename: CLOUDFLARE_BINDING_FILENAME,
413            source: CLOUDFLARE_BINDING,
414        },
415        id_block: 1,
416        platform: Some(Platform::Cloudflare),
417        env_providers: &["WorkersKv", "WorkersService"],
418        stability: Stability::Stable,
419        // Interop I5 (#1972): a Service Binding to a non-Bynk Worker is new
420        // and its surface is untyped, so it lands outside the 1.0 promise.
421        experimental: &["Service"],
422        extensible: &[],
423        instantiable: &[
424            Instantiable {
425                capability: "Kv",
426                binding_prefix: KV_BINDING_NAME,
427                options: &[],
428                required: &[],
429                bare: true,
430            },
431            Instantiable {
432                capability: "Service",
433                binding_prefix: SERVICE_BINDING_PREFIX,
434                options: &[SERVICE_WORKER_OPTION],
435                required: &[SERVICE_WORKER_OPTION],
436                bare: false,
437            },
438        ],
439    },
440    Unit {
441        name: LIST_UNIT,
442        kind: UnitKind::Commons,
443        identity_path: "bynk/list.bynk",
444        source: BYNK_LIST_SRC,
445        binding: Binding::None,
446        id_block: 3,
447        platform: None,
448        env_providers: &[],
449        stability: Stability::Stable,
450        experimental: &[],
451        extensible: &[],
452        instantiable: &[],
453    },
454    Unit {
455        name: MAP_UNIT,
456        kind: UnitKind::Commons,
457        identity_path: "bynk/map.bynk",
458        source: BYNK_MAP_SRC,
459        binding: Binding::None,
460        id_block: 2,
461        platform: None,
462        env_providers: &[],
463        stability: Stability::Stable,
464        experimental: &[],
465        extensible: &[],
466        instantiable: &[],
467    },
468    Unit {
469        name: STRING_UNIT,
470        kind: UnitKind::Commons,
471        identity_path: "bynk/string.bynk",
472        source: BYNK_STRING_SRC,
473        binding: Binding::None,
474        id_block: 4,
475        platform: None,
476        env_providers: &[],
477        stability: Stability::Stable,
478        experimental: &[],
479        extensible: &[],
480        instantiable: &[],
481    },
482    Unit {
483        name: LOCALE_UNIT,
484        kind: UnitKind::Commons,
485        identity_path: "bynk/locale.bynk",
486        source: BYNK_LOCALE_SRC,
487        binding: Binding::None,
488        id_block: 6,
489        platform: None,
490        env_providers: &[],
491        stability: Stability::Stable,
492        experimental: &[],
493        extensible: &[],
494        instantiable: &[],
495    },
496    Unit {
497        name: LOCALE_TYPES_UNIT,
498        kind: UnitKind::Commons,
499        identity_path: "bynk/locale/types.bynk",
500        source: BYNK_LOCALE_TYPES_SRC,
501        binding: Binding::None,
502        id_block: 5,
503        platform: None,
504        env_providers: &[],
505        stability: Stability::Stable,
506        experimental: &[],
507        extensible: &[],
508        instantiable: &[],
509    },
510];
511
512/// The registry entry for `name`, if it is a first-party unit.
513pub fn unit(name: &str) -> Option<&'static Unit> {
514    UNITS.iter().find(|u| u.name == name)
515}
516
517/// The unit a dotted item path lies in, and the rest of the path relative to it
518/// (empty for the unit itself). Unit names contain dots, so the longest unit
519/// name that is a whole-segment prefix wins: `bynk.cloudflare.Kv` is in
520/// `bynk.cloudflare`, not in `bynk`.
521pub fn split_item_path(path: &str) -> Option<(&'static Unit, &str)> {
522    split_item_path_in(UNITS, path)
523}
524
525/// [`split_item_path`] over a given table (tests pass a synthetic one).
526pub fn split_item_path_in<'u, 'p>(units: &'u [Unit], path: &'p str) -> Option<(&'u Unit, &'p str)> {
527    units
528        .iter()
529        .filter_map(|u| {
530            if path == u.name {
531                Some((u, ""))
532            } else {
533                path.strip_prefix(u.name)?
534                    .strip_prefix('.')
535                    .map(|rest| (u, rest))
536            }
537        })
538        .max_by_key(|(u, _)| u.name.len())
539}
540
541/// Whether `path` is `prefix` or lies under it, by whole dotted segments.
542pub fn covers(prefix: &str, path: &str) -> bool {
543    path == prefix
544        || path
545            .strip_prefix(prefix)
546            .is_some_and(|rest| rest.starts_with('.'))
547}
548
549/// The stability of a first-party item: a unit (`bynk.cloudflare`), an
550/// exported item (`bynk.cloudflare.Kv`) or a capability operation
551/// (`bynk.cloudflare.Kv.list`). An item in an experimental unit, or under an
552/// experimental item, is experimental. `None` when no first-party unit holds
553/// the path.
554pub fn stability_of(path: &str) -> Option<Stability> {
555    stability_of_in(UNITS, path)
556}
557
558/// [`stability_of`] over a given table.
559pub fn stability_of_in(units: &[Unit], path: &str) -> Option<Stability> {
560    let (unit, rest) = split_item_path_in(units, path)?;
561    if unit.stability == Stability::Experimental
562        || unit.experimental.iter().any(|item| covers(item, rest))
563    {
564        Some(Stability::Experimental)
565    } else {
566        Some(Stability::Stable)
567    }
568}
569
570/// Every experimental item, as a full path: each experimental unit, and each
571/// listed item of a stable one.
572pub fn experimental_items() -> Vec<String> {
573    experimental_items_in(UNITS)
574}
575
576/// [`experimental_items`] over a given table.
577pub fn experimental_items_in(units: &[Unit]) -> Vec<String> {
578    let mut out = Vec::new();
579    for u in units {
580        if u.stability == Stability::Experimental {
581            out.push(u.name.to_string());
582        }
583        out.extend(
584            u.experimental
585                .iter()
586                .map(|item| format!("{}.{item}", u.name)),
587        );
588    }
589    out
590}
591
592/// Whether an `[unstable] allow` list covers `path`: an entry covers itself and
593/// everything under it.
594pub fn allows(allow: &[String], path: &str) -> bool {
595    allow.iter().any(|entry| covers(entry, path))
596}
597
598/// Whether `entry`, an `[unstable] allow` entry, reaches anything in
599/// `experimental` (a list of experimental item paths, such as
600/// [`experimental_items`]): it names an experimental item, lies under one, or
601/// contains one. An entry that reaches nothing is stale.
602pub fn allow_entry_is_live(experimental: &[String], entry: &str) -> bool {
603    experimental
604        .iter()
605        .any(|item| covers(item, entry) || covers(entry, item))
606}
607
608/// The instance kind `capability` of `unit` is, if a context may declare named
609/// instances of it (S4a, #1966).
610pub fn instantiable(unit: &str, capability: &str) -> Option<&'static Instantiable> {
611    self::unit(unit)?
612        .instantiable
613        .iter()
614        .find(|i| i.capability == capability)
615}
616
617/// Whether `name`, an enum exported by `unit`, is extensible.
618pub fn is_extensible(unit: &str, name: &str) -> bool {
619    is_extensible_in(UNITS, unit, name)
620}
621
622/// [`is_extensible`] over a given table.
623pub fn is_extensible_in(units: &[Unit], unit: &str, name: &str) -> bool {
624    units
625        .iter()
626        .any(|u| u.name == unit && u.extensible.contains(&name))
627}
628
629/// The names a first-party unit exports: those on its `exports … { … }` lines
630/// (an adapter) and its own `fn`s and `type`s (a commons). Read from the
631/// source, so the list cannot fall behind it.
632pub fn exported_names(unit: &Unit) -> Vec<&'static str> {
633    let mut out = Vec::new();
634    for line in unit.source.lines() {
635        let trimmed = line.trim_start();
636        if let Some(rest) = trimmed.strip_prefix("exports ")
637            && let Some((_, list)) = rest.split_once('{')
638            && let Some((names, _)) = list.split_once('}')
639        {
640            out.extend(names.split(',').map(str::trim).filter(|n| !n.is_empty()));
641        } else if unit.kind == UnitKind::Commons
642            // A commons' own declarations: at column 0 in the flat form, one tab
643            // in the braced `commons bynk.list { … }` form the first-party
644            // sources use. Nothing deeper is a declaration.
645            && (line == trimmed || line.strip_prefix('\t') == Some(trimmed))
646        {
647            for keyword in ["fn ", "type "] {
648                if let Some(rest) = trimmed.strip_prefix(keyword) {
649                    let end = rest
650                        .find(|c: char| !(c.is_alphanumeric() || c == '_'))
651                        .unwrap_or(rest.len());
652                    if end > 0 {
653                        out.push(&rest[..end]);
654                    }
655                }
656            }
657        }
658    }
659    out
660}
661
662/// Whether `path` names a first-party unit or an item it exports. Operation
663/// paths (`bynk.cloudflare.Kv.list`) are accepted when their item exists.
664pub fn names_first_party_item(path: &str) -> bool {
665    names_first_party_item_in(UNITS, path)
666}
667
668/// [`names_first_party_item`] over a given table.
669pub fn names_first_party_item_in(units: &[Unit], path: &str) -> bool {
670    match split_item_path_in(units, path) {
671        None => false,
672        Some((_, "")) => true,
673        Some((unit, rest)) => {
674            let item = rest.split('.').next().unwrap_or(rest);
675            exported_names(unit).contains(&item)
676        }
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    /// A synthetic registry with an experimental unit, an experimental item
685    /// and an experimental operation inside stable units.
686    const SYNTH: &[Unit] = &[
687        Unit {
688            name: "bynk",
689            kind: UnitKind::Adapter,
690            identity_path: "bynk.bynk",
691            source: "adapter bynk {\n\texports capability { Clock, Fetch }\n\texports transparent { FetchError }\n}\n",
692            binding: Binding::PerPlatform,
693            id_block: 0,
694            platform: None,
695            env_providers: &[],
696            stability: Stability::Stable,
697            experimental: &["Fetch.stream"],
698            extensible: &["FetchError"],
699            instantiable: &[],
700        },
701        Unit {
702            name: "bynk.cloudflare",
703            kind: UnitKind::Adapter,
704            identity_path: "bynk/cloudflare.bynk",
705            source: "adapter bynk.cloudflare {\n\texports capability { Kv, Ai }\n}\n",
706            binding: Binding::None,
707            id_block: 1,
708            platform: Some(Platform::Cloudflare),
709            env_providers: &[],
710            stability: Stability::Stable,
711            experimental: &["Ai"],
712            extensible: &[],
713            instantiable: &[],
714        },
715        Unit {
716            name: "bynk.cloudflare.d1",
717            kind: UnitKind::Adapter,
718            identity_path: "bynk/cloudflare/d1.bynk",
719            source: "adapter bynk.cloudflare.d1 {\n\texports capability { D1 }\n}\n",
720            binding: Binding::None,
721            id_block: 2,
722            platform: Some(Platform::Cloudflare),
723            env_providers: &[],
724            stability: Stability::Experimental,
725            experimental: &[],
726            extensible: &[],
727            instantiable: &[],
728        },
729        Unit {
730            name: "bynk.text",
731            kind: UnitKind::Commons,
732            identity_path: "bynk/text.bynk",
733            source: "commons bynk.text\n\ntype Slug = String\n\nfn shout(s: String) -> String {\n\ts\n}\n",
734            binding: Binding::None,
735            id_block: 3,
736            platform: None,
737            env_providers: &[],
738            stability: Stability::Stable,
739            experimental: &[],
740            extensible: &[],
741            instantiable: &[],
742        },
743    ];
744
745    #[test]
746    fn item_paths_resolve_to_the_longest_unit() {
747        let unit_of = |p| split_item_path_in(SYNTH, p).map(|(u, rest)| (u.name, rest));
748        assert_eq!(unit_of("bynk"), Some(("bynk", "")));
749        assert_eq!(unit_of("bynk.Fetch.send"), Some(("bynk", "Fetch.send")));
750        assert_eq!(
751            unit_of("bynk.cloudflare.Kv"),
752            Some(("bynk.cloudflare", "Kv"))
753        );
754        assert_eq!(
755            unit_of("bynk.cloudflare.d1.D1"),
756            Some(("bynk.cloudflare.d1", "D1"))
757        );
758        assert_eq!(unit_of("bynkx"), None, "a whole-segment prefix only");
759        assert_eq!(unit_of("user.unit"), None);
760    }
761
762    #[test]
763    fn stability_follows_unit_item_and_operation() {
764        let st = |p| stability_of_in(SYNTH, p);
765        use Stability::*;
766        assert_eq!(st("bynk.Clock"), Some(Stable));
767        assert_eq!(st("bynk.Fetch.send"), Some(Stable));
768        assert_eq!(
769            st("bynk.Fetch.stream"),
770            Some(Experimental),
771            "an experimental operation"
772        );
773        assert_eq!(
774            st("bynk.Fetch"),
775            Some(Stable),
776            "its capability stays stable"
777        );
778        assert_eq!(
779            st("bynk.cloudflare.Ai"),
780            Some(Experimental),
781            "an experimental item"
782        );
783        assert_eq!(
784            st("bynk.cloudflare.Ai.run"),
785            Some(Experimental),
786            "its operations too"
787        );
788        assert_eq!(st("bynk.cloudflare.Kv"), Some(Stable));
789        assert_eq!(
790            st("bynk.cloudflare.d1"),
791            Some(Experimental),
792            "an experimental unit"
793        );
794        assert_eq!(st("bynk.cloudflare.d1.D1.query"), Some(Experimental));
795        assert_eq!(st("user.unit"), None);
796    }
797
798    #[test]
799    fn allow_entries_cover_by_whole_segment_prefix() {
800        let allow = vec![
801            "bynk.cloudflare.Ai".to_string(),
802            "bynk.cloudflare.d1".to_string(),
803        ];
804        assert!(allows(&allow, "bynk.cloudflare.Ai"));
805        assert!(allows(&allow, "bynk.cloudflare.Ai.run"));
806        assert!(allows(&allow, "bynk.cloudflare.d1.D1.query"));
807        assert!(
808            !allows(&allow, "bynk.cloudflare.AiX"),
809            "not a segment prefix"
810        );
811        assert!(!allows(&allow, "bynk.Fetch.stream"));
812        assert!(!allows(&[], "bynk.cloudflare.Ai"));
813    }
814
815    #[test]
816    fn an_allow_entry_is_live_while_it_reaches_something_experimental() {
817        let synth = experimental_items_in(SYNTH);
818        let live = |e| allow_entry_is_live(&synth, e);
819        assert!(live("bynk.cloudflare.Ai"));
820        assert!(live("bynk.cloudflare.Ai.run"), "under an experimental item");
821        assert!(live("bynk.cloudflare"), "contains an experimental item");
822        assert!(live("bynk.Fetch.stream"));
823        assert!(live("bynk"), "contains an experimental operation");
824        assert!(!live("bynk.cloudflare.Kv"), "stable: a stale entry");
825        assert!(!live("bynk.text"));
826        assert_eq!(
827            experimental_items_in(SYNTH),
828            [
829                "bynk.Fetch.stream",
830                "bynk.cloudflare.Ai",
831                "bynk.cloudflare.d1"
832            ]
833        );
834    }
835
836    #[test]
837    fn exported_names_and_item_existence_come_from_the_sources() {
838        assert_eq!(exported_names(&SYNTH[0]), ["Clock", "Fetch", "FetchError"]);
839        assert_eq!(exported_names(&SYNTH[3]), ["Slug", "shout"]);
840        let names = |p| names_first_party_item_in(SYNTH, p);
841        assert!(names("bynk.cloudflare"));
842        assert!(names("bynk.cloudflare.Ai"));
843        assert!(
844            names("bynk.cloudflare.Ai.run"),
845            "an operation of an existing item"
846        );
847        assert!(names("bynk.text.shout"));
848        assert!(!names("bynk.cloudflare.Aii"), "a typo");
849        assert!(!names("user.unit"));
850    }
851
852    #[test]
853    fn extensibility_is_by_unit_and_name() {
854        assert!(is_extensible_in(SYNTH, "bynk", "FetchError"));
855        assert!(
856            !is_extensible_in(SYNTH, "user.app", "FetchError"),
857            "a user's own FetchError"
858        );
859        assert!(!is_extensible_in(SYNTH, "bynk", "Method"));
860    }
861
862    /// The shipped instantiable kinds: `bynk.cloudflare`'s `Kv` (S4a, #1966),
863    /// binding as `KV_<NAME>` with no creation options, and its `Service`
864    /// (interop I5, #1972), binding as `SERVICE_<NAME>` and naming its Worker
865    /// with a required `@worker`, never usable bare. Each one is a capability
866    /// its unit exports.
867    #[test]
868    fn the_shipped_instantiable_kinds_are_pinned() {
869        let kinds: Vec<(&str, &str, &str)> = UNITS
870            .iter()
871            .flat_map(|u| {
872                u.instantiable
873                    .iter()
874                    .map(move |i| (u.name, i.capability, i.binding_prefix))
875            })
876            .collect();
877        assert_eq!(
878            kinds,
879            [
880                ("bynk.cloudflare", "Kv", "KV"),
881                ("bynk.cloudflare", "Service", "SERVICE")
882            ]
883        );
884        let kv = instantiable("bynk.cloudflare", "Kv").expect("Kv is instantiable");
885        assert!(kv.options.is_empty() && kv.required.is_empty() && kv.bare);
886        let service = instantiable("bynk.cloudflare", "Service").expect("Service is instantiable");
887        assert_eq!(service.options, ["worker"]);
888        assert_eq!(service.required, ["worker"]);
889        assert!(!service.bare);
890        for u in UNITS {
891            for i in u.instantiable {
892                assert!(
893                    i.required.iter().all(|r| i.options.contains(r)),
894                    "{}.{}: a required option must be an option",
895                    u.name,
896                    i.capability
897                );
898            }
899        }
900        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Kv"));
901        assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Service"));
902        assert_eq!(instantiable("bynk", "Clock"), None);
903        assert_eq!(instantiable("bynk.cloudflare", "Nope"), None);
904    }
905
906    /// The shipped registry: every unit is stable (S3 ships the mechanism,
907    /// #1963 Decision C), the one experimental item is `bynk.cloudflare`'s
908    /// `Service` (interop I5, #1972), and the `bynk` surface's two growing
909    /// enums are extensible. A slice that marks an item experimental updates
910    /// this test, so the change is visible in review.
911    #[test]
912    fn the_shipped_stability_is_pinned() {
913        for u in UNITS {
914            assert_eq!(u.stability, Stability::Stable, "{}", u.name);
915        }
916        assert_eq!(experimental_items(), ["bynk.cloudflare.Service"]);
917        let extensible: Vec<(&str, &str)> = UNITS
918            .iter()
919            .flat_map(|u| u.extensible.iter().map(move |e| (u.name, *e)))
920            .collect();
921        assert_eq!(extensible, [("bynk", "FetchError"), ("bynk", "Method")]);
922        for (unit, name) in extensible {
923            assert!(
924                exported_names(self::unit(unit).unwrap()).contains(&name),
925                "{unit} lists {name} as extensible but does not export it"
926            );
927        }
928        assert!(names_first_party_item("bynk.cloudflare.Kv.list"));
929        assert!(names_first_party_item("bynk.list.reverse"));
930        assert!(!names_first_party_item("bynk.cloudflare.Kvv"));
931    }
932
933    /// #1958: `WorkersKv` reads the namespace at the name every generated
934    /// Worker declares it under (`[[kv_namespaces]] binding`, its `Env`), the
935    /// one TypeScript literal no Rust constructor can supply. Since S4a (#1966)
936    /// the name is a constructor parameter, so a derived capability's instance
937    /// can pass its own; the bare `Kv`'s is the parameter's default.
938    ///
939    /// Matched as a whole identifier, then by its neighbours, so the test
940    /// ignores formatting but not a rename: `KVX` is not `KV` (review of
941    /// #1962).
942    #[test]
943    fn the_cloudflare_binding_reads_kv_binding_name() {
944        let ident = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
945        let src = CLOUDFLARE_BINDING;
946        let uses: Vec<(&str, &str)> = src
947            .match_indices(KV_BINDING_NAME)
948            .map(|(at, name)| (&src[..at], &src[at + name.len()..]))
949            .filter(|(before, after)| !before.ends_with(ident) && !after.starts_with(ident))
950            .collect();
951        assert!(
952            uses.iter().any(|(b, a)| {
953                b.trim_end().ends_with("binding: string = \"") && a.starts_with('"')
954            }),
955            "the constructor's `binding` no longer defaults to \"{KV_BINDING_NAME}\""
956        );
957        assert!(
958            src.contains("?.[this.binding]"),
959            "the namespace is no longer read at `env[binding]`"
960        );
961    }
962
963    /// Each unit's `ExprId` block is permanent: emitted output and the
964    /// process-wide parse cache depend on it. These are the blocks the
965    /// hand-written injection used before the registry (S1, #1945); a new
966    /// unit takes a new block and never moves one of these.
967    #[test]
968    fn id_blocks_are_pinned_and_distinct() {
969        let blocks: Vec<(&str, u32)> = UNITS.iter().map(|u| (u.name, u.id_block)).collect();
970        assert_eq!(
971            blocks,
972            [
973                ("bynk", 0),
974                ("bynk.cloudflare", 1),
975                ("bynk.list", 3),
976                ("bynk.map", 2),
977                ("bynk.string", 4),
978                ("bynk.locale", 6),
979                ("bynk.locale.types", 5),
980            ]
981        );
982        let mut sorted: Vec<u32> = UNITS.iter().map(|u| u.id_block).collect();
983        sorted.sort_unstable();
984        sorted.dedup();
985        assert_eq!(sorted.len(), UNITS.len(), "two units share an id block");
986    }
987
988    /// Hover and completion read [`FIRSTPARTY_SOURCES`] in order, so it is
989    /// pinned to the order the hand-written list had.
990    #[test]
991    fn firstparty_sources_keep_their_order() {
992        let names: Vec<&str> = FIRSTPARTY_SOURCES.iter().map(|(n, _)| *n).collect();
993        assert_eq!(
994            names,
995            [
996                "bynk",
997                "bynk.cloudflare",
998                "bynk.list",
999                "bynk.map",
1000                "bynk.string",
1001                "bynk.locale",
1002                "bynk.locale.types",
1003            ]
1004        );
1005        for (unit, (name, source)) in UNITS.iter().zip(FIRSTPARTY_SOURCES) {
1006            assert_eq!((unit.name, unit.source), (*name, *source));
1007        }
1008    }
1009
1010    /// The views over the table answer as the hand-written matches did.
1011    #[test]
1012    fn metadata_views_match_the_previous_answers() {
1013        assert_eq!(platform_of("bynk.cloudflare"), Some(Platform::Cloudflare));
1014        for name in [
1015            "bynk",
1016            "bynk.list",
1017            "bynk.map",
1018            "bynk.string",
1019            "bynk.locale",
1020            "user.unit",
1021        ] {
1022            assert_eq!(platform_of(name), None, "{name}");
1023        }
1024        assert!(provider_takes_env("bynk", "SecretsProvider"));
1025        assert!(provider_takes_env("bynk.cloudflare", "WorkersKv"));
1026        assert!(!provider_takes_env("bynk", "WorkersKv"));
1027        assert!(!provider_takes_env("bynk.cloudflare", "SecretsProvider"));
1028        assert!(!provider_takes_env("bynk", "ClockProvider"));
1029        assert!(!provider_takes_env("user.unit", "SecretsProvider"));
1030    }
1031
1032    /// An adapter carries a binding and a commons does not, and every name and
1033    /// identity path is unique, every name inside the reserved prefix.
1034    #[test]
1035    fn entries_are_well_formed() {
1036        for u in UNITS {
1037            assert!(
1038                u.name == "bynk" || u.name.starts_with("bynk."),
1039                "{}",
1040                u.name
1041            );
1042            assert_eq!(
1043                UNITS.iter().filter(|o| o.name == u.name).count(),
1044                1,
1045                "{}",
1046                u.name
1047            );
1048            // Two entries under one identity path would be two parsed units
1049            // that diagnostics and emission cannot tell apart.
1050            assert_eq!(
1051                UNITS
1052                    .iter()
1053                    .filter(|o| o.identity_path == u.identity_path)
1054                    .count(),
1055                1,
1056                "{}",
1057                u.identity_path
1058            );
1059            match u.kind {
1060                UnitKind::Adapter => assert_ne!(u.binding, Binding::None, "{}", u.name),
1061                UnitKind::Commons => {
1062                    assert_eq!(u.binding, Binding::None, "{}", u.name);
1063                    assert!(u.env_providers.is_empty(), "{}", u.name);
1064                }
1065            }
1066        }
1067        assert_eq!(
1068            unit("bynk.cloudflare").and_then(|u| u.binding_for(Platform::Node)),
1069            Some((CLOUDFLARE_BINDING_FILENAME, CLOUDFLARE_BINDING))
1070        );
1071        assert_eq!(
1072            unit("bynk").and_then(|u| u.binding_for(Platform::Browser)),
1073            Some(("bynk-browser.ts", BYNK_BROWSER_BINDING))
1074        );
1075        assert_eq!(
1076            unit("bynk.list").and_then(|u| u.binding_for(Platform::Node)),
1077            None
1078        );
1079    }
1080
1081    /// The hardcoded [`BYNK_SURFACE_CAPABILITIES`] must match the adapter's
1082    /// `exports capability { … }` line — otherwise the unknown-name quick-fix
1083    /// (#852) would offer a stale `consumes bynk { … }` set.
1084    #[test]
1085    fn bynk_surface_capabilities_match_the_adapter() {
1086        let src = BYNK_ADAPTER_SRC;
1087        let line = src
1088            .lines()
1089            .find(|l| l.trim_start().starts_with("exports capability {"))
1090            .expect("adapter declares `exports capability`");
1091        let inner = line
1092            .split_once('{')
1093            .and_then(|(_, r)| r.split_once('}'))
1094            .map(|(names, _)| names)
1095            .expect("braced capability list");
1096        let declared: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1097        assert_eq!(
1098            declared, BYNK_SURFACE_CAPABILITIES,
1099            "BYNK_SURFACE_CAPABILITIES is out of sync with bynk.bynk"
1100        );
1101    }
1102}