bynk_check/wire.rs
1//! #855: the wire-contract IR — the shape a Bynk type takes crossing a
2//! context boundary, derived once from the AST + type table so both the
3//! emitter's codec generation and the editor's "wire contract" peek render
4//! the *same* derivation instead of two hand-synchronised ones.
5//!
6//! **The seam:** *`bynk-check` says what the boundary is. `bynk-emit` says
7//! how that reads as TypeScript.*
8//!
9//! Crossing this module boundary is legal for `BaseType`, `PredKind`,
10//! `TypeRef`, `Expr`, `TypeDecl` (all `bynk-syntax`) — the vocabulary a
11//! boundary type is built from. It is **not** legal for any TS-token string,
12//! `pred_condition_and_message` (`bynk-emit/src/emitter.rs`, stays put),
13//! `lower_field_default_wire` (`bynk-emit/src/emitter/serialisation.rs`,
14//! stays put — a default's *wire literal* is a rendering, not a boundary
15//! fact), or the `Qual` type-name→TS-namespace-prefix map (`bynk-emit`'s
16//! `serialisation.rs`; see the [`Provenance`] doc for why it cannot move
17//! here). `bynk-emit` renders this IR into TypeScript; it does not re-derive
18//! it.
19//!
20//! **Derived from AST + type table, not `checker::Ty`.** `Ty::Named { name,
21//! kind, args }` carries no refinement predicates, so a `Ty`-based
22//! derivation would still need this same type-table lookup for `PredKind`s —
23//! `Ty` would buy only generic substitution, which the moved walks below
24//! already implement directly over `TypeRef`. `contract.rs` (this crate)
25//! also derives straight from the AST + type table and must not depend on
26//! checker output being available; this module keeps the same shape of
27//! dependency.
28//!
29//! **This IR is *not* `contract.rs`'s canonical form**, and the two must
30//! never be unified — they disagree on purpose, on every axis that matters:
31//!
32//! | | `contract.rs` (hash) | `wire.rs` (this module) |
33//! |---|---|---|
34//! | predicates | sorted, deduped | **declaration order**, not deduped |
35//! | record fields | sorted by name | declaration order (emitted key order) |
36//! | sum variants | sorted by name | declaration order (`switch` arm order) |
37//! | opaque predicate | **elided** — unobservable to the consumer | **present** — the owner still re-validates |
38//!
39//! `contract.rs:248` documents the sorting as a *precondition* for hash
40//! correctness — hashing predicates in source order "would make two contexts
41//! that agree perfectly fail closed against each other." The hash is a
42//! **type identity** (order-insensitive by design); this module's shapes are
43//! an **emission order** (order-sensitive by necessity, since a JS `switch`
44//! and an inlined `if` chain both have a literal source order a reader can
45//! see). Merging them would create the exact spurious-409 failure the hash
46//! exists to prevent. What the two genuinely share — and what a cross-check
47//! test elsewhere asserts — is *boundary-type reachability*, not shape.
48
49use std::collections::{BTreeSet, HashMap};
50use std::sync::Arc;
51
52use bynk_syntax::ast::*;
53
54/// I4 (#1970): the declaration shapes this module's public API is
55/// parameterised by ([`collect_codec_closure`] walks `TypeRef` roots over a
56/// `TypeDecl` table, whose `body` is a `TypeBody`), re-exported so a renderer
57/// over that API (`bynk-emit`'s TypeScript client) names them from here — the
58/// P6.27 / P6.49 precedent `project_model`'s `TypeDecl` re-export follows.
59pub use bynk_syntax::ast::{TypeBody, TypeRef};
60
61// ---------------------------------------------------------------------
62// Core vocabulary
63// ---------------------------------------------------------------------
64
65/// The JSON value shape a [`BaseType`] occupies on the wire. Replaces
66/// `bynk-emit`'s `ts_base_for_serialisation` *classification* — the TS-token
67/// spelling of each kind (`"number"`, `"string"`, …) stays in `bynk-emit`,
68/// since a TS token is exactly what this seam excludes.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum JsonKind {
71 Number,
72 String,
73 Boolean,
74 Object,
75 Array,
76 Null,
77}
78
79/// The JSON kind a base type wires as. Same mapping as the `ts_base_for_serialisation`
80/// it replaces: `Int`/`Float`/`Duration`/`Instant` → `Number` (all four erase to a TS
81/// `number`), `String`/`Bytes` → `String` (a `Bytes` wires as a base64 string, ADR
82/// 0142 D5), `Bool` → `Boolean`.
83pub fn json_kind_of(b: BaseType) -> JsonKind {
84 match b {
85 BaseType::Int | BaseType::Float | BaseType::Duration | BaseType::Instant => {
86 JsonKind::Number
87 }
88 BaseType::String | BaseType::Bytes => JsonKind::String,
89 BaseType::Bool => JsonKind::Boolean,
90 }
91}
92
93/// An extra structural guard a base-typed wire value must pass beyond its
94/// `typeof`: an `Int`/`Instant` must be whole, a `Float` must be finite.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum BaseGuard {
97 Integral,
98 Finite,
99}
100
101/// What a structural-mismatch error reports as `expected` — the vocabulary a
102/// renderer needs to explain *why* a wire value was rejected, independent of
103/// the TS spelling of the check that rejected it.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Expected {
106 Json(JsonKind),
107 Integer,
108 FiniteNumber,
109 Base64String,
110 SumVariantKind,
111}
112
113/// Where a boundary type's declaration lives relative to the module doing
114/// the crossing.
115///
116/// Carries the owner's *qualified unit name*, never a TS namespace prefix —
117/// the IR picks the re-validation *strategy* ([`Revalidation`]); `bynk-emit`
118/// alone knows how to spell that owner as an `import type * as <ns>` alias
119/// (`bynk-emit/src/emitter/serialisation.rs`'s `Qual`/`qual_prefix`, built
120/// per-emission from build-mode facts this crate does not have). Unifying
121/// the two would drag `BuildTarget` into the checker.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum Provenance {
124 /// The emitting module's own declaration: re-validation routes through
125 /// the type's own constructor.
126 Owned,
127 /// A type declared by another unit and reached through it — `owner_unit`
128 /// is that unit's qualified name (`commerce.payment`, never a TS
129 /// namespace spelling).
130 Consumed { owner_unit: String },
131}
132
133/// #661 Decisions C/D plus the owner path, stated once: how a boundary
134/// scalar's refinement is re-checked on the way in, given who declared it
135/// and where the check is happening.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum Revalidation {
138 /// Owner's own module: run the type's own `.of`.
139 ViaConstructor,
140 /// Consumed + transparent (Decision D): re-check each predicate inline,
141 /// since the consumer knows the shape by declaration even without an
142 /// importable constructor.
143 Inline,
144 /// Consumed + opaque (Decision C): the predicate is the owner's secret —
145 /// cast structurally after the base check; skew is caught by the
146 /// contract hash instead.
147 StructuralOnly,
148 /// `Bytes`: decoded, not cast — the one base type whose wire value is
149 /// not a direct cast of its erased representation. Applies regardless of
150 /// provenance, mirroring `emit_refined`'s early return to the dedicated
151 /// `Bytes` codec before the owned/consumed split is even considered.
152 Base64Decode,
153}
154
155/// A named boundary type's declared shape, refinement-independent of who is
156/// asking. `owner`-relative facts ([`Revalidation`]) live one level down, in
157/// [`WireScalar`] — the shape itself does not vary with provenance, only how
158/// hard the receiver re-checks it.
159#[derive(Debug, Clone)]
160pub struct WireType {
161 pub name: String,
162 /// The codec-name suffix this type resolves to (`Order`, or a
163 /// monomorphised generic's `Paginated_User`). Equal to `name` for every
164 /// non-generic declaration `wire_type` produces; kept as its own field
165 /// because a future generic-instantiation `WireType` (Phase 2+) needs
166 /// the two to diverge the same way `serialisation.rs`'s `fn_suffix` /
167 /// `ts_type` pair already does.
168 pub codec_suffix: String,
169 pub provenance: Provenance,
170 pub body: WireBody,
171}
172
173#[derive(Debug, Clone)]
174pub enum WireBody {
175 Scalar(WireScalar),
176 Record { fields: Vec<WireField> },
177 Sum(WireSum),
178}
179
180/// A refined- or opaque-base-type boundary scalar.
181#[derive(Debug, Clone)]
182pub struct WireScalar {
183 pub base: BaseType,
184 pub json: JsonKind,
185 /// `true` for `opaque BaseType`, `false` for a transparent refined type.
186 pub opaque: bool,
187 /// **Declaration order**, not sorted, not deduped — this is the
188 /// highest-risk drift trap between this IR and `contract.rs`'s canonical
189 /// form (see the module doc's comparison table). `Inline` revalidation
190 /// emits one check per predicate in exactly this order.
191 pub predicates: Vec<PredKind>,
192 /// The base-type guards this scalar's `Inline` revalidation applies,
193 /// *before* the declared predicates. Mirrors
194 /// `emit_inline_refinement_checks` exactly: `Int` guards `Integral`,
195 /// `Float` guards `Finite` — deliberately **not** `Instant`, which is
196 /// guarded at record-*field* position (`emit_field_deserialise`'s
197 /// `TypeRef::Base` arm) but not here. That asymmetry already exists in
198 /// the emitter this IR was extracted from; it is preserved verbatim
199 /// rather than "fixed" by this move, since fixing it would be a
200 /// behaviour change this phase must not make. (See `field_base_guards`
201 /// for the field-position set, which does include `Instant`.)
202 pub base_guards: Vec<BaseGuard>,
203 pub revalidation: Revalidation,
204}
205
206/// One field of a boundary record, in **declaration order** (the emitted
207/// JSON key order).
208#[derive(Debug, Clone)]
209pub struct WireField {
210 pub name: String,
211 pub shape: WireRef,
212 /// The JSON path segment this field contributes to a validation error's
213 /// `path` (`` `${path}.<segment>` ``) — equal to `name` today; kept
214 /// distinct because a future field-rename annotation would move the wire
215 /// key without moving the path segment a hover/error should still name.
216 pub path_segment: String,
217 /// The field's default initialiser, if any, carried as raw AST — its
218 /// *wire-JSON literal* rendering is `lower_field_default_wire`
219 /// (`bynk-emit`), which stays in `bynk-emit` per the module doc: a
220 /// default's lowered literal is a rendering, not a boundary fact.
221 pub default: Option<(Expr, TypeRef)>,
222 /// #1969: the field's inline refinement (`qty: Int where Positive`), in
223 /// declaration order, checked on decode after the field's own codec. Empty
224 /// for an unrefined field and for every sum-variant payload field (the
225 /// grammar gives those no `where`).
226 pub predicates: Vec<PredKind>,
227}
228
229/// #1969: the JSON key a sum value's variant name travels under (`{"kind":
230/// "Circle", …}`). Part of the published wire format, version 1
231/// ([`crate::wire_format`]); the in-memory discriminant is `tag`.
232pub const WIRE_DISCRIMINANT: &str = "kind";
233
234/// A boundary sum type. The wire and in-memory discriminants are carried
235/// side by side because the codec's whole job is translating between them —
236/// `memory_discriminant` is the softest part of this seam (a host
237/// representation choice, not a wire fact); moving it back into `bynk-emit`
238/// alone is a one-field change if a reviewer objects.
239#[derive(Debug, Clone)]
240pub struct WireSum {
241 /// Always [`WIRE_DISCRIMINANT`].
242 pub wire_discriminant: &'static str,
243 pub memory_discriminant: &'static str,
244 pub variants: Vec<WireVariant>,
245}
246
247/// One variant of a [`WireSum`], in **declaration order** (the emitted
248/// `switch` arm order).
249#[derive(Debug, Clone)]
250pub struct WireVariant {
251 pub name: String,
252 pub payload: Vec<WireField>,
253}
254
255/// Why a [`WireRef`] carries no generated codec — the runtime-owned error
256/// family, which has no `TypeDecl` to derive a shape from.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum UncheckedReason {
259 /// An `Effect[T]` reached in a field/element position (never a top-level
260 /// handler return, which strips it before it gets here).
261 Effect,
262 ValidationError,
263 JsonError,
264 HttpResult,
265 QueueResult,
266 /// #1883, slice 1a: an `Asset` (rejected at boundaries by the checker).
267 Asset,
268}
269
270/// The resolved shape of one `TypeRef` occurrence — a field's type, a sum
271/// variant payload's type, a generic instantiation's argument. Mirrors the
272/// exhaustive dispatch `bynk-emit`'s `emit_field_deserialise` performs today,
273/// one level removed from any TS string.
274#[derive(Debug, Clone)]
275pub enum WireRef {
276 Base {
277 base: BaseType,
278 json: JsonKind,
279 guards: Vec<BaseGuard>,
280 expected: Expected,
281 },
282 /// `Bytes`: base64 string on the wire, decoded (never cast) into a
283 /// `Uint8Array`-shaped value.
284 Bytes,
285 /// A named boundary type — resolve further via the model's own
286 /// `WireType` table.
287 Named { name: String },
288 /// A generic instantiation — `Result[A, B]`, `Option[A]`, `List[A]`,
289 /// `Map[K, V]`, or a generic record/sum application (`Paginated[User]`).
290 /// `key` is the same codec-suffix string in every case
291 /// (`Result_Int_String`, `Paginated_User`, …) — resolve further via the
292 /// model's `instantiations` list, keyed by [`WireInst::ts_name`].
293 Inst { key: String },
294 /// `()` — no wire content; the wire slot is `null`, the value `undefined`.
295 Unit,
296 /// The runtime-owned error family (plus a stray field-position `Effect`)
297 /// — cast through unchecked, since there is no `TypeDecl` to derive a
298 /// shape from. See [`UncheckedReason`].
299 Unchecked { reason: UncheckedReason },
300}
301
302/// The set of `TypeRef`s a boundary walk resolved and their re-derived
303/// structural shapes — the single source of truth both the emitter's codec
304/// generation and the wire-contract peek render from.
305#[derive(Debug, Clone)]
306pub struct WireModel {
307 pub types: Vec<WireType>,
308 pub instantiations: Vec<WireInst>,
309 /// The generic-record names (v0.174 #592) that are transitively
310 /// self-referential and therefore have no finite monomorphised codec set
311 /// — rejected at the boundary before emit, carried here as the same
312 /// membership set the codec walks use to short-circuit.
313 pub recursive: BTreeSet<String>,
314}
315
316// ---------------------------------------------------------------------
317// Moved verbatim from `bynk-emit/src/emitter/serialisation.rs` (pure,
318// AST-only walks — no TS string ever touches these). `pub(crate)` in the
319// original became `pub` here; everything else is unchanged apart from the
320// renames the plan calls for (`inner_ts_name` → `codec_suffix`, `app_ts_name`
321// → `inst_codec_suffix`).
322//
323// `GenericInst` moved as `WireInst` but **keeps its original variant names**
324// (`ResultInst`/`OptionInst`/…, not the plan's bare `Result`/`Option`/…):
325// `bynk-emit/src/emitter/serialisation.rs`'s `emit_generic_helpers_qualified`
326// — a codec-*emission* function, out of scope for this phase — pattern-matches
327// those variants directly, and this phase must not touch codec-emission call
328// sites.
329//
330// #855 (Phase 2 step 9, decided): the plan allowed renaming to the bare
331// spelling here since this step touches `emit_generic_helpers_qualified`
332// anyway — but the RecordInst/SumInst arms it touches already consume the
333// IR's `WireField`/`WireSum` shapes (steps 7/8's byte-identical rewrite of
334// `emit_record_codec`/`emit_sum_codec` covers them), and the other four arms
335// (`ResultInst`/`OptionInst`/`ListInst`/`MapInst`) build their TS inline —
336// they were never going to move to a `Result`/`Option`/`List`/`Map` spelling
337// of *this* enum either way. A pure rename would touch six match arms here
338// and this module's doc/tests for zero behavioural or architectural gain, so
339// the `*Inst`-suffixed names are kept. Revisit only if a future consumer
340// (e.g. the Phase 3+ peek) finds the bare spelling actually reads better at
341// its own call sites.
342// ---------------------------------------------------------------------
343
344/// Compute the set of type names (transitively reachable) that need
345/// serialise/deserialise helpers for this context: any type used in the
346/// argument or return position of a service handler exposed by this
347/// context, walked through record fields, sum payloads, and the generic
348/// type parameters of Result/Option/Effect.
349pub fn collect_boundary_types(
350 types: &HashMap<String, Arc<TypeDecl>>,
351 services: &HashMap<String, ServiceDecl>,
352 // v0.96 (ADR 0124): rehydration is a trust boundary — an agent's persisted
353 // `store`-field types are validated on load, so they need their deserialisers
354 // emitted. Register every store field's kind-argument types (the element /
355 // key / value types of `Cell`/`Map`/`Set`/`Cache`/`Log`).
356 agents: &HashMap<String, AgentDecl>,
357 // #1678: further boundary positions to root the walk at. On the `workers`
358 // target an agent call crosses a Durable Object `fetch`, so the emitter
359 // passes every agent handler's parameter and return types here.
360 extra_roots: &[TypeRef],
361) -> Vec<String> {
362 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
363 let mut out: Vec<String> = Vec::new();
364 let mut stack: Vec<String> = Vec::new();
365 let recursive_set = recursive_generic_names(types);
366 let recursive = &recursive_set;
367 for r in extra_roots {
368 collect_type_names(r, &mut stack, types, recursive);
369 }
370
371 let mut svc_names: Vec<&String> = services.keys().collect();
372 svc_names.sort();
373 for name in svc_names {
374 let service = &services[name];
375 for h in &service.handlers {
376 for p in &h.params {
377 collect_type_names(&p.type_ref, &mut stack, types, recursive);
378 }
379 collect_type_names(&h.return_type, &mut stack, types, recursive);
380 }
381 }
382
383 let mut agent_names: Vec<&String> = agents.keys().collect();
384 agent_names.sort();
385 for name in agent_names {
386 for f in &agents[name].store_fields {
387 for arg in &f.kind.args {
388 collect_type_names(arg, &mut stack, types, recursive);
389 }
390 }
391 // State-migrations S2 (#2224): a `migrate` step decodes stored bytes
392 // through its old record type, so that type needs a deserialiser too.
393 for m in &agents[name].migrations {
394 if let Some(old) = &m.old {
395 collect_type_names(&old.type_ref, &mut stack, types, recursive);
396 }
397 }
398 }
399
400 while let Some(name) = stack.pop() {
401 if !seen.insert(name.clone()) {
402 continue;
403 }
404 out.push(name.clone());
405 let Some(decl) = types.get(&name) else {
406 continue;
407 };
408 match &decl.body {
409 TypeBody::Record(r) => {
410 for f in &r.fields {
411 collect_type_names(&f.type_ref, &mut stack, types, recursive);
412 }
413 }
414 TypeBody::Sum(s) => {
415 for v in &s.variants {
416 for p in &v.payload {
417 collect_type_names(&p.type_ref, &mut stack, types, recursive);
418 }
419 }
420 }
421 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
422 }
423 }
424
425 out.sort();
426 out
427}
428
429/// v0.174 (#592): the set of generic-record names that are *recursive* — they
430/// transitively contain themselves, so they have no finite monomorphised codec
431/// (rejected at the boundary by the checker before emit). Precomputed once per
432/// collector so the per-`App` guard in the codec walks is an O(1) membership test
433/// rather than a fresh graph reachability walk at every occurrence.
434fn recursive_generic_names(
435 types: &HashMap<String, Arc<TypeDecl>>,
436) -> std::collections::HashSet<String> {
437 types
438 .iter()
439 .filter(|(_, d)| !d.type_params.is_empty())
440 .map(|(n, _)| n.clone())
441 .filter(|n| generic_record_is_recursive(n, types))
442 .collect()
443}
444
445fn collect_type_names(
446 t: &TypeRef,
447 stack: &mut Vec<String>,
448 types: &HashMap<String, Arc<TypeDecl>>,
449 recursive: &std::collections::HashSet<String>,
450) {
451 match t {
452 TypeRef::Named(id) => stack.push(id.name.clone()),
453 // Query/Stream/Connection types carry no boundary-collectable user
454 // types (non-boundary).
455 TypeRef::Query(..)
456 | TypeRef::Stream(..)
457 | TypeRef::Connection(..)
458 | TypeRef::History(..) => {}
459 // v0.174 (#592): a generic-record instantiation is boundary-serialisable
460 // through its monomorphised codec (`serialise_Paginated_User`). The
461 // *named* helpers that codec calls come from its concrete field types —
462 // the type arguments (`User`) and any non-parameter named field types
463 // (`Envelope[T] = { meta: Metadata, … }`) — so walk the substituted
464 // fields. A *recursive* generic record has no finite codec set and is
465 // rejected at the boundary before emit; the guard here is defence in
466 // depth so this walk can never fail to terminate.
467 TypeRef::App { name, args, .. } => {
468 if recursive.contains(&name.name) {
469 return;
470 }
471 // #593: a generic-sum instantiation's codec (`serialise_ApiResult_User`)
472 // likewise calls the named helpers of its *substituted variant
473 // payloads* (`serialise_User` for `Loaded(value: T)` at `T = User`),
474 // so walk those the same way records walk their fields.
475 if let Some(fields) = record_inst_fields(&name.name, args, types) {
476 for (_, ft) in &fields {
477 collect_type_names(ft, stack, types, recursive);
478 }
479 } else if let Some(variants) = sum_inst_variants(&name.name, args, types) {
480 for (_, payload) in &variants {
481 for (_, ft) in payload {
482 collect_type_names(ft, stack, types, recursive);
483 }
484 }
485 }
486 }
487 // v0.20a: function types carry no user-named types to collect and are
488 // rejected at boundaries anyway.
489 TypeRef::Fn(..) => {}
490 TypeRef::Result(a, b, _) => {
491 collect_type_names(a, stack, types, recursive);
492 collect_type_names(b, stack, types, recursive);
493 }
494 TypeRef::Option(a, _) => collect_type_names(a, stack, types, recursive),
495 TypeRef::Effect(a, _) => collect_type_names(a, stack, types, recursive),
496 TypeRef::HttpResult(a, _) => collect_type_names(a, stack, types, recursive),
497 // v0.20b: collections serialise element-/entry-wise; their inner
498 // named types need helpers.
499 TypeRef::List(a, _) => collect_type_names(a, stack, types, recursive),
500 TypeRef::Map(k, v, _) => {
501 collect_type_names(k, stack, types, recursive);
502 collect_type_names(v, stack, types, recursive);
503 }
504 TypeRef::Base(_, _)
505 | TypeRef::QueueResult(_)
506 | TypeRef::Asset(_)
507 | TypeRef::RequestHeaders(_)
508 | TypeRef::TokenClaims(_)
509 | TypeRef::BoundaryRejection(_)
510 | TypeRef::ValidationError(_)
511 | TypeRef::JsonError(_)
512 | TypeRef::Unit(_) => {}
513 }
514}
515
516/// v0.174 (#592): substitute a generic record's declared field type — replacing
517/// each type-parameter name with the concrete argument type-ref — so a
518/// per-instantiation codec sees fully concrete field types.
519/// `Paginated[User]`'s `items: List[T]` becomes `items: List[User]`.
520fn subst_type_ref(t: &TypeRef, subst: &HashMap<String, TypeRef>) -> TypeRef {
521 match t {
522 TypeRef::Named(id) => match subst.get(&id.name) {
523 Some(replacement) => replacement.clone(),
524 None => t.clone(),
525 },
526 TypeRef::App { name, args, span } => TypeRef::App {
527 name: name.clone(),
528 args: args.iter().map(|a| subst_type_ref(a, subst)).collect(),
529 span: *span,
530 },
531 TypeRef::Result(a, b, s) => TypeRef::Result(
532 Box::new(subst_type_ref(a, subst)),
533 Box::new(subst_type_ref(b, subst)),
534 *s,
535 ),
536 TypeRef::Option(a, s) => TypeRef::Option(Box::new(subst_type_ref(a, subst)), *s),
537 TypeRef::Effect(a, s) => TypeRef::Effect(Box::new(subst_type_ref(a, subst)), *s),
538 TypeRef::HttpResult(a, s) => TypeRef::HttpResult(Box::new(subst_type_ref(a, subst)), *s),
539 TypeRef::List(a, s) => TypeRef::List(Box::new(subst_type_ref(a, subst)), *s),
540 TypeRef::Map(k, v, s) => TypeRef::Map(
541 Box::new(subst_type_ref(k, subst)),
542 Box::new(subst_type_ref(v, subst)),
543 *s,
544 ),
545 TypeRef::Query(a, s) => TypeRef::Query(Box::new(subst_type_ref(a, subst)), *s),
546 TypeRef::Stream(a, s) => TypeRef::Stream(Box::new(subst_type_ref(a, subst)), *s),
547 TypeRef::Connection(a, s) => TypeRef::Connection(Box::new(subst_type_ref(a, subst)), *s),
548 TypeRef::History(a, s) => TypeRef::History(Box::new(subst_type_ref(a, subst)), *s),
549 TypeRef::Fn(ps, r, s) => TypeRef::Fn(
550 ps.iter().map(|p| subst_type_ref(p, subst)).collect(),
551 Box::new(subst_type_ref(r, subst)),
552 *s,
553 ),
554 TypeRef::Base(..)
555 | TypeRef::QueueResult(_)
556 | TypeRef::Asset(_)
557 | TypeRef::RequestHeaders(_)
558 | TypeRef::TokenClaims(_)
559 | TypeRef::BoundaryRejection(_)
560 | TypeRef::ValidationError(_)
561 | TypeRef::JsonError(_)
562 | TypeRef::Unit(_) => t.clone(),
563 }
564}
565
566/// #1846: `t` with each type `renames` names renamed, wherever it is named:
567/// a bare name, and the base of a generic application (`Envelope[Int]`),
568/// which `subst_type_ref` leaves alone because a type parameter is never
569/// applied. `bynk-emit` renames a consumed context's unexported types with it.
570pub fn rename_type_ref(t: &TypeRef, renames: &HashMap<String, String>) -> TypeRef {
571 match t {
572 TypeRef::Named(id) => match renames.get(&id.name) {
573 Some(to) => TypeRef::Named(Ident {
574 name: to.clone(),
575 span: id.span,
576 }),
577 None => t.clone(),
578 },
579 TypeRef::App { name, args, span } => TypeRef::App {
580 name: match renames.get(&name.name) {
581 Some(to) => Ident {
582 name: to.clone(),
583 span: name.span,
584 },
585 None => name.clone(),
586 },
587 args: args.iter().map(|a| rename_type_ref(a, renames)).collect(),
588 span: *span,
589 },
590 TypeRef::Result(a, b, s) => TypeRef::Result(
591 Box::new(rename_type_ref(a, renames)),
592 Box::new(rename_type_ref(b, renames)),
593 *s,
594 ),
595 TypeRef::Option(a, s) => TypeRef::Option(Box::new(rename_type_ref(a, renames)), *s),
596 TypeRef::Effect(a, s) => TypeRef::Effect(Box::new(rename_type_ref(a, renames)), *s),
597 TypeRef::HttpResult(a, s) => TypeRef::HttpResult(Box::new(rename_type_ref(a, renames)), *s),
598 TypeRef::List(a, s) => TypeRef::List(Box::new(rename_type_ref(a, renames)), *s),
599 TypeRef::Map(k, v, s) => TypeRef::Map(
600 Box::new(rename_type_ref(k, renames)),
601 Box::new(rename_type_ref(v, renames)),
602 *s,
603 ),
604 TypeRef::Query(a, s) => TypeRef::Query(Box::new(rename_type_ref(a, renames)), *s),
605 TypeRef::Stream(a, s) => TypeRef::Stream(Box::new(rename_type_ref(a, renames)), *s),
606 TypeRef::Connection(a, s) => TypeRef::Connection(Box::new(rename_type_ref(a, renames)), *s),
607 TypeRef::History(a, s) => TypeRef::History(Box::new(rename_type_ref(a, renames)), *s),
608 TypeRef::Fn(ps, r, s) => TypeRef::Fn(
609 ps.iter().map(|p| rename_type_ref(p, renames)).collect(),
610 Box::new(rename_type_ref(r, renames)),
611 *s,
612 ),
613 TypeRef::Base(..)
614 | TypeRef::QueueResult(_)
615 | TypeRef::Asset(_)
616 | TypeRef::RequestHeaders(_)
617 | TypeRef::TokenClaims(_)
618 | TypeRef::BoundaryRejection(_)
619 | TypeRef::ValidationError(_)
620 | TypeRef::JsonError(_)
621 | TypeRef::Unit(_) => t.clone(),
622 }
623}
624
625/// v0.174 (#592): the concrete `(field-name, field-type)` list for a generic
626/// record instantiation `Name[args…]` — the declared fields with every type
627/// parameter substituted by the matching argument. Returns `None` if `name` is
628/// not a declared generic record or the arity does not match (both guaranteed
629/// impossible by the checker, so this is purely defensive).
630pub fn record_inst_fields(
631 name: &str,
632 args: &[TypeRef],
633 types: &HashMap<String, Arc<TypeDecl>>,
634) -> Option<Vec<(String, TypeRef)>> {
635 let decl = types.get(name)?;
636 let TypeBody::Record(r) = &decl.body else {
637 return None;
638 };
639 if decl.type_params.len() != args.len() {
640 return None;
641 }
642 let subst: HashMap<String, TypeRef> = decl
643 .type_params
644 .iter()
645 .map(|p| p.name.name.clone())
646 .zip(args.iter().cloned())
647 .collect();
648 Some(
649 r.fields
650 .iter()
651 .map(|f| (f.name.name.clone(), subst_type_ref(&f.type_ref, &subst)))
652 .collect(),
653 )
654}
655
656/// #593: the concrete `(variant-name, [(field-name, field-type)])` list for a
657/// generic sum instantiation `Name[args…]` — the declared variants with every
658/// type parameter substituted by the matching argument. The sum analogue of
659/// [`record_inst_fields`]; `None` (defensively) if `name` is not a declared
660/// generic sum or the arity does not match.
661#[allow(clippy::type_complexity)]
662pub fn sum_inst_variants(
663 name: &str,
664 args: &[TypeRef],
665 types: &HashMap<String, Arc<TypeDecl>>,
666) -> Option<Vec<(String, Vec<(String, TypeRef)>)>> {
667 let decl = types.get(name)?;
668 let TypeBody::Sum(s) = &decl.body else {
669 return None;
670 };
671 if decl.type_params.len() != args.len() {
672 return None;
673 }
674 let subst: HashMap<String, TypeRef> = decl
675 .type_params
676 .iter()
677 .map(|p| p.name.name.clone())
678 .zip(args.iter().cloned())
679 .collect();
680 Some(
681 s.variants
682 .iter()
683 .map(|v| {
684 (
685 v.name.name.clone(),
686 v.payload
687 .iter()
688 .map(|f| (f.name.name.clone(), subst_type_ref(&f.type_ref, &subst)))
689 .collect(),
690 )
691 })
692 .collect(),
693 )
694}
695
696/// v0.174 (#592): the monomorphised codec suffix for a generic-record
697/// instantiation — `Paginated[User]` → `Paginated_User`,
698/// `Pair[User, String]` → `Pair_User_String`. #593: shared with generic sums.
699/// Renamed from `app_ts_name` in the move from `bynk-emit`.
700pub fn inst_codec_suffix(name: &str, args: &[TypeRef]) -> String {
701 let mut s = name.to_string();
702 for a in args {
703 s.push('_');
704 s.push_str(&codec_suffix(a));
705 }
706 s
707}
708
709/// The codec-name suffix a `TypeRef` resolves to — `Int`, `Order`,
710/// `Result_Int_String`, `Paginated_User`. Renamed from `inner_ts_name` in the
711/// move from `bynk-emit`; used both to key a [`WireInst`] and, unqualified,
712/// as the bare codec function suffix a same-module call reaches.
713pub fn codec_suffix(t: &TypeRef) -> String {
714 match t {
715 TypeRef::Base(b, _) => b.name().to_string(),
716 // v0.20a: function types are confined to non-boundary positions
717 // (`bynk.types.function_at_boundary`), so the serialisation machinery
718 // can never legally see one.
719 TypeRef::Fn(..)
720 | TypeRef::Query(..)
721 | TypeRef::Stream(..)
722 | TypeRef::Connection(..)
723 | TypeRef::History(..) => {
724 unreachable!("function/query/stream types are rejected at boundaries")
725 }
726 // v0.174 (#592): the codec suffix for a generic-record instantiation —
727 // `Paginated[User]` → `Paginated_User`.
728 TypeRef::App { name, args, .. } => inst_codec_suffix(&name.name, args),
729 TypeRef::Named(id) => id.name.clone(),
730 TypeRef::Result(a, b, _) => format!("Result_{}_{}", codec_suffix(a), codec_suffix(b)),
731 TypeRef::Option(a, _) => format!("Option_{}", codec_suffix(a)),
732 TypeRef::Effect(a, _) => format!("Effect_{}", codec_suffix(a)),
733 TypeRef::HttpResult(a, _) => format!("HttpResult_{}", codec_suffix(a)),
734 TypeRef::List(a, _) => format!("List_{}", codec_suffix(a)),
735 TypeRef::Map(k, v, _) => format!("Map_{}_{}", codec_suffix(k), codec_suffix(v)),
736 TypeRef::QueueResult(_) => "QueueResult".to_string(),
737 TypeRef::Asset(_) => "Asset".to_string(),
738 TypeRef::RequestHeaders(_) => "RequestHeaders".to_string(),
739 TypeRef::TokenClaims(_) => "TokenClaims".to_string(),
740 TypeRef::BoundaryRejection(_) => "BoundaryRejection".to_string(),
741 TypeRef::ValidationError(_) => "ValidationError".to_string(),
742 TypeRef::JsonError(_) => "JsonError".to_string(),
743 TypeRef::Unit(_) => "Unit".to_string(),
744 }
745}
746
747/// v0.22b: the codec closure for a set of `Json.encode`/`Json.decode[T]`
748/// target type-refs — the named types needing per-type helpers (transitively
749/// through record fields and sum payloads) plus the generic instantiations
750/// needing specialised helpers. The same closure logic as the boundary
751/// collectors, rooted at expressions instead of service signatures.
752pub fn collect_codec_closure(
753 roots: &[TypeRef],
754 types: &HashMap<String, Arc<TypeDecl>>,
755) -> (Vec<String>, Vec<WireInst>) {
756 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
757 let mut names: Vec<String> = Vec::new();
758 let mut stack: Vec<String> = Vec::new();
759 let recursive_set = recursive_generic_names(types);
760 let recursive = &recursive_set;
761 for r in roots {
762 collect_type_names(r, &mut stack, types, recursive);
763 }
764 while let Some(name) = stack.pop() {
765 if !seen.insert(name.clone()) {
766 continue;
767 }
768 names.push(name.clone());
769 let Some(decl) = types.get(&name) else {
770 continue;
771 };
772 match &decl.body {
773 TypeBody::Record(r) => {
774 for f in &r.fields {
775 collect_type_names(&f.type_ref, &mut stack, types, recursive);
776 }
777 }
778 TypeBody::Sum(s) => {
779 for v in &s.variants {
780 for p in &v.payload {
781 collect_type_names(&p.type_ref, &mut stack, types, recursive);
782 }
783 }
784 }
785 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
786 }
787 }
788 names.sort();
789
790 let mut insts: Vec<WireInst> = Vec::new();
791 let mut inst_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
792 for r in roots {
793 walk_generic_inst(r, &mut insts, &mut inst_seen, types, recursive);
794 }
795 for name in &names {
796 let Some(decl) = types.get(name) else {
797 continue;
798 };
799 match &decl.body {
800 TypeBody::Record(r) => {
801 for f in &r.fields {
802 walk_generic_inst(&f.type_ref, &mut insts, &mut inst_seen, types, recursive);
803 }
804 }
805 TypeBody::Sum(s) => {
806 for v in &s.variants {
807 for p in &v.payload {
808 walk_generic_inst(
809 &p.type_ref,
810 &mut insts,
811 &mut inst_seen,
812 types,
813 recursive,
814 );
815 }
816 }
817 }
818 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
819 }
820 }
821 (names, insts)
822}
823
824/// Collect the set of `Result<A, B>` / `Option<A>` instantiations used in
825/// boundary positions so the emitter can synthesise the specialised
826/// helpers. v0.18: an instantiation may also appear in the *fields* of a
827/// boundary record or sum payload (e.g. the bynk surface's
828/// `Request.contentType: Option[String]`) — the per-type serialisers
829/// delegate to the specialised generic helpers, so walk those too.
830pub fn collect_generic_instantiations(
831 services: &HashMap<String, ServiceDecl>,
832 // v0.96 (ADR 0124): an agent's `store`-field element types are validated on
833 // rehydration, so a `Cell[Option[Int]]` / `Log[List[T]]` needs its specialised
834 // generic helper emitted just like a boundary signature does.
835 agents: &HashMap<String, AgentDecl>,
836 boundary_type_names: &[String],
837 types: &HashMap<String, Arc<TypeDecl>>,
838 // #1678: further boundary positions, as for `collect_boundary_types`. Walked
839 // after the services and agent stores, so existing emission order is kept.
840 extra_roots: &[TypeRef],
841) -> Vec<WireInst> {
842 let mut out: Vec<WireInst> = Vec::new();
843 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
844 let recursive_set = recursive_generic_names(types);
845 let recursive = &recursive_set;
846 // Iterate services in name order: `HashMap::values()` order varies per
847 // process, and the *emission order* of the specialised helpers follows
848 // first-encounter order here. Surfaced by the first fixture with
849 // multiple same-file services carrying different instantiations (v0.23
850 // #35 CI); latent since v0.8.
851 let mut svc_names: Vec<&String> = services.keys().collect();
852 svc_names.sort();
853 for name in svc_names {
854 let s = &services[name];
855 for h in &s.handlers {
856 for p in &h.params {
857 walk_generic_inst(&p.type_ref, &mut out, &mut seen, types, recursive);
858 }
859 walk_generic_inst(&h.return_type, &mut out, &mut seen, types, recursive);
860 }
861 }
862 let mut agent_names: Vec<&String> = agents.keys().collect();
863 agent_names.sort();
864 for name in agent_names {
865 for f in &agents[name].store_fields {
866 for arg in &f.kind.args {
867 walk_generic_inst(arg, &mut out, &mut seen, types, recursive);
868 }
869 }
870 for m in &agents[name].migrations {
871 if let Some(old) = &m.old {
872 walk_generic_inst(&old.type_ref, &mut out, &mut seen, types, recursive);
873 }
874 }
875 }
876 for r in extra_roots {
877 walk_generic_inst(r, &mut out, &mut seen, types, recursive);
878 }
879 for name in boundary_type_names {
880 let Some(decl) = types.get(name) else {
881 continue;
882 };
883 // v0.174 (#592): never walk a *generic* declaration's own fields — they
884 // are the declared, unsubstituted body (`Paginated[T] = { items: List[T]
885 // }`), so walking `List[T]` would emit a bogus `serialise_List_T` over the
886 // unbound type variable `T`. The instantiations a generic record needs
887 // come from its *use* sites (`Paginated[User]`, a `TypeRef::App`), which
888 // `walk_generic_inst` expands with concrete arguments. This mirrors the
889 // `emit_helpers_for_owner` skip that keeps a bare `serialise_Paginated`
890 // from being emitted.
891 if !decl.type_params.is_empty() {
892 continue;
893 }
894 match &decl.body {
895 TypeBody::Record(r) => {
896 for f in &r.fields {
897 walk_generic_inst(&f.type_ref, &mut out, &mut seen, types, recursive);
898 }
899 }
900 TypeBody::Sum(s) => {
901 for v in &s.variants {
902 for p in &v.payload {
903 walk_generic_inst(&p.type_ref, &mut out, &mut seen, types, recursive);
904 }
905 }
906 }
907 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
908 }
909 }
910 out
911}
912
913/// A boundary occurrence of a generic instantiation needing its own
914/// specialised codec — `Result<A, B>`, `Option<A>`, `List<A>`, `Map<K, V>`,
915/// or a generic user record/sum applied to concrete arguments
916/// (`Paginated[User]`, `ApiResult[User]`).
917///
918/// Moved from `bynk-emit/src/emitter/serialisation.rs`'s `GenericInst` as
919/// part of the #855 IR extraction. Variant names are kept exactly as they
920/// were (`ResultInst`, not the plan's bare `Result`, …): `bynk-emit`'s
921/// `emit_generic_helpers_qualified` pattern-matches these variants directly.
922/// Phase 2 step 9 ("Generic helpers") revisited the plan's bare
923/// `Result`/`Option`/`List`/`Map`/`Record`/`Sum` spelling and **kept** the
924/// `*Inst` suffix — see the "Moved verbatim" comment block above this
925/// module's walk functions for the reasoning (a pure rename of six match
926/// arms with no behavioural or architectural payoff).
927#[derive(Debug, Clone)]
928#[allow(clippy::enum_variant_names)]
929pub enum WireInst {
930 ResultInst {
931 ok: TypeRef,
932 err: TypeRef,
933 },
934 OptionInst {
935 inner: TypeRef,
936 },
937 /// v0.20b: a `List[T]` boundary instantiation — element-wise wire format.
938 ListInst {
939 elem: TypeRef,
940 },
941 /// v0.20b: a `Map[K, V]` boundary instantiation — entries-array wire
942 /// format (`[[k, v], …]`), insertion-ordered.
943 MapInst {
944 key: TypeRef,
945 val: TypeRef,
946 },
947 /// v0.174 (#592): a generic user-record instantiation `Name[args…]` — a
948 /// monomorphised per-instantiation record codec (`serialise_Paginated_User`)
949 /// specialised to the concrete arguments (ADR 0183 Decision C's follow-on).
950 RecordInst {
951 name: String,
952 args: Vec<TypeRef>,
953 },
954 /// #593: a generic user-sum instantiation `Name[args…]` — a monomorphised
955 /// per-instantiation discriminated-union codec (`serialise_ApiResult_User`),
956 /// the sum analogue of [`WireInst::RecordInst`].
957 SumInst {
958 name: String,
959 args: Vec<TypeRef>,
960 },
961}
962
963impl WireInst {
964 pub fn ts_name(&self) -> String {
965 match self {
966 WireInst::ResultInst { ok, err } => {
967 format!("Result_{}_{}", codec_suffix(ok), codec_suffix(err))
968 }
969 WireInst::OptionInst { inner } => {
970 format!("Option_{}", codec_suffix(inner))
971 }
972 WireInst::ListInst { elem } => format!("List_{}", codec_suffix(elem)),
973 WireInst::MapInst { key, val } => {
974 format!("Map_{}_{}", codec_suffix(key), codec_suffix(val))
975 }
976 WireInst::RecordInst { name, args } => inst_codec_suffix(name, args),
977 WireInst::SumInst { name, args } => inst_codec_suffix(name, args),
978 }
979 }
980}
981
982fn walk_generic_inst(
983 t: &TypeRef,
984 out: &mut Vec<WireInst>,
985 seen: &mut std::collections::HashSet<String>,
986 types: &HashMap<String, Arc<TypeDecl>>,
987 recursive: &std::collections::HashSet<String>,
988) {
989 match t {
990 // v0.174 (#592): a generic-record instantiation needs a monomorphised
991 // codec, and so do the generic instantiations reachable through its
992 // concrete field types (`Paginated[User]` → `List[User]` →
993 // `serialise_List_User`, `Envelope[Box[User]]` → `Box[User]` →
994 // `serialise_Box_User`). Substitute the fields and walk them. A recursive
995 // generic record (no finite codec set) is rejected at the boundary before
996 // emit; the guard here is defence in depth so this walk always terminates
997 // (the `seen` dedup alone cannot bound *polymorphic* recursion, whose
998 // instantiations each carry a distinct name).
999 TypeRef::App { name, args, .. } => {
1000 if recursive.contains(&name.name) {
1001 return;
1002 }
1003 // #593: an `App` names a generic record OR a generic sum; dispatch on
1004 // the declaration's body so the right monomorphised codec is emitted,
1005 // and walk the reachable instantiations through its concrete member
1006 // types (a sum's variant payloads, a record's fields).
1007 let is_sum = matches!(
1008 types.get(&name.name).map(|d| &d.body),
1009 Some(TypeBody::Sum(_))
1010 );
1011 let inst = if is_sum {
1012 WireInst::SumInst {
1013 name: name.name.clone(),
1014 args: args.clone(),
1015 }
1016 } else {
1017 WireInst::RecordInst {
1018 name: name.name.clone(),
1019 args: args.clone(),
1020 }
1021 };
1022 let key = inst.ts_name();
1023 if !seen.insert(key) {
1024 return;
1025 }
1026 out.push(inst);
1027 for a in args {
1028 walk_generic_inst(a, out, seen, types, recursive);
1029 }
1030 if is_sum {
1031 if let Some(variants) = sum_inst_variants(&name.name, args, types) {
1032 for (_, payload) in &variants {
1033 for (_, ft) in payload {
1034 walk_generic_inst(ft, out, seen, types, recursive);
1035 }
1036 }
1037 }
1038 } else if let Some(fields) = record_inst_fields(&name.name, args, types) {
1039 for (_, ft) in &fields {
1040 walk_generic_inst(ft, out, seen, types, recursive);
1041 }
1042 }
1043 }
1044 TypeRef::Result(a, b, _) => {
1045 let inst = WireInst::ResultInst {
1046 ok: (**a).clone(),
1047 err: (**b).clone(),
1048 };
1049 let key = inst.ts_name();
1050 if seen.insert(key) {
1051 out.push(inst);
1052 }
1053 walk_generic_inst(a, out, seen, types, recursive);
1054 walk_generic_inst(b, out, seen, types, recursive);
1055 }
1056 TypeRef::Option(a, _) => {
1057 let inst = WireInst::OptionInst {
1058 inner: (**a).clone(),
1059 };
1060 let key = inst.ts_name();
1061 if seen.insert(key) {
1062 out.push(inst);
1063 }
1064 walk_generic_inst(a, out, seen, types, recursive);
1065 }
1066 TypeRef::Effect(a, _) => walk_generic_inst(a, out, seen, types, recursive),
1067 TypeRef::HttpResult(a, _) => walk_generic_inst(a, out, seen, types, recursive),
1068 TypeRef::List(a, _) => {
1069 let inst = WireInst::ListInst {
1070 elem: (**a).clone(),
1071 };
1072 let key = inst.ts_name();
1073 if seen.insert(key) {
1074 out.push(inst);
1075 }
1076 walk_generic_inst(a, out, seen, types, recursive);
1077 }
1078 TypeRef::Map(k, v, _) => {
1079 let inst = WireInst::MapInst {
1080 key: (**k).clone(),
1081 val: (**v).clone(),
1082 };
1083 let key = inst.ts_name();
1084 if seen.insert(key) {
1085 out.push(inst);
1086 }
1087 walk_generic_inst(k, out, seen, types, recursive);
1088 walk_generic_inst(v, out, seen, types, recursive);
1089 }
1090 _ => {}
1091 }
1092}
1093
1094// ---------------------------------------------------------------------
1095// New: resolving a `TypeRef` / `TypeDecl` into the IR above. Not yet called
1096// by any codec path — `bynk-emit` still derives its own shape inline
1097// (Phase 2 switches it over). Exercised only by this module's own tests
1098// until then.
1099// ---------------------------------------------------------------------
1100
1101/// The base-type guards a bare **field-position** value (a record field, a
1102/// sum-variant payload field, a collection element) is checked against,
1103/// mirroring `emit_field_deserialise`'s `TypeRef::Base` arm: `Int`/`Duration`/
1104/// `Instant` must be whole, `Float` must be finite. #1969: `Duration` joined the
1105/// first group; a top-level `Duration` was already guarded, a field was not. Contrast [`WireScalar::base_guards`]
1106/// (a **named-type** consumed-inline revalidation), which guards `Int`/`Float`
1107/// only — the two positions are not the same check in the code this was
1108/// extracted from, and that asymmetry is preserved rather than merged.
1109pub fn field_base_guards(b: BaseType) -> Vec<BaseGuard> {
1110 match b {
1111 BaseType::Int | BaseType::Duration | BaseType::Instant => vec![BaseGuard::Integral],
1112 BaseType::Float => vec![BaseGuard::Finite],
1113 _ => Vec::new(),
1114 }
1115}
1116
1117/// The base-type guards a named type's `Inline` (consumed, transparent)
1118/// revalidation applies, mirroring `emit_inline_refinement_checks` exactly:
1119/// `Int` → `Integral`, `Float` → `Finite`. See [`WireScalar::base_guards`]'s
1120/// doc for why `Instant` is deliberately absent here despite being guarded
1121/// at field position.
1122fn scalar_base_guards(b: BaseType) -> Vec<BaseGuard> {
1123 match b {
1124 BaseType::Int => vec![BaseGuard::Integral],
1125 BaseType::Float => vec![BaseGuard::Finite],
1126 _ => Vec::new(),
1127 }
1128}
1129
1130/// Resolve one `TypeRef` occurrence (a field's type, a variant payload
1131/// field's type, a generic argument) into its [`WireRef`] shape — the same
1132/// dispatch `emit_field_deserialise` performs today, one level removed from
1133/// any TS string.
1134///
1135/// `types` is threaded through for signature symmetry with the walks above
1136/// and forward compatibility with a future disambiguation need; this
1137/// resolution is single-level (unlike `collect_type_names`'s transitive
1138/// walk) and does not currently consult it.
1139///
1140/// **This encodes the *deserialise*-side dispatch only** (mirroring
1141/// `emit_field_deserialise`'s arms one-for-one, including its `Effect` /
1142/// `HttpResult` arm, which it folds into the same unchecked cast as
1143/// `ValidationError`/`JsonError`/`QueueResult`). The emitter does not
1144/// actually agree with itself on those two shapes across its other three
1145/// dispatches: `serialise_field_expr_via` *recurses* through `Effect` rather
1146/// than casting it unchecked, `deserialise_expr_via` also recurses through
1147/// `Effect`, and `codec_suffix` gives both `Effect` and `HttpResult` their
1148/// own composed names (`Effect_<inner>`, `HttpResult_<inner>`) rather than
1149/// treating them as opaque. A future serialise-side or codec-suffix-side
1150/// `WireRef` derivation must **not** assume this function's `Unchecked`
1151/// answer already covers it — that would silently change `Effect`-at-field-
1152/// position from "recurse into the inner type" to "cast unchecked" the
1153/// moment Phase 2 routes the serialise path through this same resolver.
1154/// Reconciling (or deliberately keeping two resolvers) is a Phase 2 decision,
1155/// not implied by this function's existence.
1156pub fn wire_ref(t: &TypeRef, _types: &HashMap<String, Arc<TypeDecl>>) -> WireRef {
1157 match t {
1158 // v0.20a: function types are confined to non-boundary positions
1159 // (`bynk.types.function_at_boundary`), so the serialisation machinery
1160 // can never legally see one — mirrors `emit_field_deserialise`'s
1161 // `unreachable!` on this same arm.
1162 TypeRef::Fn(..)
1163 | TypeRef::Query(..)
1164 | TypeRef::Stream(..)
1165 | TypeRef::Connection(..)
1166 | TypeRef::History(..) => {
1167 unreachable!(
1168 "function/query/stream/connection/history types are rejected at boundaries"
1169 )
1170 }
1171 // v0.110 (ADR 0142 D5): a bare `Bytes` field is a base64 JSON string,
1172 // decoded (not cast) into a `Uint8Array`.
1173 TypeRef::Base(BaseType::Bytes, _) => WireRef::Bytes,
1174 TypeRef::Base(b, _) => {
1175 let json = json_kind_of(*b);
1176 WireRef::Base {
1177 base: *b,
1178 json,
1179 guards: field_base_guards(*b),
1180 expected: Expected::Json(json),
1181 }
1182 }
1183 TypeRef::Named(id) => WireRef::Named {
1184 name: id.name.clone(),
1185 },
1186 // Every generic instantiation — App/Result/Option/List/Map — resolves
1187 // to the same codec-suffix key; `codec_suffix` already builds exactly
1188 // that string for each of these shapes.
1189 TypeRef::App { .. }
1190 | TypeRef::Result(..)
1191 | TypeRef::Option(..)
1192 | TypeRef::List(..)
1193 | TypeRef::Map(..) => WireRef::Inst {
1194 key: codec_suffix(t),
1195 },
1196 TypeRef::Effect(..) => WireRef::Unchecked {
1197 reason: UncheckedReason::Effect,
1198 },
1199 TypeRef::HttpResult(..) => WireRef::Unchecked {
1200 reason: UncheckedReason::HttpResult,
1201 },
1202 TypeRef::ValidationError(_) => WireRef::Unchecked {
1203 reason: UncheckedReason::ValidationError,
1204 },
1205 TypeRef::JsonError(_) => WireRef::Unchecked {
1206 reason: UncheckedReason::JsonError,
1207 },
1208 TypeRef::QueueResult(_) => WireRef::Unchecked {
1209 reason: UncheckedReason::QueueResult,
1210 },
1211 // #1883, slice 1a: never reached through a well-checked program — an
1212 // `Asset` at a boundary is `bynk.types.asset_at_boundary` — but the
1213 // model still needs a total answer.
1214 TypeRef::Asset(_)
1215 | TypeRef::RequestHeaders(_)
1216 | TypeRef::TokenClaims(_)
1217 | TypeRef::BoundaryRejection(_) => WireRef::Unchecked {
1218 reason: UncheckedReason::Asset,
1219 },
1220 TypeRef::Unit(_) => WireRef::Unit,
1221 }
1222}
1223
1224fn wire_fields(fields: &[RecordField], types: &HashMap<String, Arc<TypeDecl>>) -> Vec<WireField> {
1225 fields
1226 .iter()
1227 .map(|f| WireField {
1228 name: f.name.name.clone(),
1229 shape: wire_ref(&f.type_ref, types),
1230 path_segment: f.name.name.clone(),
1231 default: f.init.as_ref().map(|e| (e.clone(), f.type_ref.clone())),
1232 predicates: field_predicates(f),
1233 })
1234 .collect()
1235}
1236
1237/// #1969: a record field's inline refinement predicates, in declaration order
1238/// (`qty: Int where Positive` → `[Positive]`). The decoder checks them after
1239/// the field's own codec, so a refined field reads the same from the wire as a
1240/// field of a named refined type.
1241pub fn field_predicates(f: &RecordField) -> Vec<PredKind> {
1242 f.refinement
1243 .as_ref()
1244 .map(|r| r.predicates.iter().map(|p| p.kind.clone()).collect())
1245 .unwrap_or_default()
1246}
1247
1248fn wire_sum(body: &SumBody, types: &HashMap<String, Arc<TypeDecl>>) -> WireSum {
1249 WireSum {
1250 wire_discriminant: WIRE_DISCRIMINANT,
1251 memory_discriminant: "tag",
1252 variants: body
1253 .variants
1254 .iter()
1255 .map(|v| WireVariant {
1256 name: v.name.name.clone(),
1257 payload: v
1258 .payload
1259 .iter()
1260 .map(|f| WireField {
1261 name: f.name.name.clone(),
1262 shape: wire_ref(&f.type_ref, types),
1263 path_segment: f.name.name.clone(),
1264 // Sum-variant payload fields carry no default in the
1265 // surface grammar (`VariantField` has no `init`).
1266 default: None,
1267 predicates: Vec::new(),
1268 })
1269 .collect(),
1270 })
1271 .collect(),
1272 }
1273}
1274
1275/// Build one [`WireScalar`], choosing [`Revalidation`] per #661 Decisions
1276/// C/D plus the owner path: `Owned` → `ViaConstructor`; consumed + opaque →
1277/// `StructuralOnly`; consumed + transparent → `Inline`; a `Bytes` base →
1278/// `Base64Decode` regardless of provenance, mirroring `emit_refined`'s early
1279/// return to the dedicated `Bytes` codec before the owned/consumed split is
1280/// even considered.
1281fn wire_scalar(
1282 base: BaseType,
1283 opaque: bool,
1284 refinement: Option<&Refinement>,
1285 prov: &Provenance,
1286) -> WireScalar {
1287 let json = json_kind_of(base);
1288 let predicates: Vec<PredKind> = refinement
1289 .map(|r| r.predicates.iter().map(|p| p.kind.clone()).collect())
1290 .unwrap_or_default();
1291 let revalidation = if base == BaseType::Bytes {
1292 Revalidation::Base64Decode
1293 } else {
1294 match prov {
1295 Provenance::Owned => Revalidation::ViaConstructor,
1296 Provenance::Consumed { .. } if opaque => Revalidation::StructuralOnly,
1297 Provenance::Consumed { .. } => Revalidation::Inline,
1298 }
1299 };
1300 WireScalar {
1301 base,
1302 json,
1303 opaque,
1304 predicates,
1305 base_guards: scalar_base_guards(base),
1306 revalidation,
1307 }
1308}
1309
1310/// Build a [`WireType`] from a `TypeDecl`, using the same base/predicate/
1311/// opaque logic currently inline in `bynk-emit`'s `emit_refined` and
1312/// `emit_bytes_named_codec` — without any TS string emission. Returns `None`
1313/// for a **generic** declaration (`decl.type_params` non-empty): a generic
1314/// record/sum has no single bare `WireType` of its own, only per-instantiation
1315/// shapes (`WireInst`), mirroring the skip in `emit_helpers_for_owner_qualified`.
1316pub fn wire_type(
1317 name: &str,
1318 decl: &TypeDecl,
1319 types: &HashMap<String, Arc<TypeDecl>>,
1320 prov: Provenance,
1321) -> Option<WireType> {
1322 if !decl.type_params.is_empty() {
1323 return None;
1324 }
1325 let body = match &decl.body {
1326 TypeBody::Refined {
1327 base, refinement, ..
1328 } => WireBody::Scalar(wire_scalar(*base, false, refinement.as_ref(), &prov)),
1329 TypeBody::Opaque {
1330 base, refinement, ..
1331 } => WireBody::Scalar(wire_scalar(*base, true, refinement.as_ref(), &prov)),
1332 TypeBody::Record(r) => WireBody::Record {
1333 fields: wire_fields(&r.fields, types),
1334 },
1335 TypeBody::Sum(s) => WireBody::Sum(wire_sum(s, types)),
1336 };
1337 Some(WireType {
1338 name: name.to_string(),
1339 codec_suffix: name.to_string(),
1340 provenance: prov,
1341 body,
1342 })
1343}
1344
1345/// Build the full [`WireModel`] for a boundary: every non-generic named type
1346/// in `type_names` resolved through `types`, plus the generic instantiations
1347/// the caller already collected (`collect_generic_instantiations` /
1348/// `collect_codec_closure`). `provenance` is supplied by the caller —
1349/// `bynk-emit` knows the build target and the consumed set; this crate must
1350/// not (see the [`Provenance`] doc).
1351pub fn boundary_model(
1352 type_names: &[String],
1353 types: &HashMap<String, Arc<TypeDecl>>,
1354 instantiations: Vec<WireInst>,
1355 provenance: impl Fn(&str) -> Provenance,
1356) -> WireModel {
1357 let recursive: BTreeSet<String> = recursive_generic_names(types).into_iter().collect();
1358 let mut wtypes = Vec::with_capacity(type_names.len());
1359 for name in type_names {
1360 let Some(decl) = types.get(name) else {
1361 continue;
1362 };
1363 if let Some(wt) = wire_type(name, decl, types, provenance(name)) {
1364 wtypes.push(wt);
1365 }
1366 }
1367 WireModel {
1368 types: wtypes,
1369 instantiations,
1370 recursive,
1371 }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376 use super::*;
1377
1378 fn types_of(src: &str) -> HashMap<String, Arc<TypeDecl>> {
1379 let tokens = bynk_syntax::lexer::tokenize(src).expect("lex");
1380 let commons = bynk_syntax::parser::parse(&tokens, src).expect("parse");
1381 commons
1382 .items
1383 .iter()
1384 .filter_map(|i| match i {
1385 CommonsItem::Type(t) => Some((t.name.name.clone(), Arc::new(t.clone()))),
1386 _ => None,
1387 })
1388 .collect()
1389 }
1390
1391 /// Parse a `context` unit and split it into its `types` and `services`
1392 /// tables, mirroring `bynk-emit`'s own boundary-collection call sites.
1393 fn context_of(src: &str) -> (HashMap<String, Arc<TypeDecl>>, HashMap<String, ServiceDecl>) {
1394 let tokens = bynk_syntax::lexer::tokenize(src).expect("lex");
1395 let unit = bynk_syntax::parser::parse_unit(&tokens, src).expect("parse");
1396 let SourceUnit::Context(ctx) = unit else {
1397 panic!("expected a context unit");
1398 };
1399 let mut types = HashMap::new();
1400 let mut services = HashMap::new();
1401 for item in &ctx.items {
1402 match item {
1403 CommonsItem::Type(t) => {
1404 types.insert(t.name.name.clone(), Arc::new(t.clone()));
1405 }
1406 CommonsItem::Service(s) => {
1407 services.insert(s.name.name.clone(), s.clone());
1408 }
1409 _ => {}
1410 }
1411 }
1412 (types, services)
1413 }
1414
1415 #[test]
1416 fn json_kind_of_matches_the_ts_base_for_serialisation_mapping() {
1417 assert_eq!(json_kind_of(BaseType::Int), JsonKind::Number);
1418 assert_eq!(json_kind_of(BaseType::Float), JsonKind::Number);
1419 assert_eq!(json_kind_of(BaseType::Duration), JsonKind::Number);
1420 assert_eq!(json_kind_of(BaseType::Instant), JsonKind::Number);
1421 assert_eq!(json_kind_of(BaseType::String), JsonKind::String);
1422 // v0.110 (ADR 0142 D5): a `Bytes` wires as a base64 JSON string.
1423 assert_eq!(json_kind_of(BaseType::Bytes), JsonKind::String);
1424 assert_eq!(json_kind_of(BaseType::Bool), JsonKind::Boolean);
1425 }
1426
1427 #[test]
1428 fn collect_boundary_types_walks_params_return_and_record_fields() {
1429 let src = r#"
1430context test
1431
1432type ClientId = String where NonEmpty
1433type Rate = { count: Int, limit: Int }
1434
1435service Api {
1436 on call(client: ClientId) -> Effect[Rate] {
1437 client
1438 }
1439}
1440"#;
1441 let (types, services) = context_of(src);
1442 let agents: HashMap<String, AgentDecl> = HashMap::new();
1443 let names = collect_boundary_types(&types, &services, &agents, &[]);
1444 assert_eq!(names, vec!["ClientId".to_string(), "Rate".to_string()]);
1445 }
1446
1447 #[test]
1448 fn collect_boundary_types_ignores_types_unreachable_from_any_handler() {
1449 let src = r#"
1450context test
1451
1452type Used = String where NonEmpty
1453type Unused = { n: Int }
1454
1455service Api {
1456 on call(x: Used) -> Effect[Used] {
1457 x
1458 }
1459}
1460"#;
1461 let (types, services) = context_of(src);
1462 let agents: HashMap<String, AgentDecl> = HashMap::new();
1463 let names = collect_boundary_types(&types, &services, &agents, &[]);
1464 assert_eq!(names, vec!["Used".to_string()]);
1465 }
1466
1467 /// The highest-risk drift trap the plan flags: `contract.rs` sorts
1468 /// predicates (a precondition for hash correctness), but this IR must
1469 /// carry **declaration order** — `Inline` revalidation emits one `if`
1470 /// per predicate in source order. `NonEmpty` before `MaxLength` sorts
1471 /// (alphabetically, as `contract.rs`'s `canon_refinement` would — folding
1472 /// `NonEmpty` to `MinLength(1)` there does not change this: `MaxLength` <
1473 /// `MinLength` alphabetically, same relative order as `MaxLength` <
1474 /// `NonEmpty` before the fold) to `MaxLength` first, so this fixture's
1475 /// declaration order visibly differs from the sorted order — a positive
1476 /// assertion that the trap stays closed.
1477 #[test]
1478 fn wire_scalar_predicates_preserve_declaration_order_not_sorted() {
1479 let types = types_of("commons x\n\ntype Id = String where NonEmpty && MaxLength(20)\n");
1480 let decl = &types["Id"];
1481 let wt = wire_type("Id", decl, &types, Provenance::Owned).expect("scalar wire type");
1482 let WireBody::Scalar(scalar) = &wt.body else {
1483 panic!("expected a scalar body, got {:?}", wt.body);
1484 };
1485 assert_eq!(scalar.predicates.len(), 2);
1486 assert!(
1487 matches!(scalar.predicates[0], PredKind::NonEmpty),
1488 "predicates[0] must be the declared-first NonEmpty, got {:?}",
1489 scalar.predicates[0]
1490 );
1491 assert!(
1492 matches!(scalar.predicates[1], PredKind::MaxLength(20)),
1493 "predicates[1] must be the declared-second MaxLength(20), got {:?}",
1494 scalar.predicates[1]
1495 );
1496 // The sorted (contract.rs-style) order would put MaxLength first —
1497 // assert this IR does not match that order.
1498 assert!(
1499 !matches!(scalar.predicates[0], PredKind::MaxLength(_)),
1500 "predicates must be in declaration order, not sorted"
1501 );
1502 }
1503
1504 #[test]
1505 fn wire_type_selects_revalidation_by_provenance_and_opacity() {
1506 let types = types_of(
1507 "commons x\n\ntype Refined = String where NonEmpty\ntype Opaque = opaque String where NonEmpty\ntype Raw = Bytes\n",
1508 );
1509
1510 let refined = wire_type("Refined", &types["Refined"], &types, Provenance::Owned)
1511 .expect("owned scalar");
1512 let WireBody::Scalar(s) = &refined.body else {
1513 panic!("expected scalar")
1514 };
1515 assert_eq!(s.revalidation, Revalidation::ViaConstructor);
1516
1517 let consumed = Provenance::Consumed {
1518 owner_unit: "other".to_string(),
1519 };
1520 let refined_consumed =
1521 wire_type("Refined", &types["Refined"], &types, consumed.clone()).expect("consumed");
1522 let WireBody::Scalar(s) = &refined_consumed.body else {
1523 panic!("expected scalar")
1524 };
1525 assert_eq!(s.revalidation, Revalidation::Inline);
1526
1527 let opaque_consumed =
1528 wire_type("Opaque", &types["Opaque"], &types, consumed.clone()).expect("consumed");
1529 let WireBody::Scalar(s) = &opaque_consumed.body else {
1530 panic!("expected scalar")
1531 };
1532 assert_eq!(s.revalidation, Revalidation::StructuralOnly);
1533
1534 // Bytes: Base64Decode regardless of provenance.
1535 let raw_owned =
1536 wire_type("Raw", &types["Raw"], &types, Provenance::Owned).expect("owned bytes");
1537 let WireBody::Scalar(s) = &raw_owned.body else {
1538 panic!("expected scalar")
1539 };
1540 assert_eq!(s.revalidation, Revalidation::Base64Decode);
1541 let raw_consumed = wire_type("Raw", &types["Raw"], &types, consumed).expect("consumed");
1542 let WireBody::Scalar(s) = &raw_consumed.body else {
1543 panic!("expected scalar")
1544 };
1545 assert_eq!(s.revalidation, Revalidation::Base64Decode);
1546 }
1547
1548 #[test]
1549 fn wire_type_returns_none_for_a_generic_declaration() {
1550 let types = types_of("commons x\n\ntype Page[T] = { items: List[T] }\n");
1551 assert!(wire_type("Page", &types["Page"], &types, Provenance::Owned).is_none());
1552 }
1553
1554 /// Part 1.4's cross-check: for an `on call` handler, `wire.rs`'s codec
1555 /// walk (`collect_boundary_types`, restricted to that one handler) and
1556 /// `contract.rs`'s canonical hash form (`service_normal_form`, over the
1557 /// same handler projected as a `CrossContextService`) must reach the
1558 /// exact same *set* of named boundary types — even though the module
1559 /// doc's comparison table says the two disagree on purpose about
1560 /// *ordering* (declaration order vs sorted) and about whether an opaque
1561 /// predicate is shown. This test asserts REACHABILITY only: the set of
1562 /// type names each derivation's walk visits from the same root, not any
1563 /// string equality or order between the two renderings.
1564 ///
1565 /// The type names in the fixture are chosen so none is a substring of
1566 /// another (`ClientId`, `Address`, `Profile`, `Unrelated`) —
1567 /// `contract.rs`'s `canon_type`/`canon_named_in` are private to that
1568 /// module, so rather than duplicating their traversal here, this test
1569 /// recovers the set of types `service_normal_form`'s rendered string
1570 /// reaches by substring containment against every declared type name.
1571 /// With non-overlapping names that containment test is unambiguous.
1572 ///
1573 /// `Unrelated` is declared but reachable from no handler, so the
1574 /// equality assertion below is not vacuously true merely because this
1575 /// fixture happens to make every *other* declared type reachable — a
1576 /// regression that made `collect_boundary_types` return every declared
1577 /// name instead of the reachable closure would leak `Unrelated` into
1578 /// `boundary_names`, and this test would catch it.
1579 #[test]
1580 fn boundary_reachability_agrees_with_contract_normal_form() {
1581 let src = r#"
1582context test
1583
1584type ClientId = String where NonEmpty
1585type Address = { street: String, city: String }
1586type Profile = { id: ClientId, home: Address }
1587
1588// Declared, but reachable from no handler below (see the doc comment on
1589// this test for why).
1590type Unrelated = { n: Int }
1591
1592service Api {
1593 on call(client: ClientId) -> Effect[Profile] {
1594 Profile { id: client, home: Address { street: "x", city: "y" } }
1595 }
1596}
1597"#;
1598 let (types, services) = context_of(src);
1599 let service = &services["Api"];
1600 let handler = &service.handlers[0];
1601
1602 // Restrict `collect_boundary_types`'s walk to exactly this handler —
1603 // the same synthetic-single-handler-service narrowing
1604 // `bynk-ide`'s `wire_contract_for_service` performs, so a service
1605 // with more than one handler could not leak an unrelated handler's
1606 // types into either side of this comparison.
1607 let mut narrowed = service.clone();
1608 narrowed.handlers = vec![handler.clone()];
1609 let narrowed_services: HashMap<String, ServiceDecl> =
1610 HashMap::from([("Api".to_string(), narrowed)]);
1611 let agents: HashMap<String, AgentDecl> = HashMap::new();
1612 let boundary_names: std::collections::HashSet<String> =
1613 collect_boundary_types(&types, &narrowed_services, &agents, &[])
1614 .into_iter()
1615 .collect();
1616
1617 // The same handler, projected as a `CrossContextService` exactly as
1618 // `bynk-emit`'s `own_contract_hashes` / `bynk-ide`'s
1619 // `wire_contract::contract_form` project it, canonicalised through
1620 // the same type table.
1621 let svc = crate::resolver::CrossContextService {
1622 name: "Api".to_string(),
1623 params: handler
1624 .params
1625 .iter()
1626 .map(|p| (p.name.name.clone(), p.type_ref.clone()))
1627 .collect(),
1628 return_type: handler.return_type.clone(),
1629 span: handler.span,
1630 foreign_callers: Vec::new(),
1631 };
1632 let normal_form = crate::contract::service_normal_form(&svc, &types);
1633
1634 let contract_reachable: std::collections::HashSet<String> = types
1635 .keys()
1636 .filter(|name| normal_form.contains(name.as_str()))
1637 .cloned()
1638 .collect();
1639
1640 assert_eq!(
1641 boundary_names, contract_reachable,
1642 "wire::collect_boundary_types and contract::service_normal_form must \
1643 agree on *which* named types a handler's contract reaches, even though \
1644 they render/order that set completely differently (see this module's \
1645 doc comparison table):\n\
1646 wire.rs reached: {boundary_names:?}\n\
1647 contract.rs reached: {contract_reachable:?}\n\
1648 normal form: {normal_form:?}"
1649 );
1650 assert!(
1651 !boundary_names.contains("Unrelated") && !contract_reachable.contains("Unrelated"),
1652 "`Unrelated` is declared but reachable from no handler — both derivations \
1653 must exclude it, not merely agree on every declared type by coincidence:\n\
1654 wire.rs reached: {boundary_names:?}\n\
1655 contract.rs reached: {contract_reachable:?}"
1656 );
1657 }
1658
1659 /// `wire_ref` is the plan's flagged highest-risk function ("its two
1660 /// functions must stay in exact agreement about which `WireRef` arm a
1661 /// `TypeRef` resolves to"). Pin the arm each shape lands on before Phase 2
1662 /// starts consuming it.
1663 #[test]
1664 fn wire_ref_dispatches_every_type_ref_shape_to_the_expected_arm() {
1665 let types: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1666 let sp = || bynk_syntax::span::Span::new(0, 0);
1667 let named = |n: &str| {
1668 TypeRef::Named(Ident {
1669 name: n.to_string(),
1670 span: sp(),
1671 })
1672 };
1673 let base = |b: BaseType| TypeRef::Base(b, sp());
1674
1675 assert!(matches!(
1676 wire_ref(&base(BaseType::Int), &types),
1677 WireRef::Base {
1678 base: BaseType::Int,
1679 json: JsonKind::Number,
1680 ..
1681 }
1682 ));
1683 assert!(matches!(
1684 wire_ref(&base(BaseType::Bytes), &types),
1685 WireRef::Bytes
1686 ));
1687 assert!(
1688 matches!(wire_ref(&named("ClientId"), &types), WireRef::Named { name } if name == "ClientId")
1689 );
1690 assert!(matches!(
1691 wire_ref(
1692 &TypeRef::Result(Box::new(base(BaseType::Int)), Box::new(base(BaseType::String)), sp()),
1693 &types
1694 ),
1695 WireRef::Inst { key } if key == "Result_Int_String"
1696 ));
1697 assert!(matches!(
1698 wire_ref(&TypeRef::Option(Box::new(base(BaseType::Int)), sp()), &types),
1699 WireRef::Inst { key } if key == "Option_Int"
1700 ));
1701 assert!(matches!(
1702 wire_ref(&TypeRef::List(Box::new(base(BaseType::Int)), sp()), &types),
1703 WireRef::Inst { key } if key == "List_Int"
1704 ));
1705 assert!(matches!(
1706 wire_ref(
1707 &TypeRef::Map(Box::new(base(BaseType::String)), Box::new(base(BaseType::Int)), sp()),
1708 &types
1709 ),
1710 WireRef::Inst { key } if key == "Map_String_Int"
1711 ));
1712 assert!(matches!(
1713 wire_ref(
1714 &TypeRef::App {
1715 name: Ident { name: "Paginated".to_string(), span: sp() },
1716 args: vec![named("User")],
1717 span: sp(),
1718 },
1719 &types
1720 ),
1721 WireRef::Inst { key } if key == "Paginated_User"
1722 ));
1723 assert!(matches!(
1724 wire_ref(&TypeRef::Unit(sp()), &types),
1725 WireRef::Unit
1726 ));
1727 assert!(matches!(
1728 wire_ref(
1729 &TypeRef::Effect(Box::new(base(BaseType::Int)), sp()),
1730 &types
1731 ),
1732 WireRef::Unchecked {
1733 reason: UncheckedReason::Effect
1734 }
1735 ));
1736 assert!(matches!(
1737 wire_ref(
1738 &TypeRef::HttpResult(Box::new(base(BaseType::Int)), sp()),
1739 &types
1740 ),
1741 WireRef::Unchecked {
1742 reason: UncheckedReason::HttpResult
1743 }
1744 ));
1745 assert!(matches!(
1746 wire_ref(&TypeRef::ValidationError(sp()), &types),
1747 WireRef::Unchecked {
1748 reason: UncheckedReason::ValidationError
1749 }
1750 ));
1751 assert!(matches!(
1752 wire_ref(&TypeRef::JsonError(sp()), &types),
1753 WireRef::Unchecked {
1754 reason: UncheckedReason::JsonError
1755 }
1756 ));
1757 assert!(matches!(
1758 wire_ref(&TypeRef::QueueResult(sp()), &types),
1759 WireRef::Unchecked {
1760 reason: UncheckedReason::QueueResult
1761 }
1762 ));
1763 }
1764}