Skip to main content

bynk_check/
wire_default.rs

1use std::sync::Arc;
2
3use bynk_syntax::ast::{
4    BaseType, Expr, ExprKind, RecordBody, SumBody, TypeBody, TypeDecl, TypeRef, UnaryOp,
5};
6
7/// Events slice 3a (#972): the **wire-form** JSON literal an event field's
8/// default deserialises from when its key is absent from the incoming JSON.
9/// Type-directed — given the field's expected `TypeRef` and the visible
10/// `types` table, every syntactic ambiguity (a bare `Ident` that is really a
11/// sum variant, a `FieldAccess` that is a qualified nullary variant) resolves
12/// the same way the checker resolves it, just narrower (a bare name must
13/// match a variant of *this* expected sum specifically) — so this needs no
14/// per-expression `Ty` map (`expr_types`), which a subscriber regenerating a
15/// publisher's own event codec (`emit_consumed_context_helpers`) has no way
16/// to obtain anyway (no cross-unit `expr_types` store exists).
17///
18/// Produces the value in its **wire** shape (`kind` discriminant for a sum,
19/// matching `emit_sum_codec`'s generated `Option`/`Result` instantiations
20/// exactly — never the in-memory `tag` discriminant, and never a qualified
21/// reference like `Region.Domestic` into a value namespace the emitting
22/// module may not import), so it can be spliced in as the field's raw JSON
23/// access and re-enter `emit_field_deserialise` (`bynk-emit`) completely
24/// unchanged.
25///
26/// `Err(reason)` for anything not closed-form. The caller
27/// (`bynk-emit/src/project/validate.rs`'s event-field-default check) turns
28/// that into `bynk.event.bad_field_default` at check time, so a value this
29/// function cannot build should never reach `emit_record` in practice — the
30/// `.ok()` fallback there is a non-panicking safety net, not the intended
31/// rejection path.
32pub fn lower_field_default_wire(
33    init: &Expr,
34    expected: &TypeRef,
35    types: &std::collections::HashMap<String, Arc<TypeDecl>>,
36) -> Result<String, String> {
37    if let ExprKind::Paren(inner) = &init.kind {
38        return lower_field_default_wire(inner, expected, types);
39    }
40    match expected {
41        TypeRef::Base(b, _) => lower_base_literal(*b, init),
42        TypeRef::Named(id) => {
43            let Some(decl) = types.get(&id.name) else {
44                return Err(format!("cannot resolve type `{}`", id.name));
45            };
46            match &decl.body {
47                TypeBody::Refined { base, .. } => lower_base_literal(*base, init),
48                // ADR 0182: `.unsafe`'s value is an identity cast (`return
49                // value as T;`, `emit.rs`'s opaque emission) — the literal
50                // itself, verbatim, is the wire form. (Its refinement is
51                // additionally checked *statically* for an event field
52                // default specifically — `check_event_field_default` — since
53                // this literal re-enters the same codec a real wire value
54                // would, unlike an ordinary `.unsafe` bypass.)
55                TypeBody::Opaque { base, .. } => match qualified_call(init) {
56                    Some((type_name, "unsafe", [lit])) if type_name == id.name => {
57                        lower_base_literal(*base, lit)
58                    }
59                    Some((_, "unsafe", _)) => {
60                        Err("`.unsafe` takes exactly one argument".to_string())
61                    }
62                    _ => Err(format!(
63                        "an opaque type's default must be `{}.unsafe(<literal>)`",
64                        id.name
65                    )),
66                },
67                TypeBody::Sum(s) => lower_sum_default(&id.name, s, init, types),
68                TypeBody::Record(r) => lower_record_default(r, init, types),
69            }
70        }
71        TypeRef::Option(inner, _) => match &init.kind {
72            ExprKind::None => Ok("{ kind: \"None\" }".to_string()),
73            ExprKind::Some(e) => {
74                let v = lower_field_default_wire(e, inner, types)?;
75                Ok(format!("{{ kind: \"Some\", value: {v} }}"))
76            }
77            _ => Err("an `Option` field default must be `Some(...)` or `None`".to_string()),
78        },
79        TypeRef::Result(ok, err, _) => match &init.kind {
80            ExprKind::Ok(e) => {
81                let v = lower_field_default_wire(e, ok, types)?;
82                Ok(format!("{{ kind: \"Ok\", value: {v} }}"))
83            }
84            ExprKind::Err(e) => {
85                let v = lower_field_default_wire(e, err, types)?;
86                Ok(format!("{{ kind: \"Err\", error: {v} }}"))
87            }
88            _ => Err("a `Result` field default must be `Ok(...)` or `Err(...)`".to_string()),
89        },
90        TypeRef::List(elem, _) => match &init.kind {
91            ExprKind::ListLit(items) => {
92                let parts = items
93                    .iter()
94                    .map(|e| lower_field_default_wire(e, elem, types))
95                    .collect::<Result<Vec<_>, _>>()?;
96                Ok(format!("[{}]", parts.join(", ")))
97            }
98            _ => Err("a `List` field default must be a list literal".to_string()),
99        },
100        TypeRef::Map(..) => Err("a `Map` field has no closed-form default literal".to_string()),
101        TypeRef::App { .. } => Err(
102            "a generic type's field cannot carry a default (events are never generic)".to_string(),
103        ),
104        TypeRef::Effect(..)
105        | TypeRef::HttpResult(..)
106        | TypeRef::QueueResult(_)
107        | TypeRef::Asset(_)
108        | TypeRef::RequestHeaders(_)
109        | TypeRef::TokenClaims(_)
110        | TypeRef::BoundaryRejection(_)
111        | TypeRef::Query(..)
112        | TypeRef::Stream(..)
113        | TypeRef::Connection(..)
114        | TypeRef::History(..)
115        | TypeRef::ValidationError(_)
116        | TypeRef::JsonError(_)
117        | TypeRef::Unit(_)
118        | TypeRef::Fn(..) => Err("this field type is not wire-representable".to_string()),
119    }
120}
121
122/// The wire form of a base-type literal — the raw literal a real wire value
123/// of this base type would also be, so it re-enters
124/// `emit_field_deserialise`'s (`bynk-emit`) ordinary `typeof`/
125/// `Number.isInteger` checks unchanged. `Bytes` has no literal syntax, so it
126/// is not admitted.
127/// A qualified `TypeName.method(args)` call, in whichever `ExprKind` shape it
128/// actually parses as. Confirmed empirically (`OrderId.unsafe("x")` parses to
129/// `ExprKind::MethodCall { receiver: Ident("OrderId"), method: "unsafe", .. }`
130/// — the parser never distinguishes a type-qualified call from an ordinary
131/// instance method call; that's a resolver-time decision).
132fn qualified_call(e: &Expr) -> Option<(&str, &str, &[Expr])> {
133    match &e.kind {
134        ExprKind::MethodCall {
135            receiver,
136            method,
137            args,
138            ..
139        } => {
140            let ExprKind::Ident(recv) = &receiver.kind else {
141                return None;
142            };
143            Some((recv.name.as_str(), method.name.as_str(), args.as_slice()))
144        }
145        _ => None,
146    }
147}
148
149fn lower_base_literal(base: BaseType, e: &Expr) -> Result<String, String> {
150    // Strip one level of negation so `-5`/`-5.0` reach the literal arms below,
151    // mirroring `const_literal`'s admission — `i64::checked_neg` guards
152    // `i64::MIN`, which has no positive counterpart to negate away from.
153    let (negate, inner) = match &e.kind {
154        ExprKind::UnaryOp(UnaryOp::Neg, inner) => (true, &inner.kind),
155        other => (false, other),
156    };
157    match (base, inner) {
158        (BaseType::Int | BaseType::Instant, ExprKind::IntLit { value, .. }) => {
159            let v = if negate {
160                value
161                    .checked_neg()
162                    .ok_or_else(|| "integer literal has no negation".to_string())?
163            } else {
164                *value
165            };
166            Ok(v.to_string())
167        }
168        (BaseType::Float, ExprKind::IntLit { value, .. }) => {
169            let v = if negate { -*value } else { *value };
170            Ok(v.to_string())
171        }
172        (BaseType::Float, ExprKind::FloatLit { lexeme, .. }) => Ok(if negate {
173            format!("-{lexeme}")
174        } else {
175            lexeme.clone()
176        }),
177        (BaseType::String, ExprKind::StrLit(s)) if !negate => {
178            Ok(format!("\"{}\"", escape_ts_literal(s)))
179        }
180        (BaseType::Bool, ExprKind::BoolLit(b)) if !negate => Ok(b.to_string()),
181        (BaseType::Duration, ExprKind::DurationLit { millis, .. }) if !negate => {
182            Ok(millis.to_string())
183        }
184        (BaseType::Bytes, _) => Err("a `Bytes` field has no literal default form".to_string()),
185        _ => Err(format!("expected a `{}` literal", base.name())),
186    }
187}
188
189/// The wire form of a sum-variant default: `{ kind: "Variant" }` (nullary) or
190/// `{ kind: "Variant", f1: ..., f2: ... }` (payload, positionally recursed
191/// against each field's *declared* type) — never a qualified reference into
192/// the sum's generated value namespace (`Sum.Variant`), which is what the
193/// ordinary handler-body lowering (`lower_expr_into`) would produce and which
194/// a foreign module regenerating this codec cannot import.
195fn lower_sum_default(
196    sum_name: &str,
197    body: &SumBody,
198    init: &Expr,
199    types: &std::collections::HashMap<String, Arc<TypeDecl>>,
200) -> Result<String, String> {
201    let (variant_name, args): (&str, &[Expr]) = match &init.kind {
202        ExprKind::Ident(id) => (id.name.as_str(), &[]),
203        ExprKind::FieldAccess { receiver, field } => {
204            let ExprKind::Ident(recv) = &receiver.kind else {
205                return Err(format!("expected a variant of `{sum_name}`"));
206            };
207            if recv.name != sum_name {
208                return Err(format!(
209                    "expected a variant of `{sum_name}`, not `{}`",
210                    recv.name
211                ));
212            }
213            (field.name.as_str(), &[])
214        }
215        ExprKind::Call { name, args, .. } => (name.name.as_str(), args.as_slice()),
216        _ => match qualified_call(init) {
217            Some((recv, method, args)) if recv == sum_name => (method, args),
218            Some((recv, ..)) => {
219                return Err(format!("expected a variant of `{sum_name}`, not `{recv}`"));
220            }
221            None => return Err(format!("expected a variant of `{sum_name}`")),
222        },
223    };
224    let Some(variant) = body.variants.iter().find(|v| v.name.name == variant_name) else {
225        return Err(format!("`{variant_name}` is not a variant of `{sum_name}`"));
226    };
227    if args.len() != variant.payload.len() {
228        return Err(format!(
229            "`{variant_name}` takes {} payload field(s), got {}",
230            variant.payload.len(),
231            args.len()
232        ));
233    }
234    let mut parts = vec![format!("kind: \"{variant_name}\"")];
235    for (field, arg) in variant.payload.iter().zip(args.iter()) {
236        let v = lower_field_default_wire(arg, &field.type_ref, types)?;
237        parts.push(format!("{}: {v}", field.name.name));
238    }
239    Ok(format!("{{ {} }}", parts.join(", ")))
240}
241
242/// The wire form of a record-literal default: a plain object literal, each
243/// field recursed against its *declared* type. Records are structurally
244/// wire-shaped (never tagged, never a named constructor), so this never
245/// needs qualification either.
246fn lower_record_default(
247    body: &RecordBody,
248    init: &Expr,
249    types: &std::collections::HashMap<String, Arc<TypeDecl>>,
250) -> Result<String, String> {
251    let ExprKind::RecordConstruction { fields, .. } = &init.kind else {
252        return Err("expected a record literal".to_string());
253    };
254    let mut parts = Vec::new();
255    for f in &body.fields {
256        let Some(given) = fields.iter().find(|fi| fi.name.name == f.name.name) else {
257            return Err(format!("record default is missing field `{}`", f.name.name));
258        };
259        let Some(value) = &given.value else {
260            return Err(format!(
261                "record default's field `{}` cannot use shorthand (no bindings are in scope)",
262                f.name.name
263            ));
264        };
265        let v = lower_field_default_wire(value, &f.type_ref, types)?;
266        parts.push(format!("{}: {v}", f.name.name));
267    }
268    Ok(format!("{{ {} }}", parts.join(", ")))
269}
270
271/// Escapes a string for embedding in a TypeScript double-quoted string
272/// literal. `pub` because `bynk-emit`'s `escape_ts_string` (~65 call sites
273/// across the emitter's real emission code) delegates here rather than
274/// keeping its own copy — the two must stay byte-identical since both
275/// splice into generated TypeScript, so this is the one shared definition.
276pub fn escape_ts_literal(s: &str) -> String {
277    let mut out = String::with_capacity(s.len());
278    for c in s.chars() {
279        match c {
280            '\\' => out.push_str("\\\\"),
281            '"' => out.push_str("\\\""),
282            '\n' => out.push_str("\\n"),
283            '\t' => out.push_str("\\t"),
284            '\r' => out.push_str("\\r"),
285            c => out.push(c),
286        }
287    }
288    out
289}