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::Query(..)
108        | TypeRef::Stream(..)
109        | TypeRef::Connection(..)
110        | TypeRef::History(..)
111        | TypeRef::ValidationError(_)
112        | TypeRef::JsonError(_)
113        | TypeRef::Unit(_)
114        | TypeRef::Fn(..) => Err("this field type is not wire-representable".to_string()),
115    }
116}
117
118/// The wire form of a base-type literal — the raw literal a real wire value
119/// of this base type would also be, so it re-enters
120/// `emit_field_deserialise`'s (`bynk-emit`) ordinary `typeof`/
121/// `Number.isInteger` checks unchanged. `Bytes` has no literal syntax, so it
122/// is not admitted.
123/// A qualified `TypeName.method(args)` call, in whichever `ExprKind` shape it
124/// actually parses as. Confirmed empirically (`OrderId.unsafe("x")` parses to
125/// `ExprKind::MethodCall { receiver: Ident("OrderId"), method: "unsafe", .. }`
126/// — the parser never distinguishes a type-qualified call from an ordinary
127/// instance method call; that's a resolver-time decision) — `ConstructorCall`
128/// is handled too, defensively, in case some other path still produces it.
129fn qualified_call(e: &Expr) -> Option<(&str, &str, &[Expr])> {
130    match &e.kind {
131        ExprKind::MethodCall {
132            receiver,
133            method,
134            args,
135            ..
136        } => {
137            let ExprKind::Ident(recv) = &receiver.kind else {
138                return None;
139            };
140            Some((recv.name.as_str(), method.name.as_str(), args.as_slice()))
141        }
142        ExprKind::ConstructorCall {
143            type_name,
144            method,
145            args,
146        } => Some((
147            type_name.name.as_str(),
148            method.name.as_str(),
149            args.as_slice(),
150        )),
151        _ => None,
152    }
153}
154
155fn lower_base_literal(base: BaseType, e: &Expr) -> Result<String, String> {
156    // Strip one level of negation so `-5`/`-5.0` reach the literal arms below,
157    // mirroring `const_literal`'s admission — `i64::checked_neg` guards
158    // `i64::MIN`, which has no positive counterpart to negate away from.
159    let (negate, inner) = match &e.kind {
160        ExprKind::UnaryOp(UnaryOp::Neg, inner) => (true, &inner.kind),
161        other => (false, other),
162    };
163    match (base, inner) {
164        (BaseType::Int | BaseType::Instant, ExprKind::IntLit { value, .. }) => {
165            let v = if negate {
166                value
167                    .checked_neg()
168                    .ok_or_else(|| "integer literal has no negation".to_string())?
169            } else {
170                *value
171            };
172            Ok(v.to_string())
173        }
174        (BaseType::Float, ExprKind::IntLit { value, .. }) => {
175            let v = if negate { -*value } else { *value };
176            Ok(v.to_string())
177        }
178        (BaseType::Float, ExprKind::FloatLit { lexeme, .. }) => Ok(if negate {
179            format!("-{lexeme}")
180        } else {
181            lexeme.clone()
182        }),
183        (BaseType::String, ExprKind::StrLit(s)) if !negate => {
184            Ok(format!("\"{}\"", escape_ts_literal(s)))
185        }
186        (BaseType::Bool, ExprKind::BoolLit(b)) if !negate => Ok(b.to_string()),
187        (BaseType::Duration, ExprKind::DurationLit { millis, .. }) if !negate => {
188            Ok(millis.to_string())
189        }
190        (BaseType::Bytes, _) => Err("a `Bytes` field has no literal default form".to_string()),
191        _ => Err(format!("expected a `{}` literal", base.name())),
192    }
193}
194
195/// The wire form of a sum-variant default: `{ kind: "Variant" }` (nullary) or
196/// `{ kind: "Variant", f1: ..., f2: ... }` (payload, positionally recursed
197/// against each field's *declared* type) — never a qualified reference into
198/// the sum's generated value namespace (`Sum.Variant`), which is what the
199/// ordinary handler-body lowering (`lower_expr_into`) would produce and which
200/// a foreign module regenerating this codec cannot import.
201fn lower_sum_default(
202    sum_name: &str,
203    body: &SumBody,
204    init: &Expr,
205    types: &std::collections::HashMap<String, Arc<TypeDecl>>,
206) -> Result<String, String> {
207    let (variant_name, args): (&str, &[Expr]) = match &init.kind {
208        ExprKind::Ident(id) => (id.name.as_str(), &[]),
209        ExprKind::FieldAccess { receiver, field } => {
210            let ExprKind::Ident(recv) = &receiver.kind else {
211                return Err(format!("expected a variant of `{sum_name}`"));
212            };
213            if recv.name != sum_name {
214                return Err(format!(
215                    "expected a variant of `{sum_name}`, not `{}`",
216                    recv.name
217                ));
218            }
219            (field.name.as_str(), &[])
220        }
221        ExprKind::Call { name, args, .. } => (name.name.as_str(), args.as_slice()),
222        _ => match qualified_call(init) {
223            Some((recv, method, args)) if recv == sum_name => (method, args),
224            Some((recv, ..)) => {
225                return Err(format!("expected a variant of `{sum_name}`, not `{recv}`"));
226            }
227            None => return Err(format!("expected a variant of `{sum_name}`")),
228        },
229    };
230    let Some(variant) = body.variants.iter().find(|v| v.name.name == variant_name) else {
231        return Err(format!("`{variant_name}` is not a variant of `{sum_name}`"));
232    };
233    if args.len() != variant.payload.len() {
234        return Err(format!(
235            "`{variant_name}` takes {} payload field(s), got {}",
236            variant.payload.len(),
237            args.len()
238        ));
239    }
240    let mut parts = vec![format!("kind: \"{variant_name}\"")];
241    for (field, arg) in variant.payload.iter().zip(args.iter()) {
242        let v = lower_field_default_wire(arg, &field.type_ref, types)?;
243        parts.push(format!("{}: {v}", field.name.name));
244    }
245    Ok(format!("{{ {} }}", parts.join(", ")))
246}
247
248/// The wire form of a record-literal default: a plain object literal, each
249/// field recursed against its *declared* type. Records are structurally
250/// wire-shaped (never tagged, never a named constructor), so this never
251/// needs qualification either.
252fn lower_record_default(
253    body: &RecordBody,
254    init: &Expr,
255    types: &std::collections::HashMap<String, Arc<TypeDecl>>,
256) -> Result<String, String> {
257    let ExprKind::RecordConstruction { fields, .. } = &init.kind else {
258        return Err("expected a record literal".to_string());
259    };
260    let mut parts = Vec::new();
261    for f in &body.fields {
262        let Some(given) = fields.iter().find(|fi| fi.name.name == f.name.name) else {
263            return Err(format!("record default is missing field `{}`", f.name.name));
264        };
265        let Some(value) = &given.value else {
266            return Err(format!(
267                "record default's field `{}` cannot use shorthand (no bindings are in scope)",
268                f.name.name
269            ));
270        };
271        let v = lower_field_default_wire(value, &f.type_ref, types)?;
272        parts.push(format!("{}: {v}", f.name.name));
273    }
274    Ok(format!("{{ {} }}", parts.join(", ")))
275}
276
277/// Escapes a string for embedding in a TypeScript double-quoted string
278/// literal. `pub` because `bynk-emit`'s `escape_ts_string` (~65 call sites
279/// across the emitter's real emission code) delegates here rather than
280/// keeping its own copy — the two must stay byte-identical since both
281/// splice into generated TypeScript, so this is the one shared definition.
282pub fn escape_ts_literal(s: &str) -> String {
283    let mut out = String::with_capacity(s.len());
284    for c in s.chars() {
285        match c {
286            '\\' => out.push_str("\\\\"),
287            '"' => out.push_str("\\\""),
288            '\n' => out.push_str("\\n"),
289            '\t' => out.push_str("\\t"),
290            '\r' => out.push_str("\\r"),
291            c => out.push(c),
292        }
293    }
294    out
295}