Skip to main content

zerodds_wire_codec/
default.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value a member takes when the stream does not carry it.
4//!
5//! XTypes 1.3 §7.4.1.2.3: "if the receiver's member is not optional, it
6//! logically takes its default value". §7.2.4.4.8, Table 19, says the
7//! same thing about assignability in general — "Each non-optional member
8//! in a T1 object that is not present in the T2 object takes its default
9//! value" — so this is the one rule behind both halves of type
10//! evolution: a `@mutable` member the writer omitted and the tail of an
11//! `@appendable` type the writer did not have.
12//!
13//! ## Where the value comes from
14//!
15//! Two places, in this order, per §7.2.2.4.4.4.9:
16//!
17//! 1. **The member's explicit default**, if its declaration carries one.
18//!    "An explicit definition associates a default value with the member
19//!    at the time where the aggregated type is defined." That is
20//!    `@default` in IDL. [`member`] is the entry point that consults it.
21//! 2. **The type's implicit default**, otherwise: "The implicit default
22//!    value for a member is defined to match the default value of the
23//!    member type. See Table 9." That is [`of_type`].
24//!
25//! Substituting Table 9's zero for a member the designer gave an
26//! explicit `@default` would be a silently wrong value, which is the
27//! failure class this project ranks above every other.
28//!
29//! ## Table 9, exactly
30//!
31//! | Type kind | Default |
32//! |---|---|
33//! | `BYTE` | `0x00` |
34//! | `BOOLEAN` | False |
35//! | `INT_8`…`UINT_64`, `FLOAT_32`, `FLOAT_64`, `FLOAT_128` | 0 |
36//! | `CHAR_8`, `CHAR_16` | `'\0'` |
37//! | `STRING_TYPE` | `""` |
38//! | `ARRAY_TYPE` | the same dimensions, each element at its default |
39//! | `ALIAS_TYPE` | the default of the alias's base type |
40//! | `SEQUENCE_TYPE` | a zero-length sequence |
41//! | `MAP_TYPE` | an empty map |
42//! | `ENUM_TYPE` | the first literal |
43//! | `UNION_TYPE` | the discriminator at its default; if that selects a
44//!   branch, the selected member at its default too |
45//! | `STRUCTURE_TYPE` | no optional members, the others at their
46//!   defaults |
47//!
48//! ## The three kinds Table 9 does not list
49//!
50//! `BITMASK_TYPE`, `BITSET_TYPE` and `fixed<P,S>` have no row. The
51//! standing rule where the specification is silent is the broadest and
52//! safest reading, written down:
53//!
54//! - A **bitmask** is held in a `UInt8`/`16`/`32`/`64` (Table 33) and
55//!   §7.4.1.1.3.2 puts the bits at or above the bound outside the
56//!   bitmask. The default of the holder is 0 by Table 9's integer row,
57//!   and 0 sets no bit, so the default is the empty set. Table 18 pairs
58//!   bitmask and enumeration for assignability, which is the same
59//!   grouping; no reading of either clause produces a bitmask with a bit
60//!   already set.
61//! - A **bitset** is likewise packed into an anchor of 1, 2, 4 or 8
62//!   bytes, whose Table 9 default is 0; every bitfield reads back 0 out
63//!   of it. An anonymous bitfield is padding and carries no value, which
64//!   is what the encoder and the decoder already agree on.
65//! - `fixed<P,S>` is not in the XTypes type system at all — P4 of
66//!   `docs/wire-feature-inventory.md` — so Table 9 could not have a row
67//!   for it. IDL 4.2 §7.4.1.4.4.3.4 gives it a decimal value, and the
68//!   only value every other row of Table 9 agrees on is zero. Written
69//!   with the type's own scale, because that is the one spelling of zero
70//!   `canonical::decimal` accepts for the type.
71//!
72//! Each is the same answer the type's serialised zero would decode to,
73//! which is the property that makes the reading safe: a peer that
74//! *wrote* the member at its default and a peer that omitted it hand the
75//! receiver the same value.
76
77use crate::canonical;
78use crate::encode::ProbeError;
79use crate::sink::{Aggregate, Sink};
80use crate::source::Source;
81use std::string::String;
82use std::vec::Vec;
83use zerodds_ast::{Ast, ConstExpr, Literal, MemberNode, PrimitiveKind, TypeKind, TypeNode};
84use zerodds_ast_core::Id;
85
86/// How deep a default may nest before the model is taken to be cyclic.
87///
88/// A structure cannot contain itself by value — the recursion in IDL
89/// goes through a sequence, whose default is empty, or through an
90/// `@optional` member, which takes no default at all. So a walk this
91/// deep means the model holds a cycle the frontend admitted, and
92/// refusing is the only answer that is not a stack overflow.
93const MAX_DEPTH: usize = 64;
94
95/// The default value of one member: its `@default`, or its type's.
96///
97/// # Errors
98/// [`ProbeError`] when the type has no default this project can name, or
99/// when a `@default` and the member's type disagree.
100pub fn member<V: Source + Sink>(
101    ast: &Ast,
102    node: &MemberNode,
103    type_: Id<TypeNode>,
104    path: &str,
105) -> Result<V, ProbeError> {
106    member_at_depth(ast, node, type_, path, 0)
107}
108
109/// [`member`], carrying the recursion depth of the walk it is part of.
110fn member_at_depth<V: Source + Sink>(
111    ast: &Ast,
112    node: &MemberNode,
113    type_: Id<TypeNode>,
114    path: &str,
115    depth: usize,
116) -> Result<V, ProbeError> {
117    // §7.2.2.4.4.4.9: "Optional members are not allowed to associate an
118    // explicit default value for the member." An optional member never
119    // reaches here — it takes no value at all — so a `@default` on one
120    // is a declaration the frontend should have refused, and reading it
121    // would give an absent member a value.
122    if let Some(expr) = node.facets.xtypes.as_ref().and_then(|x| x.default_value) {
123        if node.facets.is_optional() {
124            return Err(ProbeError::Unsupported {
125                path: path.to_string(),
126                kind: "an `@optional` member with an explicit `@default`, which \
127                       §7.2.2.4.4.4.9 does not allow",
128            });
129        }
130        return explicit(ast, expr, type_, path);
131    }
132    at_depth(ast, type_, path, depth)
133}
134
135/// Table 9's default for a type, with no member annotation in play.
136///
137/// # Errors
138/// [`ProbeError`] for a type kind with no default.
139pub fn of_type<V: Source + Sink>(
140    ast: &Ast,
141    type_: Id<TypeNode>,
142    path: &str,
143) -> Result<V, ProbeError> {
144    at_depth(ast, type_, path, 0)
145}
146
147/// zerodds-lint: recursion-depth 64 — `MAX_DEPTH` above, checked at the
148/// top of this function. Table 9's default is a walk over the **type** and
149/// not over a value, so nothing else bounds it: a model with a cycle would
150/// descend forever, and the refusal names that rather than overflowing.
151#[allow(clippy::too_many_lines)]
152fn at_depth<V: Source + Sink>(
153    ast: &Ast,
154    type_: Id<TypeNode>,
155    path: &str,
156    depth: usize,
157) -> Result<V, ProbeError> {
158    if depth > MAX_DEPTH {
159        return Err(ProbeError::Unsupported {
160            path: path.to_string(),
161            kind: "a type whose default value nests deeper than a model without a cycle can",
162        });
163    }
164    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
165        detail: format!("{error}"),
166    })?;
167    match &node.kind {
168        TypeKind::Primitive(kind) => primitive(*kind, path),
169        // `""` for both widths. Table 9 has one `STRING_TYPE` row and
170        // §7.2.2.2.1.3 makes the wide string a string type; the empty
171        // string is the same value in both.
172        TypeKind::String { .. } => Ok(V::text(String::new())),
173        TypeKind::Fixed { digits, scale } => {
174            Ok(V::fixed(canonical::decimal("0", *digits, *scale, path)?))
175        }
176        TypeKind::Alias { aliased } => {
177            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
178                detail: format!("{path}: the typedef target is unresolved"),
179            })?;
180            at_depth(ast, target, path, depth + 1)
181        }
182        TypeKind::Enumeration { .. } => first_enumerator(ast, type_, path),
183        // No bit set. See the module note.
184        TypeKind::Bitmask { .. } => Ok(V::bit_names(Vec::new())),
185        TypeKind::Bitset { .. } => {
186            let mut out = Vec::new();
187            for id in &ast.all_bitfields_of(type_) {
188                let Ok(field) = ast.bitfield(*id) else {
189                    continue;
190                };
191                let Some(name) = field.name.as_ref() else {
192                    // Anonymous: padding, and no member of the value.
193                    continue;
194                };
195                out.push((name.clone(), 0));
196            }
197            V::bitfields(out, path)
198        }
199        // "A zero-length sequence of the same element type" and "an
200        // empty map of the same element type". Both are the empty array
201        // this project's canonical form gives them.
202        TypeKind::Sequence { .. } | TypeKind::Map { .. } => Ok(V::collection(Vec::new())),
203        TypeKind::Array { element, dims } => {
204            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
205                detail: format!("{path}: the array element is unresolved"),
206            })?;
207            let counts: Vec<usize> = dims
208                .iter()
209                .map(|dim| {
210                    ast.eval_integral(*dim)
211                        .ok()
212                        .and_then(|value| usize::try_from(value).ok())
213                        .unwrap_or(0)
214                })
215                .collect();
216            array(ast, element, path, &counts, depth + 1)
217        }
218        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
219            structure(ast, type_, path, depth + 1)
220        }
221        TypeKind::Union { .. } => union(ast, type_, path, depth + 1),
222        other => Err(ProbeError::Unfilled {
223            path: path.to_string(),
224            kind: other.as_str(),
225        }),
226    }
227}
228
229/// Table 9's primitive rows, each in the shape its own width has.
230///
231/// The shapes are `canonical::primitive`'s and not new ones — the same
232/// [`Sink`] constructors, told the same [`PrimitiveKind`] — because a
233/// default that came back as `0` where a read value comes back as `"0"`
234/// would compare unequal to the very sample it is supposed to stand in
235/// for. That was a real risk while the two spelled their zeros
236/// separately.
237fn primitive<V: Sink>(kind: PrimitiveKind, path: &str) -> Result<V, ProbeError> {
238    match kind {
239        PrimitiveKind::Boolean => Ok(V::boolean(false)),
240        // `'\0'`, which is a one-character value here as every other
241        // character is. §7.2.2.2.1.2.2 makes Char8 eight bits wide, and
242        // U+0000 is inside it.
243        PrimitiveKind::Char | PrimitiveKind::WChar => Ok(V::character(kind, '\0')),
244        PrimitiveKind::Float128 => Ok(V::float128(zerodds_float128::Float128::ZERO)),
245        PrimitiveKind::Float32 | PrimitiveKind::Float64 => V::float(kind, 0.0_f64, path),
246        PrimitiveKind::Octet
247        | PrimitiveKind::UInt8
248        | PrimitiveKind::Int8
249        | PrimitiveKind::Int16
250        | PrimitiveKind::UInt16
251        | PrimitiveKind::Int32
252        | PrimitiveKind::UInt32
253        | PrimitiveKind::Int64
254        | PrimitiveKind::UInt64 => V::integer(kind, 0, path),
255    }
256}
257
258/// "The first literal in the enumeration", or the one `@default_literal`
259/// names.
260///
261/// §7.3.1.2.1.10 exists for exactly this: "Normally the default value
262/// for an object of a type is pre-defined based on the generic rules …
263/// for an enumeration it is the first literal. This generic rule is not
264/// desirable in some situations. The annotation `@default_literal`
265/// allows this behavior to be changed."
266///
267/// First in **declaration** order and not in value order. Table 9 says
268/// "the first literal in the enumeration", and `@value` reorders the
269/// numbers without reordering the declaration.
270fn first_enumerator<V: Sink>(ast: &Ast, type_: Id<TypeNode>, path: &str) -> Result<V, ProbeError> {
271    let mut first = None;
272    for id in ast.enumerators_of(type_) {
273        let Ok(node) = ast.enumerator(*id) else {
274            continue;
275        };
276        if node
277            .facets
278            .xtypes
279            .as_ref()
280            .is_some_and(|x| x.is_default_literal)
281        {
282            return Ok(V::enumerator(node.name.clone()));
283        }
284        if first.is_none() {
285            first = Some(node.name.clone());
286        }
287    }
288    first
289        .map(V::enumerator)
290        .ok_or_else(|| ProbeError::Mismatch {
291            path: path.to_string(),
292            expected: "an enumeration with at least one enumerator, whose first is its default"
293                .to_string(),
294            found: "an empty enumeration".to_string(),
295        })
296}
297
298/// "An array of the same dimensions and same element type whose elements
299/// take the default value for their corresponding type."
300///
301/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
302/// `counts` drops its head at each frame and the innermost element goes to
303/// `at_depth`, which carries `MAX_DEPTH`.
304fn array<V: Source + Sink>(
305    ast: &Ast,
306    element: Id<TypeNode>,
307    path: &str,
308    counts: &[usize],
309    depth: usize,
310) -> Result<V, ProbeError> {
311    let Some((&count, rest)) = counts.split_first() else {
312        return at_depth(ast, element, path, depth);
313    };
314    // One element built and cloned rather than built `count` times: the
315    // default of a type does not depend on the index, and a 4096-element
316    // array of a nested structure would otherwise walk the model 4096
317    // times to produce 4096 equal values.
318    let one: V = array(ast, element, path, rest, depth)?;
319    Ok(V::collection(std::iter::repeat_n(one, count).collect()))
320}
321
322/// "A structure without any of the optional members and with other
323/// members set to their default values based on their corresponding
324/// types."
325///
326/// `all_members_of` and not `members_of`, because §7.2.4.4.8 says
327/// members of a base type "shall be considered 'expanded' inside" the
328/// derived one: an inherited member is a member of this structure and
329/// takes its own default like any other.
330fn structure<V: Source + Sink>(
331    ast: &Ast,
332    type_: Id<TypeNode>,
333    path: &str,
334    depth: usize,
335) -> Result<V, ProbeError> {
336    let mut out = Aggregate::new();
337    for id in &ast.all_members_of(type_) {
338        let Ok(node) = ast.member(*id) else { continue };
339        // "A structure without any of the optional members": an absent
340        // optional member is absent, not zero.
341        if node.facets.is_optional() {
342            continue;
343        }
344        let Some(member_type) = node.type_.resolved() else {
345            return Err(ProbeError::NoLayout {
346                detail: format!("{path}.{}: the member type is unresolved", node.name),
347            });
348        };
349        let member_path = format!("{path}.{}", node.name);
350        out.insert(
351            node.name.clone(),
352            member_at_depth(ast, node, member_type, &member_path, depth)?,
353        );
354    }
355    Ok(V::aggregate(out))
356}
357
358/// "A union with the discriminator set to the default value for the
359/// discriminator type. If this selects a branch, then the selected
360/// member is also set to the default value for the member type.
361/// Otherwise the value of the union is fully specified by the
362/// discriminator value."
363///
364/// zerodds-lint: recursion-depth 64 — `MAX_DEPTH`, through `at_depth`,
365/// which this hands the depth it was given.
366fn union<V: Source + Sink>(
367    ast: &Ast,
368    type_: Id<TypeNode>,
369    path: &str,
370    depth: usize,
371) -> Result<V, ProbeError> {
372    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
373        detail: format!("{error}"),
374    })?;
375    let TypeKind::Union { discriminator, .. } = &node.kind else {
376        return Err(ProbeError::Unsupported {
377            path: path.to_string(),
378            kind: "a union whose kind changed under the probe",
379        });
380    };
381    let discriminator = discriminator
382        .resolved()
383        .ok_or_else(|| ProbeError::NoLayout {
384            detail: format!("{path}: the discriminator is unresolved"),
385        })?;
386    let discriminant_path = format!("{path}._d");
387    let discriminant: V = at_depth(ast, discriminator, &discriminant_path, depth)?;
388
389    let mut out = Aggregate::new();
390    let selector =
391        crate::decode::selector_of(ast, discriminator, &discriminant, &discriminant_path)?;
392    out.insert(String::from("_d"), discriminant);
393    // "If this selects a branch" — which includes the `default:` branch,
394    // because a discriminator value with no explicit label selects it.
395    //
396    // `false` for `UnionLabelMatch`, and it is a checked non-existence
397    // rather than a plan this walk does not have: the selector here is
398    // always Table 9's default for the discriminator's type, which is
399    // zero for every integer, `'\0'` for a `char` and `FALSE` for a
400    // `boolean`. The setting suppresses **negative** labels only, so no
401    // value it can reach is one this walk ever compares against.
402    // A label the model cannot evaluate is refused here too. This walk
403    // builds the default value of a union, and a skipped label would
404    // silently give the type a different branch's default — or none —
405    // which is the value every column then agrees on for the wrong
406    // reason.
407    let selected = crate::decode::select_case(ast, type_, selector, false).map_err(|_| {
408        ProbeError::Mismatch {
409            path: path.to_string(),
410            expected: String::from("a union whose case labels all evaluate"),
411            found: String::from("a case label the model cannot evaluate"),
412        }
413    })?;
414    if let Some(case) = selected {
415        let member_path = format!("{path}.{}", case.name);
416        let value = at_depth(ast, case.type_, &member_path, depth)?;
417        out.insert(case.name, value);
418    }
419    Ok(V::aggregate(out))
420}
421
422/// The value an explicit `@default` names, in the member type's own
423/// canonical form.
424///
425/// The literal is read and then put through `canonical::canonicalise`
426/// against the member's type, which is the same gate a value file goes
427/// through. That is what refuses `@default("abc")` on a `long` instead
428/// of quietly writing a zero into it: an annotation whose value the
429/// member cannot hold is a declaration error, and a probe that dropped
430/// it would produce a member the designer never asked for.
431fn explicit<V: Source + Sink>(
432    ast: &Ast,
433    expr: Id<zerodds_ast::ConstExprNode>,
434    type_: Id<TypeNode>,
435    path: &str,
436) -> Result<V, ProbeError> {
437    let raw: V = literal_value(ast, expr, path)?;
438    canonical::canonicalise(ast, type_, &raw, path)
439}
440
441/// One `@default` expression in loose form, before the type sees it.
442///
443/// An integral expression is evaluated — `@default(1 << 3)` is legal
444/// IDL — and every other kind has to be a literal, because there is no
445/// arithmetic on strings or characters to fold.
446///
447/// Loose because the literal is written without a type: a `@default(3)`
448/// on a `long` and on a `long long` are the same literal, and which one
449/// it is a value of is decided one line above, by
450/// [`canonical::canonicalise`] against the member's own type. So the
451/// text form is [`Sink::text`] for every kind that is not a number,
452/// including the enumerator's name — it is not yet known to *be* an
453/// enumerator name until the member's type says so.
454fn literal_value<V: Sink>(
455    ast: &Ast,
456    expr: Id<zerodds_ast::ConstExprNode>,
457    path: &str,
458) -> Result<V, ProbeError> {
459    let node = ast.expr(expr).map_err(|error| ProbeError::NoLayout {
460        detail: format!("{path}: {error}"),
461    })?;
462    match &node.expr {
463        ConstExpr::Literal(Literal::Boolean(flag)) => Ok(V::boolean(*flag)),
464        ConstExpr::Literal(Literal::Char(ch)) => Ok(V::text(ch.to_string())),
465        ConstExpr::Literal(Literal::WChar(code)) => match char::from_u32(*code) {
466            Some(ch) => Ok(V::text(ch.to_string())),
467            None => Err(ProbeError::Mismatch {
468                path: path.to_string(),
469                expected: "a `@default` wide character that is a code point".to_string(),
470                found: format!("{code:#x}"),
471            }),
472        },
473        ConstExpr::Literal(Literal::String(text)) => Ok(V::text(text.clone())),
474        ConstExpr::Literal(Literal::WString(codes)) => {
475            let mut text = String::with_capacity(codes.len());
476            for code in codes {
477                let Some(ch) = char::from_u32(*code) else {
478                    return Err(ProbeError::Mismatch {
479                        path: path.to_string(),
480                        expected: "a `@default` wide string of code points".to_string(),
481                        found: format!("{code:#x}"),
482                    });
483                };
484                text.push(ch);
485            }
486            Ok(V::text(text))
487        }
488        // Written out rather than parsed into an `f64` here: the string
489        // is what `canonical::canonicalise` narrows against the member's
490        // own width, so a `float` member and a `double` member with the
491        // same literal each get the value their width holds.
492        ConstExpr::Literal(Literal::Float {
493            mantissa,
494            exponent: None,
495        }) => number_text(mantissa, path),
496        ConstExpr::Literal(Literal::Float {
497            mantissa,
498            exponent: Some(exponent),
499        }) => number_text(&format!("{mantissa}e{exponent}"), path),
500        ConstExpr::Literal(Literal::Fixed { digits, scale }) => {
501            Ok(V::text(fixed_text(digits, *scale)))
502        }
503        ConstExpr::Literal(Literal::Enumerator(reference)) => {
504            let target = reference.resolved().ok_or_else(|| ProbeError::NoLayout {
505                detail: format!("{path}: the `@default` enumerator is unresolved"),
506            })?;
507            let node = ast
508                .enumerator(target)
509                .map_err(|error| ProbeError::NoLayout {
510                    detail: format!("{path}: {error}"),
511                })?;
512            Ok(V::text(node.name.clone()))
513        }
514        // An integer literal, a named constant or any arithmetic over
515        // them. `eval_integral` is the same folder every bound and every
516        // case label goes through.
517        _ => {
518            let value = ast
519                .eval_integral(expr)
520                .map_err(|error| ProbeError::Mismatch {
521                    path: path.to_string(),
522                    expected: "a `@default` this frontend can evaluate".to_string(),
523                    found: format!("{error:?}"),
524                })?;
525            // A string, so that a 64-bit member keeps its low bits;
526            // `canonicalise` reads both spellings for the narrow widths.
527            Ok(V::text(format!("{value}")))
528        }
529    }
530}
531
532/// A float literal's text as a number the representation can hold,
533/// refusing what it cannot write.
534fn number_text<V: Sink>(text: &str, path: &str) -> Result<V, ProbeError> {
535    let parsed: f64 = text.parse().map_err(|_| ProbeError::Mismatch {
536        path: path.to_string(),
537        expected: "a `@default` floating-point literal".to_string(),
538        found: format!("`{text}`"),
539    })?;
540    V::number(parsed, path)
541}
542
543/// A fixed-point literal's digits and scale back into a decimal.
544///
545/// `IDL 4.2` records the digits without the point and the scale beside
546/// them; `canonical::decimal` wants the point written, and it is the one
547/// that decides how many digits the member's own `fixed<P,S>` keeps.
548fn fixed_text(digits: &str, scale: u8) -> String {
549    let (negative, magnitude) = match digits.strip_prefix('-') {
550        Some(rest) => (true, rest),
551        None => (false, digits),
552    };
553    let scale = usize::from(scale);
554    let padded = if magnitude.len() <= scale {
555        format!("{}{magnitude}", "0".repeat(scale + 1 - magnitude.len()))
556    } else {
557        magnitude.to_string()
558    };
559    let split = padded.len() - scale;
560    let mut out = String::with_capacity(padded.len() + 2);
561    if negative {
562        out.push('-');
563    }
564    out.push_str(&padded[..split]);
565    if scale > 0 {
566        out.push('.');
567        out.push_str(&padded[split..]);
568    }
569    out
570}