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}