Skip to main content

bynk_emit/emitter/
serialisation.rs

1//! Per-type serialise / deserialise helper generation for workers mode
2//! (v0.8 §3.4 / §5.2).
3//!
4//! Every Bynk type that crosses a context boundary needs:
5//!   - `serialise_<Type>(value): JsonValue` — structural lowering.
6//!   - `deserialise_<Type>(json): Result<<Type>, BoundaryError>` —
7//!     structural validation + refinement re-validation, then a nominal
8//!     cast back to the receiving context's view.
9//!
10//! Helpers live in the *owning* module — commons modules emit helpers for
11//! commons types, context modules emit helpers for the types they declare.
12
13use std::fmt::Write as _;
14
15use bynk_syntax::ast::*;
16
17/// Compute the set of type names (transitively reachable) that need
18/// serialise/deserialise helpers for this context: any type used in the
19/// argument or return position of a service handler exposed by this
20/// context, walked through record fields, sum payloads, and the generic
21/// type parameters of Result/Option/Effect.
22pub fn collect_boundary_types(
23    types: &std::collections::HashMap<String, TypeDecl>,
24    services: &std::collections::HashMap<String, ServiceDecl>,
25    // v0.96 (ADR 0124): rehydration is a trust boundary — an agent's persisted
26    // `store`-field types are validated on load, so they need their deserialisers
27    // emitted. Register every store field's kind-argument types (the element /
28    // key / value types of `Cell`/`Map`/`Set`/`Cache`/`Log`).
29    agents: &std::collections::HashMap<String, AgentDecl>,
30) -> Vec<String> {
31    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
32    let mut out: Vec<String> = Vec::new();
33    let mut stack: Vec<String> = Vec::new();
34
35    let mut svc_names: Vec<&String> = services.keys().collect();
36    svc_names.sort();
37    for name in svc_names {
38        let service = &services[name];
39        for h in &service.handlers {
40            for p in &h.params {
41                collect_type_names(&p.type_ref, &mut stack);
42            }
43            collect_type_names(&h.return_type, &mut stack);
44        }
45    }
46
47    let mut agent_names: Vec<&String> = agents.keys().collect();
48    agent_names.sort();
49    for name in agent_names {
50        for f in &agents[name].store_fields {
51            for arg in &f.kind.args {
52                collect_type_names(arg, &mut stack);
53            }
54        }
55    }
56
57    while let Some(name) = stack.pop() {
58        if !seen.insert(name.clone()) {
59            continue;
60        }
61        out.push(name.clone());
62        let Some(decl) = types.get(&name) else {
63            continue;
64        };
65        match &decl.body {
66            TypeBody::Record(r) => {
67                for f in &r.fields {
68                    collect_type_names(&f.type_ref, &mut stack);
69                }
70            }
71            TypeBody::Sum(s) => {
72                for v in &s.variants {
73                    for p in &v.payload {
74                        collect_type_names(&p.type_ref, &mut stack);
75                    }
76                }
77            }
78            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
79        }
80    }
81
82    out.sort();
83    out
84}
85
86fn collect_type_names(t: &TypeRef, stack: &mut Vec<String>) {
87    match t {
88        TypeRef::Named(id) => stack.push(id.name.clone()),
89        // Query/Stream/Connection types carry no boundary-collectable user
90        // types (non-boundary).
91        TypeRef::Query(..) | TypeRef::Stream(..) | TypeRef::Connection(..) => {}
92        // v0.20a: function types carry no user-named types to collect and are
93        // rejected at boundaries anyway.
94        TypeRef::Fn(..) => {}
95        TypeRef::Result(a, b, _) => {
96            collect_type_names(a, stack);
97            collect_type_names(b, stack);
98        }
99        TypeRef::Option(a, _) => collect_type_names(a, stack),
100        TypeRef::Effect(a, _) => collect_type_names(a, stack),
101        TypeRef::HttpResult(a, _) => collect_type_names(a, stack),
102        // v0.20b: collections serialise element-/entry-wise; their inner
103        // named types need helpers.
104        TypeRef::List(a, _) => collect_type_names(a, stack),
105        TypeRef::Map(k, v, _) => {
106            collect_type_names(k, stack);
107            collect_type_names(v, stack);
108        }
109        TypeRef::Base(_, _)
110        | TypeRef::QueueResult(_)
111        | TypeRef::ValidationError(_)
112        | TypeRef::JsonError(_)
113        | TypeRef::Unit(_) => {}
114    }
115}
116
117/// Emit `serialise_<T>` and `deserialise_<T>` for every named type the
118/// owner declares that crosses a boundary. `owner_qualified` is the
119/// qualified name used as the brand path so that refinement-violation
120/// messages identify the origin context.
121pub fn emit_helpers_for_owner(
122    out: &mut String,
123    type_names: &[String],
124    types: &std::collections::HashMap<String, TypeDecl>,
125    _owner_qualified: &str,
126) {
127    // Only emit helpers for *named* types declared by this owner. Skip
128    // unknown names — they belong to another module or to the runtime's
129    // generic helpers (Result / Option).
130    let mut emitted_any = false;
131    for name in type_names {
132        let Some(decl) = types.get(name) else {
133            continue;
134        };
135        emitted_any = true;
136        emit_one(out, name, decl);
137    }
138    if emitted_any {
139        writeln!(out).unwrap();
140    }
141}
142
143fn emit_one(out: &mut String, name: &str, decl: &TypeDecl) {
144    match &decl.body {
145        TypeBody::Refined { base, .. } => emit_refined(out, name, *base, decl),
146        TypeBody::Opaque { base, .. } => emit_refined(out, name, *base, decl),
147        TypeBody::Record(r) => emit_record(out, name, r),
148        TypeBody::Sum(s) => emit_sum(out, name, s),
149    }
150}
151
152fn ts_base_for_serialisation(b: BaseType) -> &'static str {
153    match b {
154        BaseType::Int => "number",
155        BaseType::String => "string",
156        BaseType::Bool => "boolean",
157        BaseType::Float => "number",
158        BaseType::Duration | BaseType::Instant => "number",
159        // v0.110 (ADR 0142 D5): a `Bytes` wires as a base64 JSON string.
160        BaseType::Bytes => "string",
161    }
162}
163
164/// v0.110 (ADR 0142 D5): the codec for a named opaque/refined type over
165/// `Bytes` (`type Digest = Bytes`). Unlike the `number`-erased base types, a
166/// `Bytes` does not round-trip as itself — it is base64-encoded on serialise
167/// and decoded (rejecting a non-string or invalid-base64 wire value) on
168/// deserialise. There are no `Bytes` refinement predicates, so there is no
169/// `.of` re-validation to thread.
170fn emit_bytes_named_codec(out: &mut String, name: &str) {
171    writeln!(
172        out,
173        "export function serialise_{name}(value: {name}): JsonValue {{"
174    )
175    .unwrap();
176    writeln!(
177        out,
178        "  return __bynkBytesToBase64(value as unknown as Uint8Array);"
179    )
180    .unwrap();
181    writeln!(out, "}}").unwrap();
182    writeln!(out).unwrap();
183
184    writeln!(
185        out,
186        "export function deserialise_{name}(json: JsonValue, path: string = \"$\"): Result<{name}, BoundaryError> {{"
187    )
188    .unwrap();
189    writeln!(out, "  if (typeof json !== \"string\") {{").unwrap();
190    writeln!(
191        out,
192        "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"base64 string\", actual: typeof json }});"
193    )
194    .unwrap();
195    writeln!(out, "  }}").unwrap();
196    writeln!(out, "  const __b = __bynkBytesFromBase64(json);").unwrap();
197    writeln!(out, "  if (__b.tag === \"None\") {{").unwrap();
198    writeln!(
199        out,
200        "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"base64 string\", actual: \"invalid base64\" }});"
201    )
202    .unwrap();
203    writeln!(out, "  }}").unwrap();
204    writeln!(out, "  return Ok(__b.value as unknown as {name});").unwrap();
205    writeln!(out, "}}").unwrap();
206    writeln!(out).unwrap();
207}
208
209fn emit_refined(out: &mut String, name: &str, base: BaseType, _decl: &TypeDecl) {
210    // v0.110: a `Bytes`-based opaque/refined type has a bespoke base64 codec.
211    if base == BaseType::Bytes {
212        emit_bytes_named_codec(out, name);
213        return;
214    }
215    let prim = ts_base_for_serialisation(base);
216    let typeof_str = match base {
217        BaseType::Int => "number",
218        BaseType::String => "string",
219        BaseType::Bool => "boolean",
220        BaseType::Float => "number",
221        BaseType::Duration | BaseType::Instant => "number",
222        // Unreachable: the `Bytes` branch returns above.
223        BaseType::Bytes => "string",
224    };
225    writeln!(
226        out,
227        "export function serialise_{name}(value: {name}): JsonValue {{"
228    )
229    .unwrap();
230    writeln!(out, "  return value as unknown as {prim};").unwrap();
231    writeln!(out, "}}").unwrap();
232    writeln!(out).unwrap();
233
234    writeln!(
235        out,
236        "export function deserialise_{name}(json: JsonValue, path: string = \"$\"): Result<{name}, BoundaryError> {{"
237    )
238    .unwrap();
239    writeln!(out, "  if (typeof json !== \"{typeof_str}\") {{").unwrap();
240    writeln!(
241        out,
242        "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"{typeof_str}\", actual: typeof json }});"
243    )
244    .unwrap();
245    writeln!(out, "  }}").unwrap();
246    // Re-validate via the type's own constructor (`.of`), which applies
247    // the refinement. If the type has no refinement, `.of` doesn't exist
248    // for refined-base types; fall back to a direct cast.
249    writeln!(
250        out,
251        "  const validated = (typeof ({name} as any).of === \"function\")"
252    )
253    .unwrap();
254    writeln!(out, "    ? ({name} as any).of(json)").unwrap();
255    writeln!(out, "    : Ok(json as unknown as {name});").unwrap();
256    writeln!(out, "  if (validated.tag === \"Err\") {{").unwrap();
257    writeln!(
258        out,
259        "    return Err({{ kind: \"RefinementViolation\", path, violation: validated.error }});"
260    )
261    .unwrap();
262    writeln!(out, "  }}").unwrap();
263    writeln!(out, "  return Ok(validated.value as {name});").unwrap();
264    writeln!(out, "}}").unwrap();
265    writeln!(out).unwrap();
266}
267
268fn emit_record(out: &mut String, name: &str, body: &RecordBody) {
269    // serialise
270    writeln!(
271        out,
272        "export function serialise_{name}(value: {name}): JsonValue {{"
273    )
274    .unwrap();
275    writeln!(out, "  return {{").unwrap();
276    for f in &body.fields {
277        let fname = &f.name.name;
278        let expr = serialise_field_expr(&f.type_ref, &format!("value.{fname}"));
279        writeln!(out, "    {fname}: {expr},").unwrap();
280    }
281    writeln!(out, "  }};").unwrap();
282    writeln!(out, "}}").unwrap();
283    writeln!(out).unwrap();
284
285    // deserialise
286    writeln!(
287        out,
288        "export function deserialise_{name}(json: JsonValue, path: string = \"$\"): Result<{name}, BoundaryError> {{"
289    )
290    .unwrap();
291    writeln!(
292        out,
293        "  if (typeof json !== \"object\" || json === null || Array.isArray(json)) {{"
294    )
295    .unwrap();
296    writeln!(
297        out,
298        "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"object\", actual: typeof json }});"
299    )
300    .unwrap();
301    writeln!(out, "  }}").unwrap();
302    writeln!(out, "  const obj = json as {{ [k: string]: JsonValue }};").unwrap();
303    for f in &body.fields {
304        let fname = &f.name.name;
305        let access = format!("obj[\"{fname}\"]");
306        let sub_path = format!("`${{path}}.{fname}`");
307        emit_field_deserialise(out, fname, &f.type_ref, &access, &sub_path);
308    }
309    write!(out, "  return Ok({{ ").unwrap();
310    let parts: Vec<String> = body
311        .fields
312        .iter()
313        .map(|f| format!("{}: __{}", f.name.name, f.name.name))
314        .collect();
315    write!(out, "{}", parts.join(", ")).unwrap();
316    writeln!(out, " }} as {name});").unwrap();
317    writeln!(out, "}}").unwrap();
318    writeln!(out).unwrap();
319}
320
321fn emit_sum(out: &mut String, name: &str, body: &SumBody) {
322    writeln!(
323        out,
324        "export function serialise_{name}(value: {name}): JsonValue {{"
325    )
326    .unwrap();
327    writeln!(out, "  switch (value.tag) {{").unwrap();
328    for v in &body.variants {
329        let vname = &v.name.name;
330        if v.payload.is_empty() {
331            writeln!(out, "    case \"{vname}\":").unwrap();
332            writeln!(out, "      return {{ kind: \"{vname}\" }};").unwrap();
333        } else {
334            writeln!(out, "    case \"{vname}\": {{").unwrap();
335            write!(out, "      return {{ kind: \"{vname}\"").unwrap();
336            for f in &v.payload {
337                let fname = &f.name.name;
338                let expr = serialise_field_expr(&f.type_ref, &format!("(value as any).{fname}"));
339                write!(out, ", {fname}: {expr}").unwrap();
340            }
341            writeln!(out, " }};").unwrap();
342            writeln!(out, "    }}").unwrap();
343        }
344    }
345    writeln!(out, "  }}").unwrap();
346    writeln!(out, "}}").unwrap();
347    writeln!(out).unwrap();
348
349    writeln!(
350        out,
351        "export function deserialise_{name}(json: JsonValue, path: string = \"$\"): Result<{name}, BoundaryError> {{"
352    )
353    .unwrap();
354    writeln!(
355        out,
356        "  if (typeof json !== \"object\" || json === null || Array.isArray(json)) {{"
357    )
358    .unwrap();
359    writeln!(
360        out,
361        "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"object\", actual: typeof json }});"
362    )
363    .unwrap();
364    writeln!(out, "  }}").unwrap();
365    writeln!(out, "  const obj = json as {{ [k: string]: JsonValue }};").unwrap();
366    writeln!(out, "  const kind = obj[\"kind\"];").unwrap();
367    writeln!(out, "  switch (kind) {{").unwrap();
368    for v in &body.variants {
369        let vname = &v.name.name;
370        if v.payload.is_empty() {
371            writeln!(out, "    case \"{vname}\":").unwrap();
372            writeln!(out, "      return Ok({{ tag: \"{vname}\" }} as {name});").unwrap();
373        } else {
374            writeln!(out, "    case \"{vname}\": {{").unwrap();
375            for f in &v.payload {
376                let fname = &f.name.name;
377                let access = format!("obj[\"{fname}\"]");
378                let sub_path = format!("`${{path}}.{fname}`");
379                emit_field_deserialise(out, fname, &f.type_ref, &access, &sub_path);
380            }
381            write!(out, "      return Ok({{ tag: \"{vname}\"").unwrap();
382            for f in &v.payload {
383                let fname = &f.name.name;
384                write!(out, ", {fname}: __{fname}").unwrap();
385            }
386            writeln!(out, " }} as {name});").unwrap();
387            writeln!(out, "    }}").unwrap();
388        }
389    }
390    writeln!(out, "    default:").unwrap();
391    writeln!(
392        out,
393        "      return Err({{ kind: \"StructuralMismatch\", path, expected: \"sum variant kind\", actual: String(kind) }});"
394    )
395    .unwrap();
396    writeln!(out, "  }}").unwrap();
397    writeln!(out, "}}").unwrap();
398    writeln!(out).unwrap();
399}
400
401/// Emit a let binding `__<field>` after destructuring & validating a
402/// nested field.
403fn emit_field_deserialise(out: &mut String, name: &str, t: &TypeRef, json: &str, path_expr: &str) {
404    match t {
405        // v0.20a: function types are confined to non-boundary positions
406        // (`bynk.types.function_at_boundary`), so the serialisation machinery
407        // can never legally see one.
408        TypeRef::Fn(..) | TypeRef::Query(..) | TypeRef::Stream(..) | TypeRef::Connection(..) => {
409            unreachable!("function/query/stream types are rejected at boundaries")
410        }
411        // v0.110 (ADR 0142 D5): a bare `Bytes` field is a base64 JSON string —
412        // require a string, then decode (rejecting invalid base64), binding the
413        // decoded `Uint8Array`. This is the one base type whose wire value is
414        // not a direct cast of its erased representation.
415        TypeRef::Base(BaseType::Bytes, _) => {
416            writeln!(out, "  if (typeof {json} !== \"string\") {{").unwrap();
417            writeln!(
418                out,
419                "    return Err({{ kind: \"StructuralMismatch\", path: {path_expr}, expected: \"base64 string\", actual: typeof {json} }});"
420            )
421            .unwrap();
422            writeln!(out, "  }}").unwrap();
423            writeln!(out, "  const __b_{name} = __bynkBytesFromBase64({json});").unwrap();
424            writeln!(out, "  if (__b_{name}.tag === \"None\") {{").unwrap();
425            writeln!(
426                out,
427                "    return Err({{ kind: \"StructuralMismatch\", path: {path_expr}, expected: \"base64 string\", actual: \"invalid base64\" }});"
428            )
429            .unwrap();
430            writeln!(out, "  }}").unwrap();
431            writeln!(out, "  const __{name} = __b_{name}.value;").unwrap();
432        }
433        TypeRef::Base(b, _) => {
434            let typeof_str = match b {
435                BaseType::Int => "number",
436                BaseType::String => "string",
437                BaseType::Bool => "boolean",
438                BaseType::Float => "number",
439                BaseType::Duration | BaseType::Instant => "number",
440                // Unreachable: handled by the dedicated `Bytes` arm above.
441                BaseType::Bytes => "string",
442            };
443            writeln!(out, "  if (typeof {json} !== \"{typeof_str}\") {{").unwrap();
444            writeln!(
445                out,
446                "    return Err({{ kind: \"StructuralMismatch\", path: {path_expr}, expected: \"{typeof_str}\", actual: typeof {json} }});"
447            )
448            .unwrap();
449            writeln!(out, "  }}").unwrap();
450            // v0.22b: bare `Int` fields validate integrality (ADR 0049) —
451            // with `Float` in the language there is no excuse for a
452            // fractional `Int` from the wire. v0.90 (ADR 0114 D7): an `Instant`
453            // is whole epoch milliseconds, so it validates integrality too.
454            if *b == BaseType::Int || *b == BaseType::Instant {
455                writeln!(out, "  if (!Number.isInteger({json})) {{").unwrap();
456                writeln!(
457                    out,
458                    "    return Err({{ kind: \"StructuralMismatch\", path: {path_expr}, expected: \"integer\", actual: String({json}) }});"
459                )
460                .unwrap();
461                writeln!(out, "  }}").unwrap();
462            }
463            // v0.21: boundary `Float` values are finite (ADR 0040) —
464            // `JSON.parse("1e999")` yields `Infinity`, which must not be
465            // admitted from the wire.
466            if *b == BaseType::Float {
467                writeln!(out, "  if (!Number.isFinite({json})) {{").unwrap();
468                writeln!(
469                    out,
470                    "    return Err({{ kind: \"StructuralMismatch\", path: {path_expr}, expected: \"finite number\", actual: String({json}) }});"
471                )
472                .unwrap();
473                writeln!(out, "  }}").unwrap();
474            }
475            writeln!(out, "  const __{name} = {json};").unwrap();
476        }
477        TypeRef::Named(id) => {
478            // Defer to the type's own deserialiser. Assumes it exists in
479            // scope (imported or declared locally).
480            writeln!(
481                out,
482                "  const __r_{name} = deserialise_{}({json}, {path_expr});",
483                id.name
484            )
485            .unwrap();
486            writeln!(out, "  if (__r_{name}.tag === \"Err\") return __r_{name};").unwrap();
487            writeln!(out, "  const __{name} = __r_{name}.value;").unwrap();
488        }
489        TypeRef::Result(a, b, _) => {
490            let ts_a = inner_ts_name(a);
491            let ts_b = inner_ts_name(b);
492            writeln!(
493                out,
494                "  const __r_{name} = deserialise_Result_{ts_a}_{ts_b}({json}, {path_expr});",
495            )
496            .unwrap();
497            writeln!(out, "  if (__r_{name}.tag === \"Err\") return __r_{name};").unwrap();
498            writeln!(out, "  const __{name} = __r_{name}.value;").unwrap();
499        }
500        TypeRef::Option(a, _) => {
501            let ts_a = inner_ts_name(a);
502            writeln!(
503                out,
504                "  const __r_{name} = deserialise_Option_{ts_a}({json}, {path_expr});",
505            )
506            .unwrap();
507            writeln!(out, "  if (__r_{name}.tag === \"Err\") return __r_{name};").unwrap();
508            writeln!(out, "  const __{name} = __r_{name}.value;").unwrap();
509        }
510        // v0.20b: collections delegate to their specialised helpers, exactly
511        // like Result/Option instantiations.
512        TypeRef::List(a, _) => {
513            let ts_a = inner_ts_name(a);
514            writeln!(
515                out,
516                "  const __r_{name} = deserialise_List_{ts_a}({json}, {path_expr});",
517            )
518            .unwrap();
519            writeln!(out, "  if (__r_{name}.tag === \"Err\") return __r_{name};").unwrap();
520            writeln!(out, "  const __{name} = __r_{name}.value;").unwrap();
521        }
522        TypeRef::Map(k, v, _) => {
523            let ts_k = inner_ts_name(k);
524            let ts_v = inner_ts_name(v);
525            writeln!(
526                out,
527                "  const __r_{name} = deserialise_Map_{ts_k}_{ts_v}({json}, {path_expr});",
528            )
529            .unwrap();
530            writeln!(out, "  if (__r_{name}.tag === \"Err\") return __r_{name};").unwrap();
531            writeln!(out, "  const __{name} = __r_{name}.value;").unwrap();
532        }
533        TypeRef::Effect(_, _)
534        | TypeRef::ValidationError(_)
535        | TypeRef::JsonError(_)
536        | TypeRef::HttpResult(_, _)
537        | TypeRef::QueueResult(_) => {
538            writeln!(out, "  const __{name} = {json} as any;").unwrap();
539        }
540        TypeRef::Unit(_) => {
541            writeln!(out, "  const __{name} = undefined;").unwrap();
542        }
543    }
544}
545
546fn serialise_field_expr(t: &TypeRef, value: &str) -> String {
547    match t {
548        // v0.20a: function types are confined to non-boundary positions
549        // (`bynk.types.function_at_boundary`), so the serialisation machinery
550        // can never legally see one.
551        TypeRef::Fn(..) | TypeRef::Query(..) | TypeRef::Stream(..) | TypeRef::Connection(..) => {
552            unreachable!("function/query/stream types are rejected at boundaries")
553        }
554        // v0.21: serialising a non-finite `Float` is a contract violation
555        // (`JSON.stringify(NaN)` would silently produce `null`); the guard is
556        // a self-contained IIFE so the module needs no extra runtime import.
557        TypeRef::Base(BaseType::Float, _) => format!(
558            "((v: number) => {{ if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; }})({value})"
559        ),
560        // v0.110 (ADR 0142 D5): a `Bytes` is base64-encoded on the wire — the
561        // one base type whose serialise is an encode, not a bare cast.
562        TypeRef::Base(BaseType::Bytes, _) => {
563            format!("__bynkBytesToBase64({value}) as JsonValue")
564        }
565        TypeRef::Base(_, _) => format!("{value} as JsonValue"),
566        TypeRef::Named(id) => format!("serialise_{}({value})", id.name),
567        TypeRef::Result(a, b, _) => format!(
568            "serialise_Result_{}_{}({value})",
569            inner_ts_name(a),
570            inner_ts_name(b)
571        ),
572        TypeRef::Option(a, _) => format!("serialise_Option_{}({value})", inner_ts_name(a)),
573        TypeRef::List(a, _) => format!("serialise_List_{}({value})", inner_ts_name(a)),
574        TypeRef::Map(k, v, _) => format!(
575            "serialise_Map_{}_{}({value})",
576            inner_ts_name(k),
577            inner_ts_name(v)
578        ),
579        TypeRef::Effect(_, _)
580        | TypeRef::ValidationError(_)
581        | TypeRef::JsonError(_)
582        | TypeRef::HttpResult(_, _)
583        | TypeRef::QueueResult(_) => {
584            format!("{value} as JsonValue")
585        }
586        TypeRef::Unit(_) => "null".to_string(),
587    }
588}
589
590fn inner_ts_name(t: &TypeRef) -> String {
591    match t {
592        TypeRef::Base(b, _) => b.name().to_string(),
593        // v0.20a: function types are confined to non-boundary positions
594        // (`bynk.types.function_at_boundary`), so the serialisation machinery
595        // can never legally see one.
596        TypeRef::Fn(..) | TypeRef::Query(..) | TypeRef::Stream(..) | TypeRef::Connection(..) => {
597            unreachable!("function/query/stream types are rejected at boundaries")
598        }
599        TypeRef::Named(id) => id.name.clone(),
600        TypeRef::Result(a, b, _) => format!("Result_{}_{}", inner_ts_name(a), inner_ts_name(b)),
601        TypeRef::Option(a, _) => format!("Option_{}", inner_ts_name(a)),
602        TypeRef::Effect(a, _) => format!("Effect_{}", inner_ts_name(a)),
603        TypeRef::HttpResult(a, _) => format!("HttpResult_{}", inner_ts_name(a)),
604        TypeRef::List(a, _) => format!("List_{}", inner_ts_name(a)),
605        TypeRef::Map(k, v, _) => format!("Map_{}_{}", inner_ts_name(k), inner_ts_name(v)),
606        TypeRef::QueueResult(_) => "QueueResult".to_string(),
607        TypeRef::ValidationError(_) => "ValidationError".to_string(),
608        TypeRef::JsonError(_) => "JsonError".to_string(),
609        TypeRef::Unit(_) => "Unit".to_string(),
610    }
611}
612
613/// v0.22b: the codec closure for a set of `Json.encode`/`Json.decode[T]`
614/// target type-refs — the named types needing per-type helpers (transitively
615/// through record fields and sum payloads) plus the generic instantiations
616/// needing specialised helpers. The same closure logic as the boundary
617/// collectors, rooted at expressions instead of service signatures.
618pub fn collect_codec_closure(
619    roots: &[TypeRef],
620    types: &std::collections::HashMap<String, TypeDecl>,
621) -> (Vec<String>, Vec<GenericInst>) {
622    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
623    let mut names: Vec<String> = Vec::new();
624    let mut stack: Vec<String> = Vec::new();
625    for r in roots {
626        collect_type_names(r, &mut stack);
627    }
628    while let Some(name) = stack.pop() {
629        if !seen.insert(name.clone()) {
630            continue;
631        }
632        names.push(name.clone());
633        let Some(decl) = types.get(&name) else {
634            continue;
635        };
636        match &decl.body {
637            TypeBody::Record(r) => {
638                for f in &r.fields {
639                    collect_type_names(&f.type_ref, &mut stack);
640                }
641            }
642            TypeBody::Sum(s) => {
643                for v in &s.variants {
644                    for p in &v.payload {
645                        collect_type_names(&p.type_ref, &mut stack);
646                    }
647                }
648            }
649            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
650        }
651    }
652    names.sort();
653
654    let mut insts: Vec<GenericInst> = Vec::new();
655    let mut inst_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
656    for r in roots {
657        walk_generic_inst(r, &mut insts, &mut inst_seen);
658    }
659    for name in &names {
660        let Some(decl) = types.get(name) else {
661            continue;
662        };
663        match &decl.body {
664            TypeBody::Record(r) => {
665                for f in &r.fields {
666                    walk_generic_inst(&f.type_ref, &mut insts, &mut inst_seen);
667                }
668            }
669            TypeBody::Sum(s) => {
670                for v in &s.variants {
671                    for p in &v.payload {
672                        walk_generic_inst(&p.type_ref, &mut insts, &mut inst_seen);
673                    }
674                }
675            }
676            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
677        }
678    }
679    (names, insts)
680}
681
682/// v0.22b: an expression-form serialise for a codec target — the same
683/// dispatch as a record field's serialisation.
684pub fn serialise_expr(t: &TypeRef, value: &str) -> String {
685    serialise_field_expr(t, value)
686}
687
688/// v0.22b: an expression-form deserialise call for a codec target. Named
689/// types and generic instantiations go through their (module-local)
690/// helpers; bases inline the structural check.
691pub fn deserialise_expr(t: &TypeRef, json: &str, path: &str) -> String {
692    match t {
693        TypeRef::Named(id) => format!("deserialise_{}({json}, \"{path}\")", id.name),
694        TypeRef::Result(..) | TypeRef::Option(..) | TypeRef::List(..) | TypeRef::Map(..) => {
695            format!("deserialise_{}({json}, \"{path}\")", inner_ts_name(t))
696        }
697        // v0.110 (ADR 0142 D5): a `Bytes` wires as a base64 string; decode it
698        // (rejecting a non-string or invalid base64) to a `Uint8Array`.
699        TypeRef::Base(BaseType::Bytes, _) => {
700            format!(
701                "((__v) => typeof __v === \"string\" ? ((__b) => __b.tag === \"Some\" ? Ok(__b.value) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"base64 string\", actual: \"invalid base64\" }} as BoundaryError))(__bynkBytesFromBase64(__v)) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"base64 string\", actual: typeof __v }} as BoundaryError))({json})"
702            )
703        }
704        TypeRef::Base(b, _) => {
705            let typeof_str = match b {
706                BaseType::Int => "number",
707                BaseType::String => "string",
708                BaseType::Bool => "boolean",
709                BaseType::Float => "number",
710                BaseType::Duration | BaseType::Instant => "number",
711                // Unreachable: handled by the dedicated `Bytes` arm above.
712                BaseType::Bytes => "string",
713            };
714            let extra = match b {
715                BaseType::Float => " && Number.isFinite(__v)",
716                // v0.86 (ADR 0112 D6): a `Duration` is whole milliseconds —
717                // reject a non-integer from the wire, as a refined `Int` does.
718                BaseType::Int | BaseType::Duration | BaseType::Instant => {
719                    " && Number.isInteger(__v)"
720                }
721                _ => "",
722            };
723            format!(
724                "((__v) => typeof __v === \"{typeof_str}\"{extra} ? Ok(__v) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"{typeof_str}\", actual: typeof __v }} as BoundaryError))({json})"
725            )
726        }
727        // Everything else is rejected by the checker's codec-domain rule.
728        _ => unreachable!("non-codable type reached the Json codec lowering"),
729    }
730}
731
732/// Collect the set of `Result<A, B>` / `Option<A>` instantiations used in
733/// boundary positions so the emitter can synthesise the specialised
734/// helpers. v0.18: an instantiation may also appear in the *fields* of a
735/// boundary record or sum payload (e.g. the bynk surface's
736/// `Request.contentType: Option[String]`) — the per-type serialisers
737/// delegate to the specialised generic helpers, so walk those too.
738pub fn collect_generic_instantiations(
739    services: &std::collections::HashMap<String, ServiceDecl>,
740    // v0.96 (ADR 0124): an agent's `store`-field element types are validated on
741    // rehydration, so a `Cell[Option[Int]]` / `Log[List[T]]` needs its specialised
742    // generic helper emitted just like a boundary signature does.
743    agents: &std::collections::HashMap<String, AgentDecl>,
744    boundary_type_names: &[String],
745    types: &std::collections::HashMap<String, TypeDecl>,
746) -> Vec<GenericInst> {
747    let mut out: Vec<GenericInst> = Vec::new();
748    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
749    // Iterate services in name order: `HashMap::values()` order varies per
750    // process, and the *emission order* of the specialised helpers follows
751    // first-encounter order here. Surfaced by the first fixture with
752    // multiple same-file services carrying different instantiations (v0.23
753    // #35 CI); latent since v0.8.
754    let mut svc_names: Vec<&String> = services.keys().collect();
755    svc_names.sort();
756    for name in svc_names {
757        let s = &services[name];
758        for h in &s.handlers {
759            for p in &h.params {
760                walk_generic_inst(&p.type_ref, &mut out, &mut seen);
761            }
762            walk_generic_inst(&h.return_type, &mut out, &mut seen);
763        }
764    }
765    let mut agent_names: Vec<&String> = agents.keys().collect();
766    agent_names.sort();
767    for name in agent_names {
768        for f in &agents[name].store_fields {
769            for arg in &f.kind.args {
770                walk_generic_inst(arg, &mut out, &mut seen);
771            }
772        }
773    }
774    for name in boundary_type_names {
775        let Some(decl) = types.get(name) else {
776            continue;
777        };
778        match &decl.body {
779            TypeBody::Record(r) => {
780                for f in &r.fields {
781                    walk_generic_inst(&f.type_ref, &mut out, &mut seen);
782                }
783            }
784            TypeBody::Sum(s) => {
785                for v in &s.variants {
786                    for p in &v.payload {
787                        walk_generic_inst(&p.type_ref, &mut out, &mut seen);
788                    }
789                }
790            }
791            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
792        }
793    }
794    out
795}
796
797#[derive(Debug, Clone)]
798pub enum GenericInst {
799    ResultInst {
800        ok: TypeRef,
801        err: TypeRef,
802    },
803    OptionInst {
804        inner: TypeRef,
805    },
806    /// v0.20b: a `List[T]` boundary instantiation — element-wise wire format.
807    ListInst {
808        elem: TypeRef,
809    },
810    /// v0.20b: a `Map[K, V]` boundary instantiation — entries-array wire
811    /// format (`[[k, v], …]`), insertion-ordered.
812    MapInst {
813        key: TypeRef,
814        val: TypeRef,
815    },
816}
817
818impl GenericInst {
819    pub fn ts_name(&self) -> String {
820        match self {
821            GenericInst::ResultInst { ok, err } => {
822                format!("Result_{}_{}", inner_ts_name(ok), inner_ts_name(err))
823            }
824            GenericInst::OptionInst { inner } => {
825                format!("Option_{}", inner_ts_name(inner))
826            }
827            GenericInst::ListInst { elem } => format!("List_{}", inner_ts_name(elem)),
828            GenericInst::MapInst { key, val } => {
829                format!("Map_{}_{}", inner_ts_name(key), inner_ts_name(val))
830            }
831        }
832    }
833}
834
835fn walk_generic_inst(
836    t: &TypeRef,
837    out: &mut Vec<GenericInst>,
838    seen: &mut std::collections::HashSet<String>,
839) {
840    match t {
841        TypeRef::Result(a, b, _) => {
842            let inst = GenericInst::ResultInst {
843                ok: (**a).clone(),
844                err: (**b).clone(),
845            };
846            let key = inst.ts_name();
847            if seen.insert(key) {
848                out.push(inst);
849            }
850            walk_generic_inst(a, out, seen);
851            walk_generic_inst(b, out, seen);
852        }
853        TypeRef::Option(a, _) => {
854            let inst = GenericInst::OptionInst {
855                inner: (**a).clone(),
856            };
857            let key = inst.ts_name();
858            if seen.insert(key) {
859                out.push(inst);
860            }
861            walk_generic_inst(a, out, seen);
862        }
863        TypeRef::Effect(a, _) => walk_generic_inst(a, out, seen),
864        TypeRef::HttpResult(a, _) => walk_generic_inst(a, out, seen),
865        TypeRef::List(a, _) => {
866            let inst = GenericInst::ListInst {
867                elem: (**a).clone(),
868            };
869            let key = inst.ts_name();
870            if seen.insert(key) {
871                out.push(inst);
872            }
873            walk_generic_inst(a, out, seen);
874        }
875        TypeRef::Map(k, v, _) => {
876            let inst = GenericInst::MapInst {
877                key: (**k).clone(),
878                val: (**v).clone(),
879            };
880            let key = inst.ts_name();
881            if seen.insert(key) {
882                out.push(inst);
883            }
884            walk_generic_inst(k, out, seen);
885            walk_generic_inst(v, out, seen);
886        }
887        _ => {}
888    }
889}
890
891/// Emit specialised helpers for each `Result<A, B>` / `Option<A>`
892/// instantiation. They delegate to the named-type serialisers for A and B.
893pub fn emit_generic_helpers(out: &mut String, insts: &[GenericInst]) {
894    for inst in insts {
895        match inst {
896            GenericInst::ResultInst { ok, err } => {
897                let ok_ts = inner_ts_name(ok);
898                let err_ts = inner_ts_name(err);
899                let ok_inner = ts_inner_type(ok);
900                let err_inner = ts_inner_type(err);
901                let serialise_ok = serialise_field_expr(ok, "value.value");
902                let serialise_err = serialise_field_expr(err, "value.error");
903                writeln!(
904                    out,
905                    "export function serialise_Result_{ok_ts}_{err_ts}(value: Result<{ok_inner}, {err_inner}>): JsonValue {{"
906                )
907                .unwrap();
908                writeln!(
909                    out,
910                    "  if (value.tag === \"Ok\") return {{ kind: \"Ok\", value: {serialise_ok} }};"
911                )
912                .unwrap();
913                writeln!(out, "  return {{ kind: \"Err\", error: {serialise_err} }};").unwrap();
914                writeln!(out, "}}").unwrap();
915                writeln!(out).unwrap();
916
917                writeln!(
918                    out,
919                    "export function deserialise_Result_{ok_ts}_{err_ts}(json: JsonValue, path: string = \"$\"): Result<Result<{ok_inner}, {err_inner}>, BoundaryError> {{"
920                )
921                .unwrap();
922                writeln!(
923                    out,
924                    "  if (typeof json !== \"object\" || json === null || Array.isArray(json)) {{"
925                )
926                .unwrap();
927                writeln!(
928                    out,
929                    "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"object\", actual: typeof json }});"
930                )
931                .unwrap();
932                writeln!(out, "  }}").unwrap();
933                writeln!(out, "  const obj = json as {{ [k: string]: JsonValue }};").unwrap();
934                writeln!(out, "  if (obj[\"kind\"] === \"Ok\") {{").unwrap();
935                emit_field_deserialise(out, "v", ok, "obj[\"value\"]", "`${path}.value`");
936                writeln!(
937                    out,
938                    "    return Ok(Ok(__v) as Result<{ok_inner}, {err_inner}>);"
939                )
940                .unwrap();
941                writeln!(out, "  }} else if (obj[\"kind\"] === \"Err\") {{").unwrap();
942                emit_field_deserialise(out, "e", err, "obj[\"error\"]", "`${path}.error`");
943                writeln!(
944                    out,
945                    "    return Ok(Err(__e) as Result<{ok_inner}, {err_inner}>);"
946                )
947                .unwrap();
948                writeln!(out, "  }}").unwrap();
949                writeln!(out, "  return Err({{ kind: \"StructuralMismatch\", path, expected: \"Ok | Err\", actual: String(obj[\"kind\"]) }});").unwrap();
950                writeln!(out, "}}").unwrap();
951                writeln!(out).unwrap();
952            }
953            GenericInst::OptionInst { inner } => {
954                let inner_ts = inner_ts_name(inner);
955                let inner_ty = ts_inner_type(inner);
956                let serialise_inner = serialise_field_expr(inner, "value.value");
957                writeln!(
958                    out,
959                    "export function serialise_Option_{inner_ts}(value: Option<{inner_ty}>): JsonValue {{"
960                )
961                .unwrap();
962                writeln!(out, "  if (value.tag === \"Some\") return {{ kind: \"Some\", value: {serialise_inner} }};").unwrap();
963                writeln!(out, "  return {{ kind: \"None\" }};").unwrap();
964                writeln!(out, "}}").unwrap();
965                writeln!(out).unwrap();
966
967                writeln!(
968                    out,
969                    "export function deserialise_Option_{inner_ts}(json: JsonValue, path: string = \"$\"): Result<Option<{inner_ty}>, BoundaryError> {{"
970                )
971                .unwrap();
972                writeln!(
973                    out,
974                    "  if (typeof json !== \"object\" || json === null || Array.isArray(json)) {{"
975                )
976                .unwrap();
977                writeln!(
978                    out,
979                    "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"object\", actual: typeof json }});"
980                )
981                .unwrap();
982                writeln!(out, "  }}").unwrap();
983                writeln!(out, "  const obj = json as {{ [k: string]: JsonValue }};").unwrap();
984                writeln!(out, "  if (obj[\"kind\"] === \"Some\") {{").unwrap();
985                emit_field_deserialise(out, "v", inner, "obj[\"value\"]", "`${path}.value`");
986                writeln!(out, "    return Ok(Some(__v) as Option<{inner_ty}>);").unwrap();
987                writeln!(out, "  }} else if (obj[\"kind\"] === \"None\") {{").unwrap();
988                writeln!(out, "    return Ok(None as Option<{inner_ty}>);").unwrap();
989                writeln!(out, "  }}").unwrap();
990                writeln!(out, "  return Err({{ kind: \"StructuralMismatch\", path, expected: \"Some | None\", actual: String(obj[\"kind\"]) }});").unwrap();
991                writeln!(out, "}}").unwrap();
992                writeln!(out).unwrap();
993            }
994            // v0.20b: `List[T]` — element-wise wire format (a JSON array).
995            GenericInst::ListInst { elem } => {
996                let elem_ts = inner_ts_name(elem);
997                let elem_ty = ts_inner_type(elem);
998                let serialise_elem = serialise_field_expr(elem, "v");
999                writeln!(
1000                    out,
1001                    "export function serialise_List_{elem_ts}(value: readonly {elem_ty}[]): JsonValue {{"
1002                )
1003                .unwrap();
1004                writeln!(out, "  return value.map((v) => {serialise_elem});").unwrap();
1005                writeln!(out, "}}").unwrap();
1006                writeln!(out).unwrap();
1007
1008                writeln!(
1009                    out,
1010                    "export function deserialise_List_{elem_ts}(json: JsonValue, path: string = \"$\"): Result<readonly {elem_ty}[], BoundaryError> {{"
1011                )
1012                .unwrap();
1013                writeln!(out, "  if (!Array.isArray(json)) {{").unwrap();
1014                writeln!(
1015                    out,
1016                    "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"array\", actual: typeof json }});"
1017                )
1018                .unwrap();
1019                writeln!(out, "  }}").unwrap();
1020                writeln!(out, "  const out: {elem_ty}[] = [];").unwrap();
1021                writeln!(out, "  for (let i = 0; i < json.length; i++) {{").unwrap();
1022                // Bind the element before validating: `json[i]` with a
1023                // mutable index does not narrow under a typeof guard.
1024                writeln!(out, "  const item = json[i];").unwrap();
1025                emit_field_deserialise(out, "el", elem, "item", "`${path}[${i}]`");
1026                writeln!(out, "  out.push(__el);").unwrap();
1027                writeln!(out, "  }}").unwrap();
1028                writeln!(out, "  return Ok(out);").unwrap();
1029                writeln!(out, "}}").unwrap();
1030                writeln!(out).unwrap();
1031            }
1032            // v0.20b: `Map[K, V]` — entries-array wire format `[[k, v], …]`,
1033            // uniform across String/Int keys and insertion-ordered
1034            // (normative, §7).
1035            GenericInst::MapInst { key, val } => {
1036                let key_ts = inner_ts_name(key);
1037                let val_ts = inner_ts_name(val);
1038                let key_ty = ts_inner_type(key);
1039                let val_ty = ts_inner_type(val);
1040                let serialise_key = serialise_field_expr(key, "k");
1041                let serialise_val = serialise_field_expr(val, "v");
1042                writeln!(
1043                    out,
1044                    "export function serialise_Map_{key_ts}_{val_ts}(value: ReadonlyMap<{key_ty}, {val_ty}>): JsonValue {{"
1045                )
1046                .unwrap();
1047                writeln!(out, "  const entries: JsonValue[] = [];").unwrap();
1048                writeln!(out, "  for (const [k, v] of value) {{").unwrap();
1049                writeln!(out, "    entries.push([{serialise_key}, {serialise_val}]);").unwrap();
1050                writeln!(out, "  }}").unwrap();
1051                writeln!(out, "  return entries;").unwrap();
1052                writeln!(out, "}}").unwrap();
1053                writeln!(out).unwrap();
1054
1055                writeln!(
1056                    out,
1057                    "export function deserialise_Map_{key_ts}_{val_ts}(json: JsonValue, path: string = \"$\"): Result<ReadonlyMap<{key_ty}, {val_ty}>, BoundaryError> {{"
1058                )
1059                .unwrap();
1060                writeln!(out, "  if (!Array.isArray(json)) {{").unwrap();
1061                writeln!(
1062                    out,
1063                    "    return Err({{ kind: \"StructuralMismatch\", path, expected: \"array\", actual: typeof json }});"
1064                )
1065                .unwrap();
1066                writeln!(out, "  }}").unwrap();
1067                writeln!(out, "  const out = new Map<{key_ty}, {val_ty}>();").unwrap();
1068                writeln!(out, "  for (let i = 0; i < json.length; i++) {{").unwrap();
1069                writeln!(out, "  const entry = json[i];").unwrap();
1070                writeln!(out, "  if (!Array.isArray(entry) || entry.length !== 2) {{").unwrap();
1071                writeln!(
1072                    out,
1073                    "    return Err({{ kind: \"StructuralMismatch\", path: `${{path}}[${{i}}]`, expected: \"[key, value] entry\", actual: typeof entry }});"
1074                )
1075                .unwrap();
1076                writeln!(out, "  }}").unwrap();
1077                writeln!(out, "  const entryK = entry[0];").unwrap();
1078                writeln!(out, "  const entryV = entry[1];").unwrap();
1079                emit_field_deserialise(out, "k", key, "entryK", "`${path}[${i}][0]`");
1080                emit_field_deserialise(out, "v", val, "entryV", "`${path}[${i}][1]`");
1081                writeln!(out, "  out.set(__k, __v);").unwrap();
1082                writeln!(out, "  }}").unwrap();
1083                writeln!(out, "  return Ok(out);").unwrap();
1084                writeln!(out, "}}").unwrap();
1085                writeln!(out).unwrap();
1086            }
1087        }
1088    }
1089}
1090
1091fn ts_inner_type(t: &TypeRef) -> String {
1092    match t {
1093        // v0.20a: function types are confined to non-boundary positions
1094        // (`bynk.types.function_at_boundary`), so the serialisation machinery
1095        // can never legally see one.
1096        TypeRef::Fn(..) | TypeRef::Query(..) | TypeRef::Stream(..) | TypeRef::Connection(..) => {
1097            unreachable!("function/query/stream types are rejected at boundaries")
1098        }
1099        TypeRef::Base(b, _) => match b {
1100            BaseType::Int => "number".to_string(),
1101            BaseType::String => "string".to_string(),
1102            BaseType::Bool => "boolean".to_string(),
1103            BaseType::Float => "number".to_string(),
1104            BaseType::Duration | BaseType::Instant => "number".to_string(),
1105            // v0.110 (ADR 0142): `Bytes` erases to `Uint8Array`.
1106            BaseType::Bytes => "Uint8Array".to_string(),
1107        },
1108        TypeRef::Named(id) => id.name.clone(),
1109        TypeRef::Result(a, b, _) => format!("Result<{}, {}>", ts_inner_type(a), ts_inner_type(b)),
1110        TypeRef::Option(a, _) => format!("Option<{}>", ts_inner_type(a)),
1111        TypeRef::Effect(a, _) => format!("Promise<{}>", ts_inner_type(a)),
1112        TypeRef::HttpResult(a, _) => format!("HttpResult<{}>", ts_inner_type(a)),
1113        TypeRef::List(a, _) => format!("readonly {}[]", ts_inner_type(a)),
1114        TypeRef::Map(k, v, _) => {
1115            format!("ReadonlyMap<{}, {}>", ts_inner_type(k), ts_inner_type(v))
1116        }
1117        TypeRef::QueueResult(_) => "QueueResult".to_string(),
1118        TypeRef::ValidationError(_) => "ValidationError".to_string(),
1119        TypeRef::JsonError(_) => "JsonError".to_string(),
1120        TypeRef::Unit(_) => "void".to_string(),
1121    }
1122}