zerodds_lang_rules/emit/decode.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! C decoder bodies, built from the same layout plan. Spec §5, §6.2.
4//!
5//! The counterpart of [`crate::encode_c`], which walks the plan and
6//! writes. This walks it and reads. Not a second reading of the model,
7//! for the reason the encoder gives: two walks that decided the framing
8//! separately would agree with each other only until one of them
9//! changed.
10//!
11//! ## Why the column needs this at all
12//!
13//! Encoding is measured by comparing bytes, and a byte comparison
14//! passes a decoder that is wrong in the same way its own encoder is
15//! wrong. A round trip through one implementation passes such a defect
16//! every time; a round trip through two does not — and the C decoder
17//! and the Rust decoder are two.
18//! `docs/specs/zerodds-interop-suite-1.0.md` §4, P2.
19//!
20//! ## Where reading needs a decision writing did not
21//!
22//! The same three that `crates/wire-probe/src/decode.rs` names for the
23//! reflective reader, and they are why this file is longer than the
24//! encoder:
25//!
26//! - **A union's member.** The writer takes the member the value names;
27//! the reader has only the discriminator, so the generated code
28//! compares it against every case label, falls back to `default:`,
29//! and reads no member at all where neither matches.
30//! - **A `@mutable` aggregate is driven by its headers and not by the
31//! plan.** A writer may order the members as it likes and leave any
32//! `@optional` one out, so the generated code is a dispatch on the
33//! member id rather than a straight line.
34//!
35//! ## What C forces that Rust does not
36//!
37//! - **Somewhere to put a string.** An IDL `string` is a
38//! `const char *` and a `sequence<T>` is a count beside a pointer, so
39//! the read direction needs storage the write direction did not. It
40//! comes from `zdw_reader_alloc`, out of a buffer the caller set with
41//! `zdw_reader_set_heap` — the same discipline as the writer's output
42//! buffer, and for the same reason: an embedded target decides where
43//! memory comes from.
44//! - **Refusals the runtime cannot see.** An integer that names no
45//! enumerator and a member that never arrived are refusals of the
46//! *type*, which `runtime/c` does not know. `zdw_reader_refuse` is
47//! how the generated code says so; without it the decoder would hand
48//! its caller a value the type does not admit.
49
50use crate::adapter::c::{ordered_types, ordered_types_from, ordered_types_from_pruning};
51use crate::emit::encode::CodecOptions;
52use crate::emit::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
53use crate::encode::EncodeError;
54use zerodds_ast::{
55 Ast, CaseLabel, CharWidth, Extensibility, PrimitiveKind, TryConstruct, TypeKind, TypeNode,
56};
57use zerodds_ast_core::Id;
58use zerodds_ast_layout::key::{
59 ElementPadding, FixedDigitCount, LayoutKey, WcharPosition, WcharWidth, WstringCodeUnits,
60 WstringLengthCounts, WstringLengthPolicy, WstringUnitOrder, WstringUnitWidth,
61};
62use zerodds_ast_layout::plan::{Extent, MemberHeader, ScalarWrite, scalar_write};
63use zerodds_ast_layout::xcdr::DepCollector;
64use zerodds_lang_cst::common::{Doc, Literal};
65
66/// The decoder function name for a type.
67///
68/// Named by the IDL kind for a primitive, for the reason
69/// [`crate::encode_c::codec_name`] gives: `double` and `long double`
70/// share a C type and do not share a wire form.
71#[must_use]
72pub fn decoder_name<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>) -> String {
73 let name = match ast.type_(id).map(|node| node.kind.clone()) {
74 Ok(TypeKind::Primitive(kind)) => {
75 format!("zd_dec_p_{}", kind.as_str().replace(' ', "_"))
76 }
77 _ => format!("zd_dec_{}", T::type_name(ast, id)),
78 };
79 // As `crate::emit::encode::codec_name`: the published spelling, in
80 // one place. See [`CodecTarget::published_name`].
81 T::published_name(name)
82}
83
84/// Build the decoder for every type in the model, under one plan.
85///
86/// Existing data entry points keep their published names and initialization.
87///
88/// # Errors
89/// [`EncodeError`] when the layout cannot be planned or a construct has
90/// no C reader — never a body that leaves a member out.
91pub fn decoder<T: CodecTarget>(
92 ast: &Ast,
93 key: LayoutKey,
94 options: CodecOptions,
95) -> Result<Vec<T::Decl>, EncodeError> {
96 decoder_over::<T>(
97 ast,
98 &ordered_types(ast),
99 &crate::emit::encode::sample_types(ast),
100 key,
101 options,
102 )
103}
104
105/// [`decoder`] without the types in `excluded`. See
106/// [`crate::emit::encode::codec_excluding`].
107///
108/// # Errors
109/// As [`decoder`].
110pub fn decoder_excluding<T: CodecTarget>(
111 ast: &Ast,
112 key: LayoutKey,
113 options: CodecOptions,
114 excluded: &[Id<TypeNode>],
115 also_reaching: &[Id<TypeNode>],
116) -> Result<Vec<T::Decl>, EncodeError> {
117 if excluded.is_empty() {
118 return decoder::<T>(ast, key, options);
119 }
120 let (types, entries) = crate::emit::encode::kept_after_excluding(ast, excluded, also_reaching);
121 decoder_over::<T>(ast, &types, &entries, key, options)
122}
123
124/// The decoder for the types one root reaches, and no others.
125///
126/// What a probe's translation unit wants: a decoder for a type the root
127/// cannot reach is a `static` function nothing calls, and a generated
128/// file that warns is one whose warnings stop being read.
129///
130/// # Errors
131/// As [`decoder`].
132pub fn decoder_from<T: CodecTarget>(
133 ast: &Ast,
134 root: Id<TypeNode>,
135 key: LayoutKey,
136 options: CodecOptions,
137) -> Result<Vec<T::Decl>, EncodeError> {
138 decoder_over::<T>(
139 ast,
140 &ordered_types_from(ast, &[root]),
141 &[root],
142 key,
143 options,
144 )
145}
146
147/// The decoder for the types several roots reach, each function once.
148///
149/// # Errors
150/// As [`decoder`].
151pub fn decoder_from_many<T: CodecTarget>(
152 ast: &Ast,
153 roots: &[Id<TypeNode>],
154 key: LayoutKey,
155 options: CodecOptions,
156) -> Result<Vec<T::Decl>, EncodeError> {
157 decoder_over::<T>(ast, &ordered_types_from(ast, roots), roots, key, options)
158}
159
160/// [`decoder_from_many`] without the types in `excluded`; see
161/// [`crate::emit::encode::codec_from_many_excluding`].
162///
163/// # Errors
164/// As [`decoder`].
165pub fn decoder_from_many_excluding<T: CodecTarget>(
166 ast: &Ast,
167 roots: &[Id<TypeNode>],
168 key: LayoutKey,
169 options: CodecOptions,
170 excluded: &[Id<TypeNode>],
171) -> Result<Vec<T::Decl>, EncodeError> {
172 let types = ordered_types_from_pruning(ast, roots, excluded);
173 decoder_over::<T>(ast, &types, roots, key, options)
174}
175
176/// The distinct name of the bounded inverse key-holder entry.
177#[must_use]
178pub fn key_holder_decoder_name<T: CodecTarget>(ast: &Ast, root: Id<TypeNode>) -> String {
179 T::published_name(format!("zd_keyread_{}", decoder_name::<T>(ast, root)))
180}
181
182/// Read a ForcedFinal PLAIN_CDR2-BE struct holder with scalar/string/enum keys.
183/// The shared key plan decides root selection and member order. Nested
184/// aggregates, aliases, collections and indirect keys explicitly refuse until
185/// their full key context can be carried, never falling through to data reads.
186/// Non-key fields come from the target's existing default materializer.
187///
188/// # Errors
189/// A missing plan, unsupported root/member/dialect or target initialization.
190pub fn key_holder_decoder_from<T: CodecTarget>(
191 ast: &Ast,
192 root: Id<TypeNode>,
193 key: LayoutKey,
194 options: CodecOptions,
195) -> Result<Option<Vec<T::Decl>>, EncodeError> {
196 let refuse = |detail: &str| EncodeError::NoLayout {
197 detail: detail.to_string(),
198 };
199 if key != LayoutKey::key(zerodds_ast_layout::Encoding::PlainCdr2) {
200 return Err(refuse(
201 "key holder inverse supports the default ForcedFinal PLAIN_CDR2-BE key dialect only",
202 ));
203 }
204 if !matches!(
205 ast.type_(root).map(|node| &node.kind),
206 Ok(TypeKind::Structure { .. })
207 ) {
208 return Err(refuse(
209 "key holder inverse currently supports struct roots only",
210 ));
211 }
212 let keys = zerodds_ast_layout::keyhash::key_members_with(
213 ast,
214 root,
215 key.key_holder_nesting,
216 key.key_holder_order,
217 )
218 .map_err(crate::encode::plan_refusal)?;
219 if keys.is_empty() {
220 return Ok(None);
221 }
222 let layout = zerodds_ast_layout::keyhash::key_holder_plan_with(
223 ast,
224 root,
225 key,
226 &mut DepCollector::default(),
227 )
228 .map_err(crate::encode::plan_refusal)?;
229 let members = slots::<T>(ast, root, &layout)?;
230 // key_members_with includes descendant member identities. Diagnose an
231 // unsupported nested context before comparing the direct slot count.
232 for slot in &members {
233 if slot.optional
234 || slot.external
235 || slot.indirect
236 || !matches!(
237 ast.type_(slot.type_).map(|node| &node.kind),
238 Ok(TypeKind::Primitive(_) | TypeKind::String { .. } | TypeKind::Enumeration { .. })
239 )
240 {
241 return Err(refuse(&format!(
242 "key holder inverse member `{}` requires unsupported nested, alias, collection or indirect key context",
243 slot.field
244 )));
245 }
246 }
247 if members.len() != keys.len() || layout.needs_dheader {
248 return Err(refuse(
249 "key holder inverse could not describe every planned key member",
250 ));
251 }
252 let mut declarations = Vec::new();
253 let mut emitted = Vec::new();
254 let mut statements = Vec::new();
255 for slot in &members {
256 let name = key_holder_decoder_name::<T>(ast, slot.type_);
257 if !emitted.contains(&name) {
258 let body = body_of::<T>(ast, slot.type_, key, options, &name)?
259 .ok_or_else(|| refuse("key holder inverse has no member reader"))?;
260 declarations.push(initialized_key_reader::<T>(
261 ast,
262 slot.type_,
263 name.clone(),
264 body,
265 )?);
266 emitted.push(name.clone());
267 }
268 statements.push(T::read_call(
269 &name,
270 arrow::<T>("v", &slot.field),
271 Handoff::Value,
272 T::constant(Constant::TryConstructDiscard),
273 ));
274 }
275 declarations.push(initialized_key_reader::<T>(
276 ast,
277 root,
278 key_holder_decoder_name::<T>(ast, root),
279 T::block(statements),
280 )?);
281 Ok(Some(declarations))
282}
283
284fn initialized_key_reader<T: CodecTarget>(
285 ast: &Ast,
286 id: Id<TypeNode>,
287 name: String,
288 body: T::Block,
289) -> Result<T::Decl, EncodeError> {
290 let initializer = T::default_into(ast, id, deref::<T>("v"))?;
291 T::initialized_reader(
292 Routine {
293 doc: Doc::none(),
294 name,
295 sample: T::value_type(ast, id),
296 held: Handoff::Value,
297 writing: false,
298 tolerate_unused: false,
299 extra: vec![(T::handle(Handle::TryConstruct), "zd_tc".to_string())],
300 body: Some(with_tc_used::<T>(body)),
301 },
302 initializer,
303 )
304 .ok_or_else(|| EncodeError::NoLayout {
305 detail: "target does not support explicit key-holder reader initialization".to_string(),
306 })
307}
308
309fn decoder_over<T: CodecTarget>(
310 ast: &Ast,
311 types: &[Id<TypeNode>],
312 entries: &[Id<TypeNode>],
313 key: LayoutKey,
314 options: CodecOptions,
315) -> Result<Vec<T::Decl>, EncodeError> {
316 crate::encode::axes_the_codecs_carry_for::<T>(key)?;
317 let mut prototypes = Vec::new();
318 let mut bodies = Vec::new();
319 let mut emitted: Vec<String> = Vec::new();
320 // As `crate::emit::encode::codec_over`: an octet decoder only a bulk
321 // read would have called is not emitted.
322 let uncalled = crate::emit::encode::called_only_in_bulk(
323 ast,
324 types,
325 entries,
326 T::bulk_octets_read("v", name::<T>("zd_n")).is_some(),
327 );
328
329 for id in types.iter().copied() {
330 if uncalled.contains(&id) {
331 continue;
332 }
333 let name = decoder_name::<T>(ast, id);
334 if emitted.contains(&name) {
335 continue;
336 }
337 let Some(body) = body_of::<T>(ast, id, key, options, &name)? else {
338 continue;
339 };
340 emitted.push(name.clone());
341 // A target with no forward declarations answers `None` to the
342 // prototype and the loop emits only the body.
343 prototypes.extend(function::<T>(ast, id, key, &name, None));
344 bodies.extend(function::<T>(
345 ast,
346 id,
347 key,
348 &name,
349 Some(with_tc_used::<T>(body)),
350 ));
351 }
352
353 prototypes.extend(bodies);
354 // Not the Table 9 defaulters. Which helper a target needs for an
355 // absent member — and whether it needs one at all — is the
356 // materialiser's answer, and the C binding prepends C's after this
357 // returns.
358 Ok(prototypes)
359}
360
361/// The body, with `zd_tc` marked as read.
362///
363/// Most decoders have nothing to do with the behaviour — a `long` has no
364/// bound to miss — and the C probe is built with `-Wall -Wextra
365/// -Werror`, where an unused parameter fails the build. `(void)zd_tc;`
366/// is valid whether or not the body goes on to use it, so it is emitted
367/// unconditionally rather than from a guess about which bodies read it.
368fn with_tc_used<T: CodecTarget>(body: T::Block) -> T::Block {
369 let mut statements = T::mark_used("zd_tc");
370 statements.extend(T::into_statements(body));
371 T::block(statements)
372}
373
374/// `static void <name>(zdw_reader *r, <T> *v, zdw_try_construct zd_tc)`,
375/// with or without a body.
376///
377/// The value is not `const` here and is in the encoder, which is the
378/// only difference in the two signatures. An array parameter is the
379/// array itself for the reason the encoder gives: C adjusts it to a
380/// pointer to its first element, and `(*v)[i][j]` would otherwise need
381/// parentheses at every use.
382fn function<T: CodecTarget>(
383 ast: &Ast,
384 id: Id<TypeNode>,
385 key: LayoutKey,
386 name: &str,
387 body: Option<T::Block>,
388) -> Option<T::Decl> {
389 T::routine(Routine {
390 doc: Doc::none(),
391 name: name.to_string(),
392 sample: T::value_type(ast, id),
393 held: if is_array(ast, id, 0) {
394 Handoff::Array
395 } else {
396 Handoff::Value
397 },
398 writing: false,
399 // The reading counterpart of `emit::encode::maybe_unused`: a
400 // `Char16` read inline at every element site leaves this
401 // function without a caller under TAO's framing.
402 tolerate_unused: crate::emit::encode::maybe_unused(ast, id, key),
403 // What this *position* does with a value the type cannot hold.
404 // A parameter and not a property of the type, because
405 // DDS-XTypes 1.3 §7.2.2.7 attaches the behaviour to the member,
406 // the element, the map key or the discriminator — and one
407 // `typedef string<5>` may be used at two positions that want
408 // different answers.
409 extra: vec![(T::handle(Handle::TryConstruct), "zd_tc".to_string())],
410 body,
411 })
412}
413
414/// Whether a type is a C array once typedefs are seen through.
415///
416/// zerodds-lint: recursion-depth 64 (typedef chains; bounded by the
417/// `depth > 64` check, past which the answer is `false` — the
418/// destination form a non-array takes)
419fn is_array(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
420 if depth > 64 {
421 return false;
422 }
423 match ast.type_(id).map(|node| node.kind.clone()) {
424 Ok(TypeKind::Array { .. }) => true,
425 Ok(TypeKind::Alias { aliased }) => aliased
426 .resolved()
427 .is_some_and(|target| is_array(ast, target, depth + 1)),
428 _ => false,
429 }
430}
431
432/// How a destination of `id` is handed to its decoder: the array
433/// itself, or anything else in the form [`Handoff::Value`] names.
434const fn destination_form(is_array: bool) -> Handoff {
435 if is_array {
436 Handoff::Array
437 } else {
438 Handoff::Value
439 }
440}
441
442/// One member's read, with its explicit `@default` where the type's
443/// decoder cannot supply it.
444///
445/// `USE_DEFAULT` plus a `@default` is the one combination a
446/// type-keyed decoder cannot answer, so the site asks for DISCARD and
447/// takes the answer back. `zdw_reader_clear_discard` clears that status
448/// and no other: a truncated stream is not a narrowing. The position is
449/// already right, because every narrowing reads the whole value off the
450/// wire before it decides.
451fn member_read<T: CodecTarget>(ast: &Ast, slot: &Slot<T>, place: T::Expr) -> Vec<T::Stmt> {
452 let Some(assign_default) = slot.explicit_default.clone() else {
453 return vec![decode_into::<T>(ast, slot.type_, place, slot.try_construct)];
454 };
455 vec![
456 decode_into::<T>(ast, slot.type_, place, TryConstruct::Discard),
457 T::when(
458 call_expr::<T>("reader_clear_discard", Vec::new()),
459 T::block(assign_default),
460 None,
461 ),
462 ]
463}
464
465/// `zd_dec_<T>(r, <destination>, <behaviour>);`
466/// As [`decode_into`], for a value reached as a collection element.
467///
468/// The decoder generated for a type is one function and cannot ask at
469/// run time where it was called from, so the one primitive whose
470/// framing depends on the position — `Char16` under
471/// [`WcharFraming::GiopLengthPrefixedMemberOnly`] — is read inline here
472/// with the element's own constant. Every other element and every other
473/// framing falls through to the ordinary call, so the generated code is
474/// unchanged outside TAO's dialect.
475fn decode_element_into<T: CodecTarget>(
476 ast: &Ast,
477 id: Id<TypeNode>,
478 place: T::Expr,
479 behaviour: TryConstruct,
480 key: LayoutKey,
481) -> T::Stmt {
482 if key.wchar_framing.has_length_octet(WcharPosition::Member)
483 != key.wchar_framing.has_length_octet(WcharPosition::Element)
484 && matches!(resolve_primitive(ast, id, 0), Some(PrimitiveKind::WChar))
485 {
486 let Some(write) = scalar_write(ast, id, key, WcharPosition::Element) else {
487 return decode_into::<T>(ast, id, place, behaviour);
488 };
489 // `primitive_read_at` already casts a wchar read to the code
490 // point holder; a second cast here survived `store`'s
491 // one-cast folding and Java's `-Xlint:cast -Werror` names the
492 // leftover `(int)` (36 tao/rti element-form cells).
493 return T::store(place, T::value_type(ast, id), primitive_read_at::<T>(write));
494 }
495 decode_into::<T>(ast, id, place, behaviour)
496}
497
498/// The primitive kind `id` is once typedefs are seen through.
499///
500/// zerodds-lint: recursion-depth 32 (typedef chains; bounded by the
501/// `depth > 32` check, past which the answer is `None` — not a
502/// primitive)
503fn resolve_primitive(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<PrimitiveKind> {
504 if depth > 32 {
505 return None;
506 }
507 match ast.type_(id).map(|node| node.kind.clone()) {
508 Ok(TypeKind::Primitive(kind)) => Some(kind),
509 Ok(TypeKind::Alias { aliased }) => aliased
510 .resolved()
511 .and_then(|target| resolve_primitive(ast, target, depth + 1)),
512 _ => None,
513 }
514}
515
516fn decode_into<T: CodecTarget>(
517 ast: &Ast,
518 id: Id<TypeNode>,
519 place: T::Expr,
520 behaviour: TryConstruct,
521) -> T::Stmt {
522 T::read_call(
523 &decoder_name::<T>(ast, id),
524 place,
525 destination_form(is_array(ast, id, 0)),
526 T::constant(try_construct_constant(behaviour)),
527 )
528}
529
530/// The `zdw_try_construct` constant one behaviour is spelled by.
531const fn try_construct_constant(behaviour: TryConstruct) -> Constant {
532 match behaviour {
533 TryConstruct::Discard => Constant::TryConstructDiscard,
534 TryConstruct::UseDefault => Constant::TryConstructUseDefault,
535 TryConstruct::Trim => Constant::TryConstructTrim,
536 }
537}
538
539/// `switch (zd_tc)` over the three behaviours, as one statement.
540///
541/// `trim` is what TRIM does where Table 11 defines it and empty where
542/// it does not — the frontend refuses such a declaration, so the arm
543/// falls through to the discard it would otherwise silently become.
544fn narrow<T: CodecTarget>(trim: Vec<T::Stmt>, use_default: Vec<T::Stmt>) -> T::Stmt {
545 let mut discard = vec![refuse::<T>(Constant::StatusDiscarded), T::give_back(None)];
546 let mut default_arm = use_default;
547 default_arm.push(T::give_back(None));
548 let mut trim_arm = trim;
549 if trim_arm.is_empty() {
550 trim_arm = core::mem::take(&mut discard);
551 } else {
552 trim_arm.push(T::give_back(None));
553 }
554 // Written as nested `if`s rather than a `switch`: the generated C is
555 // compiled with `-Wswitch-enum` off but `-Wall -Wextra -Werror` on,
556 // and a `switch` over an enum without a `default` is one warning
557 // away from breaking every build the moment the enum grows.
558 T::when(
559 T::binary(
560 "==",
561 name::<T>("zd_tc"),
562 T::constant(Constant::TryConstructTrim),
563 ),
564 T::block(trim_arm),
565 Some(T::block(vec![T::when(
566 T::binary(
567 "==",
568 name::<T>("zd_tc"),
569 T::constant(Constant::TryConstructUseDefault),
570 ),
571 T::block(default_arm),
572 Some(T::block(if discard.is_empty() {
573 vec![refuse::<T>(Constant::StatusDiscarded), T::give_back(None)]
574 } else {
575 discard
576 })),
577 )])),
578 )
579}
580
581#[allow(clippy::too_many_lines)]
582fn body_of<T: CodecTarget>(
583 ast: &Ast,
584 id: Id<TypeNode>,
585 key: LayoutKey,
586 options: CodecOptions,
587 own_name: &str,
588) -> Result<Option<T::Block>, EncodeError> {
589 let Ok(node) = ast.type_(id) else {
590 return Ok(None);
591 };
592 let statements = match &node.kind {
593 TypeKind::Primitive(_) => {
594 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
595 return Err(EncodeError::NoWriter {
596 member: T::type_name(ast, id),
597 kind: "primitive the layout plan has no scalar write for",
598 });
599 };
600 vec![T::store(
601 deref::<T>("v"),
602 T::value_type(ast, id),
603 primitive_read_at::<T>(write),
604 )]
605 }
606
607 TypeKind::String {
608 width: CharWidth::Char,
609 bound,
610 } => {
611 let mut statements = vec![assign::<T>(
612 deref::<T>("v"),
613 call_expr::<T>("read_string_alloc", Vec::new()),
614 )];
615 statements.extend(string_fit::<T>(ast, id, bound, CharWidth::Char)?);
616 statements
617 }
618 TypeKind::String {
619 width: CharWidth::WChar,
620 bound,
621 } => {
622 let mut statements = vec![assign::<T>(
623 deref::<T>("v"),
624 match scalar_write(ast, id, key, WcharPosition::Member) {
625 Some(ScalarWrite::Wstr {
626 policy,
627 order,
628 code_units,
629 }) => match code_units {
630 WstringCodeUnits::Utf8 => call_expr::<T>(
631 "read_wstring_utf8_octets",
632 vec![T::constant(wstring_policy(policy))],
633 ),
634 WstringCodeUnits::Utf16 => call_expr::<T>(
635 "read_wstring_utf8",
636 vec![
637 T::constant(wstring_policy(policy)),
638 T::constant(wstring_unit_order(order)),
639 ],
640 ),
641 },
642 _ => {
643 return Err(EncodeError::NoWriter {
644 member: T::type_name(ast, id),
645 kind: "wstring the layout plan has no scalar write for",
646 });
647 }
648 },
649 )];
650 statements.extend(string_fit::<T>(ast, id, bound, CharWidth::WChar)?);
651 statements
652 }
653
654 TypeKind::Fixed { .. } => {
655 let Some(ScalarWrite::Fixed {
656 digits,
657 scale,
658 count,
659 }) = scalar_write(ast, id, key, WcharPosition::Member)
660 else {
661 return Err(EncodeError::NoWriter {
662 member: T::type_name(ast, id),
663 kind: "fixed the layout plan has no scalar write for",
664 });
665 };
666 vec![assign::<T>(
667 deref::<T>("v"),
668 call_expr::<T>(
669 "read_fixed",
670 vec![
671 int::<T>(i128::from(digits)),
672 int::<T>(i128::from(scale)),
673 T::constant(match count {
674 FixedDigitCount::Declared => Constant::FixedDeclared,
675 FixedDigitCount::ValuePrecision => Constant::FixedValuePrecision,
676 FixedDigitCount::ValueStringDigits => Constant::FixedValueStringDigits,
677 }),
678 ],
679 ),
680 )]
681 }
682
683 TypeKind::Enumeration { .. } => enumeration_body::<T>(ast, id, key),
684 TypeKind::Bitmask { .. } => bitmask_body::<T>(ast, id),
685 TypeKind::Bitset { .. } => {
686 // The whole chain, base-first: see `encode_c`.
687 let fields = &ast.all_bitfields_of(id);
688 let mut deps = DepCollector::default();
689 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
690 .map_err(crate::encode::plan_refusal)?;
691 let Extent::Sized(anchor) = layout.extent else {
692 return Err(EncodeError::NoWriter {
693 member: own_name.to_string(),
694 kind: "bitset whose anchor the planner does not size",
695 });
696 };
697 let read = call_expr::<T>("read_bitset", vec![int::<T>(i128::from(anchor))]);
698 // A bitset of padding alone still has its anchor on the wire,
699 // and no field to read out of it: the octets are read and not
700 // kept, where a local would be one no statement uses.
701 let named = fields.iter().any(|field| {
702 ast.bitfield(*field)
703 .is_ok_and(|field_node| field_node.name.is_some())
704 });
705 let mut statements = if named {
706 vec![T::declare(T::scalar(Scalar::UInt64), "zd_bits", Some(read))]
707 } else {
708 vec![T::drop_value(read)]
709 };
710 let mut position: u32 = 0;
711 for field in fields {
712 let Ok(field_node) = ast.bitfield(*field) else {
713 continue;
714 };
715 let width = u32::from(field_node.bits);
716 if position.saturating_add(width) > 64 {
717 return Err(EncodeError::NoWriter {
718 member: own_name.to_string(),
719 kind: "bitset wider than the sixty-four bits an anchor holds",
720 });
721 }
722 // An anonymous field is padding: it moves the next
723 // field along and carries no value of its own.
724 if let Some(field_name) = field_node.name.as_ref() {
725 let mask: u64 = if width >= 64 {
726 u64::MAX
727 } else {
728 (1u64 << width) - 1
729 };
730 statements.push(assign::<T>(
731 arrow::<T>("v", &T::escape(field_name)),
732 // Into the member's own width and not into a
733 // `uint64_t`: the anchor is sixty-four bits and
734 // the field is as wide as `c_holder` made it,
735 // so the narrowing is a decision written out
736 // here rather than a conversion the compiler
737 // makes quietly. The mask above has already
738 // taken everything the field cannot hold.
739 cast::<T>(
740 T::unsigned_holder(field_node.bits),
741 T::paren(T::binary(
742 "&",
743 T::paren(T::binary(
744 ">>",
745 name::<T>("zd_bits"),
746 // The shift distance in the anchor's
747 // own width. C and Java shift a
748 // 64-bit value by an `int` happily;
749 // a TypeScript `bigint` shifted by a
750 // `number` is a compile error, and
751 // the distance is a property of the
752 // anchor rather than of the field.
753 T::wide_unsigned(u64::from(position)),
754 )),
755 T::wide_unsigned(mask),
756 )),
757 ),
758 ));
759 }
760 position = position.saturating_add(width);
761 }
762 statements
763 }
764
765 TypeKind::Alias { aliased } => {
766 let Some(target) = aliased.resolved() else {
767 return Err(EncodeError::NoWriter {
768 member: own_name.to_string(),
769 kind: "typedef with an unresolved target",
770 });
771 };
772 // A typedef is transparent on the wire and the C typedef
773 // makes the two the same type, so the destination goes
774 // straight through.
775 // The behaviour goes through with the value: a typedef is
776 // transparent on the wire and transparent to §7.2.2.7 too,
777 // and the site that called this one is the site the bound
778 // belongs to.
779 let callee = decoder_name::<T>(ast, target);
780 // Unless the alias and its target are one decoder. A target
781 // that folds a `typedef` into the type it names gives the
782 // two the same decoder name — Zig's `pub const NodeRef =
783 // Node;` is the alias, so `Zig::type_name` answers `m_Node`
784 // for both — and a forwarder to itself is not a forwarder:
785 // `recursive-typedef-sequence` built
786 // `fn zd_dec_m_Node(..) { … zd_dec_m_Node(..) }` and the
787 // probe took SIGSEGV on the stack it ran out of. `None`
788 // leaves the name unclaimed and the aliased type emits its
789 // own body under it.
790 if callee == own_name {
791 return Ok(None);
792 }
793 vec![T::read_forward(&callee)]
794 }
795
796 TypeKind::Sequence { element, bound } => {
797 let Some(element) = element.resolved() else {
798 return Err(EncodeError::NoWriter {
799 member: own_name.to_string(),
800 kind: "sequence with an unresolved element",
801 });
802 };
803 sequence_body::<T>(ast, id, element, bound, key)?
804 }
805
806 TypeKind::Array { element, dims } => {
807 let Some(element) = element.resolved() else {
808 return Err(EncodeError::NoWriter {
809 member: own_name.to_string(),
810 kind: "array with an unresolved element",
811 });
812 };
813 let mut lengths = Vec::new();
814 for dim in dims {
815 lengths.push(ast.eval_integral(*dim).map_err(|_| EncodeError::NoWriter {
816 member: own_name.to_string(),
817 kind: "array whose length does not evaluate",
818 })?);
819 }
820 // Table 17 gives an array no member-level behaviour — its
821 // bounds must match the sender's, so only its elements can
822 // fail — and there is no template syntax for its element
823 // slot on a member declarator. `element_try_construct`
824 // therefore lets the enclosing member's own annotation
825 // reach the elements, and `zd_tc` is that annotation.
826 array_body::<T>(
827 ast,
828 id,
829 element,
830 &lengths,
831 key,
832 ast.element_try_construct(id, None),
833 )
834 }
835
836 TypeKind::Map {
837 key: k,
838 value,
839 bound,
840 } => {
841 let (Some(k), Some(value)) = (k.resolved(), value.resolved()) else {
842 return Err(EncodeError::NoWriter {
843 member: own_name.to_string(),
844 kind: "map with an unresolved key or value",
845 });
846 };
847 let mut deps = DepCollector::default();
848 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
849 .map_err(crate::encode::plan_refusal)?;
850 map_body::<T>(ast, id, k, value, bound, layout.needs_dheader, options)?
851 }
852
853 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
854 aggregate_body::<T>(ast, id, key)?
855 }
856
857 TypeKind::Union { discriminator, .. } => {
858 union_body::<T>(ast, id, key, discriminator.resolved(), own_name)?
859 }
860
861 // No wire form at all, so there is nothing to read and nothing
862 // is claimed.
863 TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
864 return Ok(None);
865 }
866 TypeKind::ValueBox { .. } => {
867 return Err(EncodeError::NoWriter {
868 member: own_name.to_string(),
869 kind: "boxed value, whose presence flag has no settled wire form",
870 });
871 }
872 // Refused by name rather than passed over. `any` is in the model
873 // because IDL 4.2 has it; DDS-XTypes has no `any` and no
874 // encoding for one, and only CORBA CDR writes it — as a TypeCode
875 // ahead of the value. Returning `Ok(None)` here would generate a
876 // codec that silently omitted the member.
877 TypeKind::Any => {
878 return Err(EncodeError::NoWriter {
879 member: own_name.to_string(),
880 kind: "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
881 as a TypeCode ahead of the value",
882 });
883 }
884 TypeKind::ValueBase => {
885 return Err(EncodeError::NoWriter {
886 member: own_name.to_string(),
887 kind: "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
888 one, as a chunked value with its repository id",
889 });
890 }
891 };
892 Ok(Some(T::block(statements)))
893}
894
895/// An enumeration, matched on the bit pattern its holder carries.
896///
897/// The pattern and not the value, because the pattern is what a holder
898/// narrower than the value's range leaves: Cyclone takes `@value(200)`
899/// under `@bit_bound(8)` and writes `c8`, which read as a signed Int8 is
900/// `-56`. Both readings name the same enumerator and only the pattern
901/// says so. `vendor_bytes.md`, `enum_bb8_200`.
902///
903/// A pattern no enumerator has is refused rather than handed back as a
904/// number: the value of an enumeration is a name, and there is no name
905/// for this.
906///
907/// # The pattern is not the value the binding holds
908///
909/// The two part company above the holder's sign bit, and the corpus
910/// caught it: `@value(200)` under `@bit_bound(8)` goes out as `c8`, and
911/// `zdw_read_enum` sign-extends that to `-56` because §7.4.3.5.3 makes
912/// the holder signed. The C binding stores the *declared* value, which
913/// is what the fill side assigns and what the encoder writes, so a
914/// decoder assigning the sign-extended pattern produces an integer no
915/// enumerator constant equals — `enum-bitbound-8-value` came back as
916/// `<enumerator -56>` where `BLUE` went in.
917///
918/// The Rust decoder never meets this: it matches on the pattern and
919/// returns a name, so it has no in-memory integer to disagree about.
920/// Each arm here therefore assigns the constant the materialiser
921/// declared, by name, which is the same one the fill side uses.
922fn enumeration_body<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, key: LayoutKey) -> Vec<T::Stmt> {
923 // The reader takes the same axis the writer took, through the one
924 // crossing function. `zerodds_ast_layout::enumeration_bit_bound`.
925 let bits = zerodds_ast_layout::plan::enumeration_holder_bits(ast, id, key);
926 let mut statements = vec![T::declare(
927 T::scalar(Scalar::Int64),
928 "zd_raw",
929 Some(call_expr::<T>(
930 "read_enum",
931 vec![int::<T>(i128::from(bits))],
932 )),
933 )];
934
935 // A pattern no enumerator has cannot construct an object of this
936 // type — Table 18: "If the name or value of the T2 object does not
937 // exist in T1, the object cannot construct any object of type T1" —
938 // so it is the *position's* behaviour that decides. TRIM has no
939 // meaning on an enumeration (Table 11) and the frontend refuses to
940 // write it there, so the trim arm is empty and falls through to the
941 // discard rather than inventing a nearest enumerator.
942 let default_arm = crate::emit::default_enumerator_name(ast, id)
943 .map(|literal| T::enumerator_constant(ast, id, &literal))
944 .map(|constant| vec![assign::<T>(deref::<T>("v"), constant)])
945 .unwrap_or_default();
946 // Built from the last arm backwards, so the chain reads in
947 // declaration order and the first enumerator with a given pattern
948 // wins — which is the one the Rust reader also names.
949 let mut chain: Option<T::Block> = Some(T::block(vec![narrow::<T>(Vec::new(), default_arm)]));
950 let mut arms = Vec::new();
951 // The same mapping the writer takes, from the same place: the
952 // pattern on the wire is the enumerator's wire value and the
953 // constant assigned is its declared one, and neither side resolves
954 // it for itself.
955 let resolved = zerodds_ast_layout::plan::enumerator_writes(ast, id, key).unwrap_or_default();
956 for (name, value) in resolved {
957 let Some(pattern) = enumerator_pattern(value, bits) else {
958 continue;
959 };
960 arms.push((pattern, T::enumerator_constant(ast, id, &name)));
961 }
962 for (pattern, constant) in arms.into_iter().rev() {
963 chain = Some(T::block(vec![T::when(
964 T::binary("==", name::<T>("zd_raw"), T::wide_signed(pattern)),
965 T::block(vec![assign::<T>(deref::<T>("v"), constant)]),
966 chain,
967 )]));
968 }
969 if let Some(chain) = chain {
970 statements.extend(T::into_statements(chain));
971 }
972 statements
973}
974
975/// The signed value `zdw_read_enum` returns for an enumerator.
976///
977/// A second copy of the writer's rule and deliberately not a call into
978/// it: the two directions are compared against each other, and a shared
979/// helper would make them agree about a mistake. §7.4.3.5.3 makes the
980/// holder signed, so the top bit of the bound reads back negative.
981fn enumerator_pattern(value: i128, bits: u16) -> Option<i128> {
982 if bits == 0 || bits > 32 {
983 return None;
984 }
985 let width = i128::from(bits);
986 if value < -(1i128 << (width - 1)) || value > (1i128 << width) - 1 {
987 return None;
988 }
989 let masked = value & ((1i128 << width) - 1);
990 Some(if masked >= (1i128 << (width - 1)) {
991 masked - (1i128 << width)
992 } else {
993 masked
994 })
995}
996
997/// A bitmask, reduced to the bits the type declares.
998///
999/// §7.4.1.1.3.2: "the states of the remaining serialized bits are not
1000/// specified, and those bits are not considered to be part of the
1001/// bitmask", and Table 18: "Preserve bit values by index for all bits
1002/// identified in both T1 and T2". A bit the stream sets and the type does
1003/// not name is not part of the value this type reads, so the read keeps
1004/// only the declared positions.
1005fn bitmask_body<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>) -> Vec<T::Stmt> {
1006 let bound = ast.effective_bit_bound(id);
1007 let mut declared: u64 = 0;
1008 for bit in ast.bits_of(id) {
1009 let Some(position) = ast.effective_bit_position(*bit) else {
1010 continue;
1011 };
1012 if position < bound && position < 64 {
1013 declared |= 1u64 << position;
1014 }
1015 }
1016 vec![
1017 T::declare(
1018 T::scalar(Scalar::UInt64),
1019 "zd_bits",
1020 Some(T::paren(T::binary(
1021 "&",
1022 call_expr::<T>("read_bitmask", vec![int::<T>(i128::from(bound))]),
1023 // The holder's own width and not a bare number: both
1024 // sides of the `&` are sixty-four bits wide, and in a
1025 // target where that width is a separate type — a
1026 // TypeScript `bigint` — mixing it with a `number` is a
1027 // compile error rather than a wrong answer.
1028 T::wide_unsigned(declared),
1029 ))),
1030 ),
1031 // `T::bitmask_value` over `T::value_type` — the two are one cast
1032 // everywhere but TypeScript, whose `as` cannot narrow the
1033 // 64-bit read to a `number` holder; the trait doc carries it.
1034 // The value type and not `T::named(T::type_name(..))`: the two
1035 // are the same string in C, where a bitmask's type name *is* its
1036 // C type name, and are not the same anywhere else.
1037 // `Cpp::type_name` answers `a_b_Flags` — the tag a codec function
1038 // is named after — where the type is `::a::b::Flags`, so a cast
1039 // built from the tag named a type no C++ scope declares.
1040 assign::<T>(
1041 deref::<T>("v"),
1042 T::bitmask_value(ast, id, name::<T>("zd_bits")),
1043 ),
1044 ]
1045}
1046
1047/// The limit a bound states, where it states one this column can read.
1048///
1049/// `None` for the unbounded form and for a bound that does not
1050/// evaluate; a negative one is not a length and is refused the same way.
1051/// One function because a `sequence`, a `string` and a `map` all read
1052/// their bound the same, and three copies would be three chances for
1053/// one collection to stop being checked while the others were.
1054fn bound_limit(ast: &Ast, bound: &zerodds_ast::Bound) -> Option<i128> {
1055 bound
1056 .expr()
1057 .and_then(|expr| ast.eval_integral(expr).ok())
1058 .filter(|limit| *limit >= 0)
1059}
1060
1061/// A sequence: the count, then the elements into storage the reader
1062/// allocated.
1063fn sequence_body<T: CodecTarget>(
1064 ast: &Ast,
1065 id: Id<TypeNode>,
1066 element: Id<TypeNode>,
1067 bound: &zerodds_ast::Bound,
1068 key: LayoutKey,
1069) -> Result<Vec<T::Stmt>, EncodeError> {
1070 let delimited = zerodds_ast_layout::collection_is_delimited(ast, element, key);
1071 let element_behaviour = ast.element_try_construct(id, None);
1072 let mut statements = Vec::new();
1073 if delimited {
1074 statements.push(discard_dheader::<T>());
1075 }
1076 statements.push(T::declare(
1077 T::scalar(Scalar::UInt32),
1078 "zd_n",
1079 Some(call_expr::<T>("read_u32", Vec::new())),
1080 ));
1081 // A count is one element or more, so a stream with fewer bytes left
1082 // than elements cannot be honest about it. Checked before the
1083 // allocation, which is how a reader turns a malformed message into
1084 // an out-of-memory when it is not.
1085 statements.push(T::when(
1086 T::binary(
1087 ">",
1088 cast::<T>(T::scalar(Scalar::Size), name::<T>("zd_n")),
1089 remaining::<T>(),
1090 ),
1091 T::block(vec![
1092 refuse::<T>(Constant::StatusImpossibleLength),
1093 T::give_back(None),
1094 ]),
1095 None,
1096 ));
1097 // The bound is applied after every element has been read and not
1098 // before: stopping at the bound would leave the rest of the
1099 // sequence in the stream and put every following member at the
1100 // wrong offset. `zd_over` is what the narrowing at the end reads.
1101 // §7.2.2.7, and OpenDDS does the same and then skips to the
1102 // collection's end.
1103 let limit = bound_limit(ast, bound);
1104 if let Some(limit) = limit {
1105 statements.push(T::declare(
1106 T::scalar(Scalar::Bool),
1107 "zd_over",
1108 Some(T::binary(">", name::<T>("zd_n"), T::unsigned_int(limit))),
1109 ));
1110 }
1111 let element_type = T::value_type(ast, element);
1112 // A run of octets is read in one piece where the target has the call
1113 // for it ([`CodecTarget::bulk_octets_read`]); the same octets as the
1114 // loop, with no per-element call.
1115 // The mirror of the writer: a `sequence` element of a non-`@final`
1116 // type is preceded by its own octet length under
1117 // `SequenceElementDelimiter::BeforeEachNonFinalElement`, and
1118 // `ElementPadding::Packed` counts every element's alignment from the
1119 // element's own first octet.
1120 let packed = key.element_padding == ElementPadding::Packed;
1121 let carries_length = crate::emit::encode::sequence_element_carries_length(ast, element, key)?;
1122 let fill = match crate::emit::encode::bulk_read::<T>(ast, element, name::<T>("zd_n")) {
1123 Some(bulk) => bulk,
1124 None => {
1125 // The element's place, and whatever the target needs around
1126 // it. See [`CodecTarget::sequence_element`]: every target but
1127 // one answers with the subscript and nothing else.
1128 let (before, place, after) =
1129 T::sequence_element("v", name::<T>("zd_i"), element_type.clone());
1130 let mut body: Vec<T::Stmt> = Vec::new();
1131 if packed {
1132 body.push(begin_packed::<T>());
1133 }
1134 if carries_length {
1135 body.extend(read_element_length::<T>());
1136 }
1137 body.extend(before);
1138 body.push(decode_element_into::<T>(
1139 ast,
1140 element,
1141 place,
1142 element_behaviour,
1143 key,
1144 ));
1145 body.extend(after);
1146 if carries_length {
1147 body.push(close_element_length::<T>());
1148 }
1149 if packed {
1150 body.push(finish_packed::<T>());
1151 }
1152 counted_loop::<T>("zd_i", name::<T>("zd_n"), T::block(body))
1153 }
1154 };
1155 statements.extend(T::make_room_of(
1156 ast,
1157 element,
1158 "v",
1159 element_type,
1160 name::<T>("zd_n"),
1161 true,
1162 vec![fill],
1163 ));
1164 if let Some(limit) = limit {
1165 statements.push(T::when(
1166 name::<T>("zd_over"),
1167 T::block(vec![narrow::<T>(
1168 // §7.3.1.2.1.12.3 keeps "the first four elements".
1169 // How a target says so is its own: C sets the
1170 // `_length` beside the buffer and leaves the elements
1171 // past the bound in it, unreferenced; a target whose
1172 // collection owns its storage has to shorten the
1173 // collection, and `v.size() = 5u` is not an
1174 // assignment at all.
1175 T::keep_first("v", T::unsigned_int(limit)),
1176 // Table 9, through this column's one derivation of
1177 // it, which for a sequence is the empty one.
1178 vec![T::default_into(ast, id, deref::<T>("v"))?],
1179 )]),
1180 None,
1181 ));
1182 }
1183 Ok(statements)
1184}
1185
1186/// The narrowing a bounded string needs once it has been read.
1187///
1188/// Nothing at all for an unbounded one: `zdw_read_string_alloc` has
1189/// already produced the value and there is no bound to miss.
1190fn string_fit<T: CodecTarget>(
1191 ast: &Ast,
1192 id: Id<TypeNode>,
1193 bound: &zerodds_ast::Bound,
1194 width: CharWidth,
1195) -> Result<Vec<T::Stmt>, EncodeError> {
1196 let Some(limit) = bound_limit(ast, bound) else {
1197 return Ok(Vec::new());
1198 };
1199 // Octets for a narrow string and UTF-16 code units for a wide one,
1200 // which is what the bound means to every peer that has to hold the
1201 // value — `string<5>` is `char[6]` in the C mapping of all four
1202 // measured implementations.
1203 //
1204 // Both are the runtime's: the measure has no receiver and the trim
1205 // takes one, which is exactly the difference between
1206 // [`CodecTarget::runtime_static`] and [`CodecTarget::runtime`]. They
1207 // were spelled `zdw_utf8_octets` and `zdw_trim_utf8` through
1208 // [`CodecTarget::call`] until a second reader column asked for them,
1209 // and `zdw_` is not a name any other runtime answers to.
1210 let (measure, trim) = match width {
1211 CharWidth::Char => ("utf8_octets", "trim_utf8"),
1212 CharWidth::WChar => ("utf16_units", "trim_utf16"),
1213 };
1214 // `(size_t)` written as a cast and not inside the literal, so that a
1215 // target with no C-style cast writes its own. C renders the two the
1216 // same.
1217 //
1218 // The number goes through `unsigned_int` and not through a
1219 // `format!("{limit}u")`: the `u` suffix is C's spelling of an
1220 // unsigned literal, and a walk that wrote it reached `32u` into a
1221 // Java probe, where `javac` calls it a syntax error. Every target
1222 // spells its own.
1223 let fits = || cast::<T>(T::scalar(Scalar::Size), T::unsigned_int(limit));
1224 let too_long = T::binary(
1225 ">",
1226 T::runtime_static(measure, vec![deref::<T>("v")]),
1227 fits(),
1228 );
1229 Ok(vec![T::when(
1230 // The value may not be there at all in a target whose reader
1231 // allocates and can fail. Where it cannot, the guard is not
1232 // written rather than written against a value that is always
1233 // present: `std::string != std::nullopt` does not compile.
1234 match T::materialised(deref::<T>("v")) {
1235 Some(present) => T::binary("&&", present, too_long),
1236 None => too_long,
1237 },
1238 T::block(vec![narrow::<T>(
1239 vec![assign::<T>(
1240 deref::<T>("v"),
1241 T::runtime("r", trim, vec![deref::<T>("v"), fits()]),
1242 )],
1243 // Table 9, through the one derivation of it this column
1244 // has. `default_c::try_construct_seeds` is what makes
1245 // sure the function exists.
1246 vec![T::default_into(ast, id, deref::<T>("v"))?],
1247 )]),
1248 None,
1249 )])
1250}
1251
1252/// An array: the element count is in the type, so only the elements are
1253/// on the wire. §7.4.3.5 treats a multi-dimensional array as one flat
1254/// run with the first index varying most slowly, which is what nested
1255/// loops in written order already are.
1256fn array_body<T: CodecTarget>(
1257 ast: &Ast,
1258 id: Id<TypeNode>,
1259 element: Id<TypeNode>,
1260 lengths: &[i128],
1261 key: LayoutKey,
1262 element_behaviour: TryConstruct,
1263) -> Vec<T::Stmt> {
1264 let _ = id;
1265 let delimited = zerodds_ast_layout::array_is_delimited(ast, element, key);
1266 if let [length] = lengths {
1267 if let Some(bulk) =
1268 crate::emit::encode::bulk_read::<T>(ast, element, T::unsigned_int(*length))
1269 {
1270 let mut statements = Vec::new();
1271 if delimited {
1272 statements.push(discard_dheader::<T>());
1273 }
1274 statements.extend(T::open_array("v", lengths));
1275 statements.push(bulk);
1276 statements.extend(T::close_array("v", lengths));
1277 return statements;
1278 }
1279 }
1280 let mut access = name::<T>("v");
1281 for level in 0..lengths.len() {
1282 access = T::index(access, name::<T>(&format!("zd_i{level}")));
1283 }
1284 let mut inner = T::block(vec![decode_element_into::<T>(
1285 ast,
1286 element,
1287 access,
1288 element_behaviour,
1289 key,
1290 )]);
1291 // `ElementPadding::Packed`: each element of every dimension is its
1292 // own alignment block, as in the reference's `read_array_dimension`.
1293 let packed = key.element_padding == ElementPadding::Packed;
1294 for (level, length) in lengths.iter().enumerate().rev() {
1295 let mut body: Vec<T::Stmt> = Vec::new();
1296 if packed {
1297 body.push(begin_packed::<T>());
1298 }
1299 body.extend(T::into_statements(inner));
1300 if packed {
1301 body.push(finish_packed::<T>());
1302 }
1303 inner = T::block(vec![counted_loop::<T>(
1304 &format!("zd_i{level}"),
1305 T::unsigned_int(*length),
1306 T::block(body),
1307 )]);
1308 }
1309 let mut statements = Vec::new();
1310 if delimited {
1311 statements.push(discard_dheader::<T>());
1312 }
1313 statements.extend(T::open_array("v", lengths));
1314 statements.extend(T::into_statements(inner));
1315 statements.extend(T::close_array("v", lengths));
1316 statements
1317}
1318
1319/// A map: §7.4.1.1.4's sequence of entries, in whichever of the two
1320/// readings the caller chose.
1321///
1322/// Under `rule-15-omits` a delimited map states no count, and the
1323/// delimiter's extent is the only thing that says where the entries
1324/// stop. The generated code then reads the entries twice: once into a
1325/// scratch entry to count them, then again into storage sized from that
1326/// count. `zdw_reader_restore` puts back both the cursor and the heap,
1327/// so the first pass leaves nothing behind.
1328///
1329/// This is the only reader mark left in the generated code. The other
1330/// one was the `@optional` guess, which rule (20) and §7.4.1.2 retired.
1331///
1332/// The bound is checked exactly as [`sequence_body`] checks a
1333/// sequence's, and for the same clauses: §7.2.2.7 makes an entry count
1334/// over the bound an invalid sample, and §7.3.1.2.1.12.3's TRIM keeps
1335/// the first `bound` entries. It is applied after every entry has been
1336/// read and not before: stopping at the bound would leave the rest of
1337/// the map in the stream and put every following member at the wrong
1338/// offset.
1339///
1340/// It was missing entirely: `map<long, long, 4>` read as many entries
1341/// as the stream stated in every one of the twenty columns, so a
1342/// sample the IDL does not describe reached the application with
1343/// nothing said about it. `sequence<long, 4>` beside it in the same
1344/// struct was checked, which is what made the gap visible.
1345///
1346/// # Errors
1347/// [`EncodeError`] when this column cannot spell Table 9's value for
1348/// the map — the same refusal a bounded sequence's TRIM raises.
1349fn map_body<T: CodecTarget>(
1350 ast: &Ast,
1351 id: Id<TypeNode>,
1352 key_type: Id<TypeNode>,
1353 value_type_: Id<TypeNode>,
1354 bound: &zerodds_ast::Bound,
1355 delimited: bool,
1356 options: CodecOptions,
1357) -> Result<Vec<T::Stmt>, EncodeError> {
1358 // Table 17 gives a map's key no escape — a key that cannot be
1359 // constructed fails the whole map — and the frontend refuses
1360 // anything but DISCARD there. The value has its own slot.
1361 let key_behaviour = ast.key_try_construct(id);
1362 let value_behaviour = ast.element_try_construct(id, None);
1363 let entry_type = T::map_entry_type(ast, id);
1364 let counted = !delimited || options.map_length_with_delimiter;
1365 let mut statements = Vec::new();
1366
1367 if delimited {
1368 statements.push(T::declare(
1369 T::scalar(Scalar::UInt32),
1370 "zd_dl",
1371 Some(call_expr::<T>("read_dheader", Vec::new())),
1372 ));
1373 statements.push(T::declare(
1374 T::scalar(Scalar::Size),
1375 "zd_end",
1376 Some(offset_plus::<T>(position::<T>(), name::<T>("zd_dl"))),
1377 ));
1378 }
1379
1380 let read_entry = |index: T::Expr| {
1381 T::block(vec![
1382 decode_into::<T>(
1383 ast,
1384 key_type,
1385 T::entry_half(T::index(T::elements("v"), index.clone()), true),
1386 key_behaviour,
1387 ),
1388 decode_into::<T>(
1389 ast,
1390 value_type_,
1391 T::entry_half(T::index(T::elements("v"), index), false),
1392 value_behaviour,
1393 ),
1394 ])
1395 };
1396
1397 if counted {
1398 statements.push(T::declare(
1399 T::scalar(Scalar::UInt32),
1400 "zd_n",
1401 Some(call_expr::<T>("read_u32", Vec::new())),
1402 ));
1403 } else {
1404 // The counting pass. A scratch entry rather than a discard,
1405 // because an entry may hold a string and the decoder has to
1406 // have somewhere to put one.
1407 statements.push(T::storage(entry_type.clone(), "zd_scratch"));
1408 statements.push(T::declare(
1409 T::handle(Handle::ReaderMark),
1410 "zd_mark",
1411 Some(call_expr::<T>("reader_save", Vec::new())),
1412 ));
1413 statements.push(T::declare(
1414 T::scalar(Scalar::UInt32),
1415 "zd_n",
1416 Some(T::unsigned_int(0)),
1417 ));
1418 statements.push(T::until(
1419 T::binary(
1420 "&&",
1421 T::binary("<", position::<T>(), name::<T>("zd_end")),
1422 status_is_ok::<T>(),
1423 ),
1424 T::block(vec![
1425 decode_into::<T>(
1426 ast,
1427 key_type,
1428 T::entry_half(name::<T>("zd_scratch"), true),
1429 key_behaviour,
1430 ),
1431 decode_into::<T>(
1432 ast,
1433 value_type_,
1434 T::entry_half(name::<T>("zd_scratch"), false),
1435 value_behaviour,
1436 ),
1437 assign::<T>(
1438 name::<T>("zd_n"),
1439 T::binary("+", name::<T>("zd_n"), T::unsigned_int(1)),
1440 ),
1441 ]),
1442 ));
1443 statements.push(T::perform(call_expr::<T>(
1444 "reader_restore",
1445 vec![name::<T>("zd_mark")],
1446 )));
1447 }
1448
1449 statements.push(T::when(
1450 T::binary(
1451 ">",
1452 cast::<T>(T::scalar(Scalar::Size), name::<T>("zd_n")),
1453 remaining::<T>(),
1454 ),
1455 T::block(vec![
1456 refuse::<T>(Constant::StatusImpossibleLength),
1457 T::give_back(None),
1458 ]),
1459 None,
1460 ));
1461 // The entry count against the bound, read before the entries are
1462 // and acted on after them. `zd_over` is the same local a bounded
1463 // sequence declares, because it is the same rule over the same
1464 // count.
1465 let limit = bound_limit(ast, bound);
1466 if let Some(limit) = limit {
1467 statements.push(T::declare(
1468 T::scalar(Scalar::Bool),
1469 "zd_over",
1470 Some(T::binary(">", name::<T>("zd_n"), T::unsigned_int(limit))),
1471 ));
1472 }
1473 statements.extend(T::make_room(
1474 "v",
1475 entry_type,
1476 name::<T>("zd_n"),
1477 false,
1478 vec![counted_loop::<T>(
1479 "zd_i",
1480 name::<T>("zd_n"),
1481 read_entry(name::<T>("zd_i")),
1482 )],
1483 ));
1484 // Ahead of the narrowing below and not after it: every arm of
1485 // [`narrow`] leaves the routine, so a map that had to be trimmed
1486 // would return with the cursor short of the delimiter's end and put
1487 // every following member at the wrong offset. The cursor is put
1488 // where the delimiter says it belongs first, and what the sample
1489 // holds is decided after.
1490 if delimited {
1491 statements.push(T::perform(call_expr::<T>(
1492 "reader_skip_to",
1493 vec![name::<T>("zd_end")],
1494 )));
1495 }
1496 if let Some(limit) = limit {
1497 statements.push(T::when(
1498 name::<T>("zd_over"),
1499 T::block(vec![narrow::<T>(
1500 // §7.3.1.2.1.12.3 keeps "the first four elements", and
1501 // an entry is what a map's element is (§7.4.1.1.4).
1502 // How a column shortens the collection is its own; the
1503 // walk says how many to keep.
1504 T::keep_first("v", T::unsigned_int(limit)),
1505 // Table 9, through this column's one derivation of it,
1506 // which for a map is the empty one.
1507 vec![T::default_into(ast, id, deref::<T>("v"))?],
1508 )]),
1509 None,
1510 ));
1511 }
1512 Ok(statements)
1513}
1514
1515/// One member of an aggregate, as the plan and the model together see
1516/// it.
1517struct Slot<T: CodecTarget> {
1518 field: String,
1519 type_: Id<TypeNode>,
1520 optional: bool,
1521 /// Whether the member is `@external` — stored behind the column's
1522 /// own indirection, where it keeps one. Table 21: no octet changes;
1523 /// which seam the access routes through does.
1524 external: bool,
1525 /// Whether the member's value has no room inside the aggregate that
1526 /// declares it — [`zerodds_ast_layout::member_held_inline_closes_a_cycle`].
1527 /// A column whose member is the value has to keep an indirection
1528 /// there whatever the annotations say.
1529 indirect: bool,
1530 /// Whether the column keeps the member behind a reference of its
1531 /// own making — [`CodecTarget::holds_by_reference`].
1532 by_reference: bool,
1533 member_id: u32,
1534 header: MemberHeader,
1535 /// The declaration, for the `@default` it may carry.
1536 member: Id<zerodds_ast::MemberNode>,
1537 /// The behaviour governing this member's value, already resolved
1538 /// through the alias chain of its type. §7.2.2.7.
1539 try_construct: TryConstruct,
1540 /// The statements that assign this member's explicit `@default`,
1541 /// when it has one and its behaviour is `USE_DEFAULT`.
1542 ///
1543 /// The type's decoder cannot answer that combination: it knows
1544 /// Table 9's value for the type and not the literal this member's
1545 /// declaration gave it, and §7.2.2.4.4.4.9 puts the literal first.
1546 /// So the call site asks for DISCARD and assigns the literal where
1547 /// it gets one.
1548 ///
1549 /// `None` where the member has no `@default` or does not take
1550 /// `USE_DEFAULT`; a literal this column cannot spell is a refusal
1551 /// and never a `None`.
1552 explicit_default: Option<Vec<T::Stmt>>,
1553}
1554
1555fn slots<T: CodecTarget>(
1556 ast: &Ast,
1557 id: Id<TypeNode>,
1558 layout: &zerodds_ast_layout::TypeLayout,
1559) -> Result<Vec<Slot<T>>, EncodeError> {
1560 let all = ast.all_members_of(id);
1561 let mut out = Vec::with_capacity(layout.members.len());
1562 for member in &layout.members {
1563 let mut found = None;
1564 for candidate in &all {
1565 let Ok(node) = ast.member(*candidate) else {
1566 continue;
1567 };
1568 if node.node_id == member.member {
1569 found = Some((*candidate, node));
1570 break;
1571 }
1572 }
1573 let Some((handle, node)) = found else {
1574 continue;
1575 };
1576 let Some(member_type) = node.type_.resolved() else {
1577 continue;
1578 };
1579 let try_construct = ast.effective_try_construct(
1580 node.facets.xtypes.as_ref().and_then(|x| x.try_construct),
1581 Some(member_type),
1582 );
1583 // Where the member is reached from: the aggregate, or the base that
1584 // declares it where the column nests the base as `parent`.
1585 let reach = super::reach::<T>(ast, id, member.member, &node.name);
1586 let explicit_default = if try_construct == TryConstruct::UseDefault
1587 && node
1588 .facets
1589 .xtypes
1590 .as_ref()
1591 .is_some_and(|x| x.default_value.is_some())
1592 {
1593 // Reported and not swallowed: a `@default` this column
1594 // cannot spell would otherwise leave the member taking
1595 // Table 9's zero, which is a different sample from the one
1596 // the designer declared.
1597 Some(T::member_default(
1598 ast,
1599 node,
1600 member_type,
1601 arrow::<T>("v", &reach.field),
1602 )?)
1603 } else {
1604 None
1605 };
1606 let optional = node.facets.is_optional();
1607 let external = super::reached_externally::<T>(ast, node);
1608 out.push(Slot::<T> {
1609 indirect: super::behind_pointer::<T>(
1610 ast,
1611 reach.holder,
1612 member_type,
1613 external,
1614 optional,
1615 ),
1616 by_reference: T::holds_by_reference(ast, reach.holder, member_type, optional, external),
1617 field: reach.field,
1618 type_: member_type,
1619 optional,
1620 external,
1621 member_id: member.member_id,
1622 header: member.header,
1623 member: handle,
1624 try_construct,
1625 explicit_default,
1626 });
1627 }
1628 Ok(out)
1629}
1630
1631/// The reader's destination for one slot's member, through the seams
1632/// its annotations and its type pick: `@external` members go through
1633/// [`CodecTarget::external_destination`], `@optional` ones through
1634/// [`CodecTarget::optional_destination`], and a plain member is its
1635/// field — and any of the three goes on through
1636/// [`CodecTarget::make_indirection`] where the member's value has no
1637/// room inside the aggregate that declares it.
1638fn slot_destination<T: CodecTarget>(ast: &Ast, slot: &Slot<T>) -> (Vec<T::Stmt>, T::Expr) {
1639 if slot.by_reference {
1640 return T::reference_destination(ast, slot.type_, "v", &slot.field);
1641 }
1642 let (mut setup, place) = if slot.external {
1643 T::external_destination("v", &slot.field, slot.optional)
1644 } else if slot.optional {
1645 T::optional_destination("v", &slot.field)
1646 } else {
1647 (Vec::new(), arrow::<T>("v", &slot.field))
1648 };
1649 // A member with no room inside its own aggregate is reached through
1650 // the column's indirection, and the indirection points nowhere until
1651 // this makes it point somewhere.
1652 if slot.indirect {
1653 let (made, through) = T::make_indirection(ast, slot.type_, place);
1654 setup.extend(made);
1655 return (setup, through);
1656 }
1657 (setup, place)
1658}
1659
1660/// Where a slot's Table 9 default goes, and what it takes to have
1661/// somewhere to put it.
1662///
1663/// The same seam [`slot_destination`] uses, for the write a *missing*
1664/// member gets: a column that keeps an indirection has nothing to
1665/// default into until the indirection points somewhere. `place` is the
1666/// answer for every member the column holds inline, which is every
1667/// member this does not report.
1668fn slot_default_place<T: CodecTarget>(
1669 ast: &Ast,
1670 slot: &Slot<T>,
1671 place: T::Expr,
1672) -> (Vec<T::Stmt>, T::Expr) {
1673 if slot.indirect {
1674 return T::make_indirection(ast, slot.type_, place);
1675 }
1676 (Vec::new(), place)
1677}
1678
1679/// Say a slot's `@optional` member is present or absent, through the
1680/// external seam where the member is also `@external`.
1681fn slot_set_present<T: CodecTarget>(slot: &Slot<T>, present: bool) -> Vec<T::Stmt> {
1682 // Held by reference: the allocation `reference_destination` makes
1683 // is what makes the member present, and absence is the reference's
1684 // own null.
1685 if slot.by_reference {
1686 return if present {
1687 Vec::new()
1688 } else {
1689 T::reference_absent("v", &slot.field)
1690 };
1691 }
1692 if slot.external {
1693 return T::external_set_present("v", &slot.field, present);
1694 }
1695 T::set_present("v", &slot.field, present)
1696}
1697
1698/// A structure or a valuetype, framed as its plan says.
1699///
1700/// Three shapes, and which one applies is the extensibility, read the
1701/// same way the planner reads it rather than inferred back out of the
1702/// plan it produced.
1703fn aggregate_body<T: CodecTarget>(
1704 ast: &Ast,
1705 id: Id<TypeNode>,
1706 key: LayoutKey,
1707) -> Result<Vec<T::Stmt>, EncodeError> {
1708 let mut deps = DepCollector::default();
1709 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1710 .map_err(crate::encode::plan_refusal)?;
1711 let members = slots::<T>(ast, id, &layout)?;
1712 let extensibility = ast.effective_extensibility(id);
1713 let mutable = extensibility == Extensibility::Mutable;
1714
1715 let mut statements = Vec::new();
1716 // `ElementPadding::Packed`: an aggregate is its own alignment block
1717 // on the read side too. See the writer's `write_aggregate`.
1718 let packed = key.element_padding == ElementPadding::Packed;
1719 if packed {
1720 statements.push(begin_packed::<T>());
1721 }
1722 if layout.needs_dheader {
1723 statements.extend(read_delimiter::<T>());
1724 }
1725 // An `@optional` member's presence flag is set by this decoder and
1726 // never left at whatever the caller's storage held.
1727 for slot in &members {
1728 if slot.optional {
1729 statements.extend(slot_set_present::<T>(slot, false));
1730 }
1731 }
1732
1733 if mutable && key.encoding.is_v1() {
1734 statements.extend(parameter_list::<T>(ast, &members)?);
1735 } else if mutable {
1736 if !layout.needs_dheader {
1737 return Err(EncodeError::NoLayout {
1738 detail: "a mutable type under XCDR2 with no DHEADER planned".to_string(),
1739 });
1740 }
1741 statements.extend(member_headers::<T>(ast, &members)?);
1742 } else {
1743 let tail = Tail {
1744 delimited: layout.needs_dheader,
1745 appendable: extensibility == Extensibility::Appendable,
1746 };
1747 statements.extend(plan_order::<T>(ast, &members, tail)?);
1748 }
1749
1750 if layout.needs_dheader {
1751 statements.extend(close_delimiter::<T>());
1752 }
1753 if packed {
1754 statements.push(finish_packed::<T>());
1755 }
1756 Ok(statements)
1757}
1758
1759/// `uint32_t zd_dl = …; size_t zd_end = position + zd_dl;`
1760fn read_delimiter<T: CodecTarget>() -> Vec<T::Stmt> {
1761 // Two statements and not one expression: C leaves the order of
1762 // evaluation between the call and `zdw_reader_position` unspecified,
1763 // so an end computed in one line would depend on the compiler.
1764 vec![
1765 T::declare(
1766 T::scalar(Scalar::UInt32),
1767 "zd_dl",
1768 Some(call_expr::<T>("read_dheader", Vec::new())),
1769 ),
1770 T::declare(
1771 T::scalar(Scalar::Size),
1772 "zd_end",
1773 Some(offset_plus::<T>(position::<T>(), name::<T>("zd_dl"))),
1774 ),
1775 ]
1776}
1777
1778/// What the delimiter is checked against once the members are read.
1779///
1780/// The tail inside a delimiter is what a peer with fewer members than
1781/// the writer had leaves behind, and skipping it is what the DHEADER is
1782/// for.
1783///
1784/// It used to carry a second check as well: bytes left inside the
1785/// delimiter after every declared member had been read falsified a
1786/// guess that an `@optional` member was absent. There is no guess left
1787/// to falsify — rule (20) and §7.4.1.2 mark the gap and this column
1788/// reads the marker — so the check has nothing to catch and the
1789/// `zd_guessed` flag it needed is gone with it.
1790fn close_delimiter<T: CodecTarget>() -> Vec<T::Stmt> {
1791 let statements = vec![T::when(
1792 T::binary(">", position::<T>(), name::<T>("zd_end")),
1793 T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
1794 None,
1795 )];
1796 let mut statements = statements;
1797 statements.push(T::perform(call_expr::<T>(
1798 "reader_skip_to",
1799 vec![name::<T>("zd_end")],
1800 )));
1801 statements
1802}
1803
1804/// The local a body's packed-element origin is saved in.
1805const PACKED_SAVED: &str = "zd_saved";
1806
1807/// `size_t zd_saved = reader_begin_packed_element(r);`
1808///
1809/// [`ElementPadding::Packed`] is HDDS's measured departure from §7.4.1:
1810/// a collection element and an aggregate value count their alignment
1811/// from their own first octet instead of from the encapsulation. The
1812/// generated codec holds the displaced origin and restores it with
1813/// [`finish_packed`]. The `reader_` prefix is the runtime's own, as for
1814/// `reader_save` and `reader_position`, and it is what keeps the read
1815/// direction's pair distinct from the write direction's.
1816fn begin_packed<T: CodecTarget>() -> T::Stmt {
1817 T::declare(
1818 T::scalar(Scalar::Size),
1819 PACKED_SAVED,
1820 Some(call_expr::<T>("reader_begin_packed_element", Vec::new())),
1821 )
1822}
1823
1824/// `reader_finish_packed_element(r, zd_saved);`
1825fn finish_packed<T: CodecTarget>() -> T::Stmt {
1826 T::perform(call_expr::<T>(
1827 "reader_finish_packed_element",
1828 vec![name::<T>(PACKED_SAVED)],
1829 ))
1830}
1831
1832/// The element's own octet length a `sequence` element of a non-`@final`
1833/// type carries.
1834///
1835/// [`zerodds_ast_layout::key::SequenceElementDelimiter::BeforeEachNonFinalElement`]
1836/// writes a four-octet `UInt32` in front of every element. The reader
1837/// checks the element against it rather than skipping to it: an element
1838/// longer or shorter than its own declaration is a stream that disagrees
1839/// with itself, and seeking past it would hide the disagreement.
1840fn read_element_length<T: CodecTarget>() -> Vec<T::Stmt> {
1841 vec![
1842 T::declare(
1843 T::scalar(Scalar::UInt32),
1844 "zd_ell",
1845 Some(call_expr::<T>("read_u32", Vec::new())),
1846 ),
1847 T::declare(
1848 T::scalar(Scalar::Size),
1849 "zd_elend",
1850 Some(offset_plus::<T>(position::<T>(), name::<T>("zd_ell"))),
1851 ),
1852 ]
1853}
1854
1855fn close_element_length<T: CodecTarget>() -> T::Stmt {
1856 ends_where_the_header_said::<T>("zd_elend")
1857}
1858
1859/// A `@final` or `@appendable` aggregate: the members in plan order.
1860///
1861/// Every member is where the plan says it is, including an `@optional`
1862/// one. §7.4.3.5.2 rule (20) puts a BOOLEAN `<is_present>` in front of
1863/// it under XCDR2 and §7.4.1.2 gives it a parameter header with a
1864/// declared length of zero under XCDR1, and all three implementations
1865/// write both — so nothing here is guessed and nothing rewinds.
1866///
1867/// This used to read the header against a `zdw_reader_mark` and rewind
1868/// when the member id was not the one expected. Against a stream that
1869/// marks the gap that guess reads the marker as a header, calls the
1870/// member absent, and takes the *next* member's bytes out of this
1871/// member's place.
1872fn plan_order<T: CodecTarget>(
1873 ast: &Ast,
1874 members: &[Slot<T>],
1875 tail: Tail,
1876) -> Result<Vec<T::Stmt>, EncodeError> {
1877 let mut statements = Vec::new();
1878 // `!members.is_empty()`, because the latch is read only inside the
1879 // loop below and a declaration nothing reads is a
1880 // `-Wunused-variable` error in a probe compiled with `-Werror`. An
1881 // aggregate with no members reaches this only in a column whose
1882 // materialiser can declare one: C refuses `struct S {}` outright —
1883 // C23 §6.7.2.1p8 leaves a structure with no named member undefined —
1884 // and C++ declares it, so the C column never saw the shape.
1885 if tail.possible() && !members.is_empty() {
1886 statements.push(T::declare(
1887 T::scalar(Scalar::Bool),
1888 "zd_tail",
1889 Some(literal::<T>("false")),
1890 ));
1891 }
1892 for (index, slot) in members.iter().enumerate() {
1893 let place = arrow::<T>("v", &slot.field);
1894 if tail.possible() {
1895 // Once the writer's members have run out, every member from
1896 // here on takes its default rather than the bytes of
1897 // whatever follows. Latched, so a member that happens to
1898 // start at the delimiter's end does not un-set it.
1899 //
1900 // The latch half is left off the **first** member: the
1901 // declaration above sets `zd_tail` to `false` on the line
1902 // before, so `!zd_tail` there is true by construction. It
1903 // is not a style point. Elixir threads a rebinding out of a
1904 // block through a tuple, which is enough for its type
1905 // checker to flow-type the literal and report the guard as
1906 // a conditional that always holds — a warning the codec
1907 // gate compiles with warnings as errors. The member reads
1908 // stay uniform, which is what the shared walk is for; only
1909 // the one conjunct that cannot be false is gone.
1910 let reached = if index == 0 {
1911 T::paren(tail.reached::<T>())
1912 } else {
1913 T::binary(
1914 "&&",
1915 T::unary("!", name::<T>("zd_tail")),
1916 T::paren(tail.reached::<T>()),
1917 )
1918 };
1919 statements.push(T::when(
1920 reached,
1921 T::block(vec![assign::<T>(
1922 name::<T>("zd_tail"),
1923 literal::<T>("true"),
1924 )]),
1925 None,
1926 ));
1927 let mut defaulted = Vec::new();
1928 if slot.optional {
1929 defaulted.extend(slot_set_present::<T>(slot, false));
1930 } else {
1931 let node = ast
1932 .member(slot.member)
1933 .map_err(|error| EncodeError::NoLayout {
1934 detail: format!("{}: {error}", slot.field),
1935 })?;
1936 let (room, into) = slot_default_place::<T>(ast, slot, place.clone());
1937 defaulted.extend(room);
1938 defaulted.extend(T::member_default(ast, node, slot.type_, into)?);
1939 }
1940 let read = plan_order_member::<T>(ast, slot, index, place)?;
1941 statements.push(T::when(
1942 name::<T>("zd_tail"),
1943 T::block(defaulted),
1944 Some(T::block(read)),
1945 ));
1946 continue;
1947 }
1948 statements.extend(plan_order_member::<T>(ast, slot, index, place)?);
1949 }
1950 Ok(statements)
1951}
1952
1953/// Where a positional aggregate's members stop, which is not always
1954/// where this reader's plan stops.
1955///
1956/// §7.2.4.4.8, Table 19: "Each non-optional member in a T1 object that
1957/// is not present in the T2 object takes its default value." The two
1958/// ways a writer's members can run out are the two shapes §7.2.4.2
1959/// gives an appendable type — delimited under encoding version 2, and
1960/// not delimited under version 1.
1961///
1962/// `@final` gets neither. Table 19 requires the same set of member IDs
1963/// on both sides of an assignment between final types, so a short stream
1964/// is a type mismatch: Cyclone and RTI refuse it and Fast DDS defaults,
1965/// measured 2026-08-09 as `evo-final-tail`, and the clause is with the
1966/// two.
1967#[derive(Debug, Clone, Copy)]
1968struct Tail {
1969 /// Whether the type carries a DHEADER, which says where the writer
1970 /// stopped.
1971 delimited: bool,
1972 /// Whether the aggregate is `@appendable`.
1973 appendable: bool,
1974}
1975
1976impl Tail {
1977 /// Whether this aggregate can meet a writer with fewer members.
1978 const fn possible(self) -> bool {
1979 self.appendable
1980 }
1981
1982 /// The C condition that says the writer's members have run out.
1983 ///
1984 /// Under XCDR2 the DHEADER is exactly that statement. Under XCDR1
1985 /// there is none, and all three vendors default the tail when the
1986 /// bytes run out instead — `evo-appendable-tail` under `xcdr1`,
1987 /// where Cyclone, RTI and Fast DDS each hand back `c = 0`. The test
1988 /// is `remaining() == 0` and not "fewer bytes than the member
1989 /// needs": an appendable type nested inside another still has the
1990 /// enclosing type's bytes behind it, so nothing fires and the read
1991 /// fails loudly further on, which is what Cyclone and RTI do with
1992 /// the nested case.
1993 fn reached<T: CodecTarget>(self) -> T::Expr {
1994 if self.delimited {
1995 return T::binary(">=", position::<T>(), name::<T>("zd_end"));
1996 }
1997 // A plain zero and not [`CodecTarget::unsigned_int`]'s count:
1998 // `reader_remaining` answers in the target's own size type —
1999 // `int` in the C#, Go, Java and F# runtimes, `usize` in Zig's —
2000 // and a count literal beside one is a comparison between two
2001 // widths. C# and Go let it through by promotion; F# does not,
2002 // and `r.ReaderRemaining() = 0u` is `error FS0001` there.
2003 T::binary("==", remaining::<T>(), T::int(0))
2004 }
2005}
2006
2007/// One member of a positional aggregate, read where the plan says.
2008fn plan_order_member<T: CodecTarget>(
2009 ast: &Ast,
2010 slot: &Slot<T>,
2011 index: usize,
2012 place: T::Expr,
2013) -> Result<Vec<T::Stmt>, EncodeError> {
2014 let mut statements = Vec::new();
2015 {
2016 match slot.header {
2017 MemberHeader::NoHeader => {
2018 if slot.optional {
2019 let (room, into) = slot_destination::<T>(ast, slot);
2020 statements.extend(room);
2021 statements.extend(member_read::<T>(ast, slot, into));
2022 statements.extend(slot_set_present::<T>(slot, true));
2023 } else if slot.external || slot.indirect {
2024 // `slot_destination` answers `place` itself for a
2025 // member the column holds inline, so this arm and
2026 // the one below differ only where the column keeps
2027 // something the read has to make first.
2028 let (room, into) = slot_destination::<T>(ast, slot);
2029 statements.extend(room);
2030 statements.extend(member_read::<T>(ast, slot, into));
2031 } else {
2032 statements.extend(member_read::<T>(ast, slot, place));
2033 }
2034 }
2035 MemberHeader::OptionalFlag => statements.extend(present_flag::<T>(ast, slot)),
2036 MemberHeader::OptionalCdr1 { .. } => {
2037 statements.extend(optional_parameter::<T>(ast, slot, index));
2038 }
2039 // The planner gives these two only to a member of a
2040 // `@mutable` type, which one of the two functions below
2041 // reads. A body that emitted them here would frame the
2042 // member one way and read it another.
2043 MemberHeader::EmHeader { .. } | MemberHeader::PidCdr1 { .. } => {
2044 return Err(EncodeError::NoLayout {
2045 detail: format!(
2046 "{}: an MMEMBER header in a type that is not `@mutable`",
2047 slot.field
2048 ),
2049 });
2050 }
2051 }
2052 }
2053 Ok(statements)
2054}
2055
2056/// Rule (20): the BOOLEAN `<is_present>`, then the value behind it.
2057///
2058/// The flag lands in a local and the branches say what the structure
2059/// holds, rather than the flag being assigned into the structure
2060/// directly: a target where absence *is* the value has nothing to
2061/// assign to — `v.x != null` is not somewhere a `boolean` can be put.
2062/// `zdw_read_bool` refuses a byte that is neither 0 nor 1, which is
2063/// what keeps a mis-framed stream from reading as "present".
2064fn present_flag<T: CodecTarget>(ast: &Ast, slot: &Slot<T>) -> Vec<T::Stmt> {
2065 let flag = format!("zd_has_{}", slot.field);
2066 let mut arrived = slot_set_present::<T>(slot, true);
2067 let (room, into) = slot_destination::<T>(ast, slot);
2068 arrived.extend(room);
2069 arrived.extend(member_read::<T>(ast, slot, into));
2070 vec![
2071 T::declare(
2072 T::scalar(Scalar::Bool),
2073 &flag,
2074 Some(call_expr::<T>("read_bool", Vec::new())),
2075 ),
2076 T::when(
2077 name::<T>(&flag),
2078 T::block(arrived),
2079 Some(T::block(slot_set_present::<T>(slot, false))),
2080 ),
2081 ]
2082}
2083
2084/// §7.4.1.2: the parameter header, whose declared length is zero when
2085/// the member took no value.
2086///
2087/// A header for another id is a refusal and not a rewind. The member is
2088/// not optional in the sense of "may be missing from the stream" — the
2089/// header is always there — so an id that is not this one means the
2090/// reader and the writer disagree about the type, and every byte after
2091/// it would come out of the wrong place.
2092fn optional_parameter<T: CodecTarget>(ast: &Ast, slot: &Slot<T>, index: usize) -> Vec<T::Stmt> {
2093 let header = format!("zd_p{index}");
2094 let flag = format!("zd_has_{}", slot.field);
2095 let mine = T::binary(
2096 "&&",
2097 T::paren(status_is_ok::<T>()),
2098 T::paren(T::binary(
2099 "&&",
2100 T::paren(T::binary(
2101 "==",
2102 T::header_field(name::<T>(&header), "pid"),
2103 T::unsigned_int(i128::from(slot.member_id)),
2104 )),
2105 T::paren(T::binary(
2106 "==",
2107 T::header_field(name::<T>(&header), "end"),
2108 literal::<T>("false"),
2109 )),
2110 )),
2111 );
2112 let mut arrived = slot_set_present::<T>(slot, true);
2113 let (room, into) = slot_destination::<T>(ast, slot);
2114 arrived.extend(room);
2115 arrived.extend(member_read::<T>(ast, slot, into));
2116 vec![
2117 T::declare(
2118 T::handle(Handle::PidRead),
2119 &header,
2120 Some(call_expr::<T>(
2121 "read_pid_as",
2122 vec![T::constant(Constant::ParameterOptional)],
2123 )),
2124 ),
2125 T::when(
2126 mine,
2127 T::block(vec![
2128 T::declare(
2129 T::scalar(Scalar::Bool),
2130 &flag,
2131 Some(T::binary(
2132 "!=",
2133 T::header_field(name::<T>(&header), "length"),
2134 T::unsigned_int(0),
2135 )),
2136 ),
2137 T::when(
2138 name::<T>(&flag),
2139 T::block(arrived),
2140 Some(T::block(slot_set_present::<T>(slot, false))),
2141 ),
2142 T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(&header)])),
2143 ]),
2144 Some(T::block(vec![
2145 refuse::<T>(Constant::StatusOutOfRange),
2146 T::give_back(None),
2147 ])),
2148 ),
2149 ]
2150}
2151
2152/// A `@mutable` aggregate under XCDR2: EMHEADERs until the delimiter's
2153/// extent is used up.
2154///
2155/// Driven by the headers and not by the plan, because that is what the
2156/// headers are for.
2157fn member_headers<T: CodecTarget>(
2158 ast: &Ast,
2159 members: &[Slot<T>],
2160) -> Result<Vec<T::Stmt>, EncodeError> {
2161 let mut statements = Vec::new();
2162 for (index, slot) in members.iter().enumerate() {
2163 let _ = slot;
2164 statements.push(T::declare(
2165 T::scalar(Scalar::Bool),
2166 &format!("zd_seen{index}"),
2167 Some(literal::<T>("false")),
2168 ));
2169 }
2170 statements.push(T::declare(T::handle(Handle::EmHeaderRead), "zd_h", None));
2171 statements.push(T::declare(
2172 T::scalar(Scalar::Size),
2173 "zd_ends",
2174 Some(literal::<T>("0")),
2175 ));
2176
2177 // The unknown-member arm, built first so the known ones can chain
2178 // in front of it. §7.4.3.5.3: a reader that cannot skip a member it
2179 // must understand may not pretend it did.
2180 let mut chain = Some(T::block(vec![T::when(
2181 T::header_field(name::<T>("zd_h"), "must_understand"),
2182 T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
2183 Some(T::block(vec![T::perform(call_expr::<T>(
2184 "reader_skip_to",
2185 vec![name::<T>("zd_ends")],
2186 ))])),
2187 )]));
2188
2189 for (index, slot) in members.iter().enumerate().rev() {
2190 let seen = format!("zd_seen{index}");
2191 // §7.4.3.4.2 gives each member one header, so a second one for
2192 // the same id is a stream that says two things.
2193 let mut arm = vec![T::when(
2194 name::<T>(&seen),
2195 T::block(vec![
2196 refuse::<T>(Constant::StatusOutOfRange),
2197 T::give_back(None),
2198 ]),
2199 None,
2200 )];
2201 arm.push(assign::<T>(name::<T>(&seen), literal::<T>("true")));
2202 if slot.optional {
2203 let (room, into) = slot_destination::<T>(ast, slot);
2204 arm.extend(room);
2205 arm.extend(member_read::<T>(ast, slot, into));
2206 arm.extend(slot_set_present::<T>(slot, true));
2207 } else {
2208 let (room, into) = slot_destination::<T>(ast, slot);
2209 arm.extend(room);
2210 arm.extend(member_read::<T>(ast, slot, into));
2211 }
2212 arm.push(ends_where_the_header_said::<T>("zd_ends"));
2213 chain = Some(T::block(vec![T::when(
2214 T::binary(
2215 "==",
2216 T::header_field(name::<T>("zd_h"), "member_id"),
2217 T::unsigned_int(i128::from(slot.member_id)),
2218 ),
2219 T::block(arm),
2220 chain,
2221 )]));
2222 }
2223
2224 let mut loop_body = vec![
2225 assign::<T>(
2226 name::<T>("zd_h"),
2227 call_expr::<T>("read_emheader", Vec::new()),
2228 ),
2229 T::when(
2230 T::binary(
2231 "!=",
2232 call_expr::<T>("reader_status", Vec::new()),
2233 T::constant(Constant::StatusOk),
2234 ),
2235 T::block(vec![T::leave()]),
2236 None,
2237 ),
2238 assign::<T>(
2239 name::<T>("zd_ends"),
2240 offset_plus::<T>(
2241 position::<T>(),
2242 T::header_field(name::<T>("zd_h"), "extent"),
2243 ),
2244 ),
2245 ];
2246 if let Some(chain) = chain {
2247 loop_body.extend(T::into_statements(chain));
2248 }
2249
2250 statements.push(T::until(
2251 T::binary(
2252 "&&",
2253 T::paren(T::binary("<", position::<T>(), name::<T>("zd_end"))),
2254 T::paren(status_is_ok::<T>()),
2255 ),
2256 T::block(loop_body),
2257 ));
2258 statements.extend(defaults_for_absent::<T>(ast, members)?);
2259 Ok(statements)
2260}
2261
2262/// A `@mutable` aggregate under XCDR1: parameters until `PID_LIST_END`.
2263fn parameter_list<T: CodecTarget>(
2264 ast: &Ast,
2265 members: &[Slot<T>],
2266) -> Result<Vec<T::Stmt>, EncodeError> {
2267 let mut statements = Vec::new();
2268 for index in 0..members.len() {
2269 statements.push(T::declare(
2270 T::scalar(Scalar::Bool),
2271 &format!("zd_seen{index}"),
2272 Some(literal::<T>("false")),
2273 ));
2274 }
2275 statements.push(T::declare(T::handle(Handle::PidRead), "zd_p", None));
2276 statements.push(T::declare(
2277 T::scalar(Scalar::Size),
2278 "zd_at",
2279 Some(literal::<T>("0")),
2280 ));
2281
2282 // Stepping over a parameter this type has no member for, in the one
2283 // place three different reasons reach it.
2284 let skip_parameter = T::block(vec![T::perform(call_expr::<T>(
2285 "reader_skip_to",
2286 vec![offset_plus::<T>(
2287 name::<T>("zd_at"),
2288 T::header_field(name::<T>("zd_p"), "length"),
2289 )],
2290 ))]);
2291 // A parameter this type does not declare. PL_CDR1 carries a length
2292 // on every parameter, so skipping is always possible — but
2293 // §7.2.2.4.4.4.6 says a reader that does not understand a
2294 // must-understand member shall discard the whole sample rather than
2295 // step over it. The XCDR2 arm below already refused; this one
2296 // skipped in silence.
2297 let unknown = T::block(vec![T::when(
2298 T::header_field(name::<T>("zd_p"), "must_understand"),
2299 T::block(vec![
2300 refuse::<T>(Constant::StatusOutOfRange),
2301 T::give_back(None),
2302 ]),
2303 Some(skip_parameter.clone()),
2304 )]);
2305 let mut chain = Some(unknown.clone());
2306
2307 for (index, slot) in members.iter().enumerate().rev() {
2308 let seen = format!("zd_seen{index}");
2309 let mut arm = vec![T::when(
2310 name::<T>(&seen),
2311 T::block(vec![
2312 refuse::<T>(Constant::StatusOutOfRange),
2313 T::give_back(None),
2314 ]),
2315 None,
2316 )];
2317 arm.push(assign::<T>(name::<T>(&seen), literal::<T>("true")));
2318 if slot.optional {
2319 let (room, into) = slot_destination::<T>(ast, slot);
2320 arm.extend(room);
2321 arm.extend(member_read::<T>(ast, slot, into));
2322 arm.extend(slot_set_present::<T>(slot, true));
2323 } else {
2324 let (room, into) = slot_destination::<T>(ast, slot);
2325 arm.extend(room);
2326 arm.extend(member_read::<T>(ast, slot, into));
2327 }
2328 chain = Some(T::block(vec![T::when(
2329 T::binary(
2330 "==",
2331 T::header_field(name::<T>("zd_p"), "pid"),
2332 T::unsigned_int(i128::from(slot.member_id)),
2333 ),
2334 T::block(arm),
2335 chain,
2336 )]));
2337 }
2338
2339 // §7.4.1.2.4: the list ends at the sentinel. A stream that runs out
2340 // first is one whose end nobody wrote.
2341 let mut loop_body = vec![
2342 T::when(
2343 T::binary("<", remaining::<T>(), T::int(4)),
2344 T::block(vec![refuse::<T>(Constant::StatusTruncated), T::leave()]),
2345 None,
2346 ),
2347 assign::<T>(name::<T>("zd_p"), call_expr::<T>("read_pid", Vec::new())),
2348 T::when(
2349 T::binary(
2350 "!=",
2351 call_expr::<T>("reader_status", Vec::new()),
2352 T::constant(Constant::StatusOk),
2353 ),
2354 T::block(vec![T::leave()]),
2355 None,
2356 ),
2357 T::when(
2358 T::header_field(name::<T>("zd_p"), "end"),
2359 T::block(vec![T::leave()]),
2360 None,
2361 ),
2362 assign::<T>(name::<T>("zd_at"), position::<T>()),
2363 ];
2364 // The same two gates the Rust and Python columns put in front of
2365 // the id chain, in the same order. `PID_IGNORE` first, because
2366 // §7.4.1.2.2 states no exception for the must-understand bit that
2367 // `unknown` would refuse on; then `zdw_pid_is_user_member`, which is
2368 // false for a vendor-private parameter and for a reserved id.
2369 if let Some(chain) = chain {
2370 loop_body.push(T::when(
2371 T::header_field(name::<T>("zd_p"), "ignore"),
2372 skip_parameter,
2373 Some(T::block(vec![T::when(
2374 T::runtime_static("pid_is_user_member", vec![name::<T>("zd_p")]),
2375 chain,
2376 Some(unknown),
2377 )])),
2378 ));
2379 }
2380 loop_body.push(T::perform(call_expr::<T>(
2381 "finish_pid_read",
2382 vec![name::<T>("zd_p")],
2383 )));
2384
2385 statements.push(T::until(status_is_ok::<T>(), T::block(loop_body)));
2386 statements.extend(defaults_for_absent::<T>(ast, members)?);
2387 Ok(statements)
2388}
2389
2390/// The members the stream did not carry, each taking its default.
2391///
2392/// §7.4.1.2.3: "any mutable structure member's value may be omitted.
2393/// For non-optional members this may be due to the sender having a
2394/// compatible type that does not have that member. In such a case, if
2395/// the receiver's member is not optional, it logically takes its
2396/// default value."
2397///
2398/// This column used to refuse with `ZDW_TRUNCATED`, which made a new
2399/// subscriber unable to receive from an old publisher — the exchange
2400/// `@mutable` exists for. Cyclone, RTI and Fast DDS all hand back the
2401/// default; measured 2026-08-09 as `evo-mutable-tail`,
2402/// `evo-mutable-middle` and `evo-mutable-defaults`, where all three
2403/// agree with Table 9 on every kind in it.
2404///
2405/// `@must_understand` makes no difference here, and that is measured
2406/// too: `evo-mutable-mustunderstand`, all three vendors, both encoding
2407/// versions. `crates/wire-probe/src/decode.rs::require_present` carries
2408/// the argument.
2409fn defaults_for_absent<T: CodecTarget>(
2410 ast: &Ast,
2411 members: &[Slot<T>],
2412) -> Result<Vec<T::Stmt>, EncodeError> {
2413 let mut statements = Vec::new();
2414 for (index, slot) in members.iter().enumerate() {
2415 // "If the member is optional, it takes no value at all." The
2416 // `_has_` flag was set false before the loop that reads, so
2417 // there is nothing to do.
2418 if slot.optional {
2419 continue;
2420 }
2421 let node = ast
2422 .member(slot.member)
2423 .map_err(|error| EncodeError::NoLayout {
2424 detail: format!("{}: {error}", slot.field),
2425 })?;
2426 let (room, into) = slot_default_place::<T>(ast, slot, arrow::<T>("v", &slot.field));
2427 let mut defaulted = room;
2428 defaulted.extend(T::member_default(ast, node, slot.type_, into)?);
2429 statements.push(T::when(
2430 T::binary(
2431 "==",
2432 name::<T>(&format!("zd_seen{index}")),
2433 literal::<T>("false"),
2434 ),
2435 T::block(defaulted),
2436 None,
2437 ));
2438 }
2439 Ok(statements)
2440}
2441
2442/// The member has to end where its header said it would.
2443fn ends_where_the_header_said<T: CodecTarget>(ends: &str) -> T::Stmt {
2444 T::when(
2445 T::binary("!=", position::<T>(), name::<T>(ends)),
2446 T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
2447 None,
2448 )
2449}
2450
2451/// A union: the discriminator, then the member its labels select.
2452///
2453/// A discriminator matching no label and a union with no `default:` is
2454/// a legal outcome and reads no member at all —
2455/// `union-default-unselected` in the corpus is that sample.
2456fn union_body<T: CodecTarget>(
2457 ast: &Ast,
2458 id: Id<TypeNode>,
2459 key: LayoutKey,
2460 discriminator: Option<Id<TypeNode>>,
2461 own_name: &str,
2462) -> Result<Vec<T::Stmt>, EncodeError> {
2463 let Some(discriminator) = discriminator else {
2464 return Err(EncodeError::NoWriter {
2465 member: own_name.to_string(),
2466 kind: "union with an unresolved discriminator",
2467 });
2468 };
2469 let mut deps = DepCollector::default();
2470 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
2471 .map_err(crate::encode::plan_refusal)?;
2472 // The one crossing [`UnionLabelMatch`] has. Under the default it is
2473 // false for every discriminator and the arm chain below is the
2474 // clause's, unchanged.
2475 let suppress_negative = zerodds_ast_layout::plan::union_negative_selects_nothing(ast, id, key);
2476
2477 let mut statements = Vec::new();
2478 // `ElementPadding::Packed`: a union is its own alignment block on the
2479 // read side too. See the writer's `write_union`.
2480 let packed = key.element_padding == ElementPadding::Packed;
2481 if packed {
2482 statements.push(begin_packed::<T>());
2483 }
2484 if layout.needs_dheader {
2485 statements.extend(read_delimiter::<T>());
2486 }
2487 // Rules (27) and (28): a `@mutable` union frames its discriminator
2488 // as an `MMEMBER` at member id 0, and the discriminator comes first
2489 // because nothing else says which member follows it.
2490 statements.extend(read_mmember::<T>(
2491 layout.discriminator,
2492 0,
2493 "zd_hd",
2494 // §7.3.1.2.1.12.5: the discriminator has a slot of its own,
2495 // and Table 19 admits only DEFAULT there.
2496 decode_into::<T>(
2497 ast,
2498 discriminator,
2499 T::discriminant("v"),
2500 ast.discriminator_try_construct(id),
2501 ),
2502 ));
2503
2504 // The default branch's member is still set from Table 9's default;
2505 // the branch the discriminator selects is the only one left live.
2506 // Every case, or the body is refused: a case skipped here keeps the
2507 // default value it was constructed with, and the decoded union then
2508 // shows a branch its discriminator did not select.
2509 let case_fields: Vec<String> = ast
2510 .cases_of(id)
2511 .iter()
2512 .map(|case| {
2513 ast.case(*case)
2514 .and_then(|case_node| ast.member(case_node.member))
2515 .map(|member| T::escape(&member.name))
2516 .map_err(|_| EncodeError::NoWriter {
2517 member: own_name.to_string(),
2518 kind: "union case the model does not hold",
2519 })
2520 })
2521 .collect::<Result<_, _>>()?;
2522 statements.extend(T::clear_union_cases("v", &case_fields));
2523
2524 // Whether the sentinel that ends the parameter list has already
2525 // been consumed, which happens where the writer's union carried no
2526 // member and this one's discriminator selects one.
2527 //
2528 // Declared wherever a **branch takes a parameter header**, and not
2529 // wherever the list has a sentinel. The two used to coincide —
2530 // `needs_sentinel` was `encoding.is_v1() && mutable`, which is
2531 // exactly when a branch gets a `PidCdr1` header — and
2532 // `MutableUnionFraming::BranchesFromZero` separates them: it
2533 // suppresses the sentinel and keeps the headers, and
2534 // `read_union_member` assigns `zd_sent` from the header's `end`
2535 // whatever the list ends with. Gating the declaration on the
2536 // sentinel left the assignment referring to a variable that does not
2537 // exist, and the C column did not compile.
2538 let takes_parameter_headers = layout
2539 .members
2540 .iter()
2541 .any(|plan| matches!(plan.header, MemberHeader::PidCdr1 { .. }));
2542 if takes_parameter_headers {
2543 statements.push(T::declare(
2544 T::scalar(Scalar::Bool),
2545 "zd_sent",
2546 Some(literal::<T>("false")),
2547 ));
2548 }
2549
2550 let mut arms: Vec<(T::Expr, T::Block)> = Vec::new();
2551 let mut fallback: Option<T::Block> = None;
2552 for case in ast.cases_of(id) {
2553 let Ok(case_node) = ast.case(*case) else {
2554 continue;
2555 };
2556 let Ok(member) = ast.member(case_node.member) else {
2557 continue;
2558 };
2559 let Some(member_type) = member.type_.resolved() else {
2560 return Err(EncodeError::NoWriter {
2561 member: member.name.clone(),
2562 kind: "union case with an unresolved type",
2563 });
2564 };
2565 let field = T::escape(&member.name);
2566 let planned = layout
2567 .members
2568 .iter()
2569 .find(|plan| plan.member == member.node_id);
2570 // Table 21: `@external` changes no octet, only where the binding
2571 // keeps the value — a column that keeps an indirection has to
2572 // make storage before there is a value to write into, exactly as
2573 // a structure member does.
2574 let external = super::reached_externally::<T>(ast, member);
2575 let (mut setup, place) = if external {
2576 T::external_case_destination("v", &field)
2577 } else {
2578 (Vec::new(), T::case_member("v", &field))
2579 };
2580 // A branch whose value has no room inside the union is reached
2581 // through the column's indirection, exactly as a structure
2582 // member is.
2583 let place = if super::behind_pointer::<T>(ast, id, member_type, external, false) {
2584 let (made, through) = T::make_indirection(ast, member_type, place);
2585 setup.extend(made);
2586 through
2587 } else {
2588 place
2589 };
2590 let mut arm_statements = setup;
2591 arm_statements.extend(read_union_member::<T>(
2592 planned.map(|plan| plan.header),
2593 planned.map_or(0, |plan| plan.member_id),
2594 &format!("zd_hu{}", planned.map_or(0, |plan| plan.member_id)),
2595 decode_into::<T>(
2596 ast,
2597 member_type,
2598 place.clone(),
2599 ast.effective_try_construct(
2600 member.facets.xtypes.as_ref().and_then(|x| x.try_construct),
2601 Some(member_type),
2602 ),
2603 ),
2604 // M11, §7.2.4.4.8, Table 19, the UNION_TYPE row: "If the
2605 // discriminator value does not select any member in O2,
2606 // then: If the discriminator value selects a member m1
2607 // in O1, then m1 is set to its default value according
2608 // to its type."
2609 T::default_into(ast, member_type, place)?,
2610 ));
2611 let read = T::block(arm_statements);
2612
2613 let mut condition: Option<T::Expr> = None;
2614 let mut is_default = false;
2615 for case_label in &case_node.labels {
2616 match case_label {
2617 CaseLabel::Default => is_default = true,
2618 CaseLabel::Value(expr) => {
2619 let value = ast
2620 .eval_integral(*expr)
2621 .map_err(|_| EncodeError::NoWriter {
2622 member: member.name.clone(),
2623 kind: "union label that does not evaluate",
2624 })?;
2625 // A label a setting says selects nothing gets no arm
2626 // at all, so the chain falls through to `default:`
2627 // where the union has one and past the whole union
2628 // where it has none — which is what the C peer this
2629 // reproduces does with its own real union.
2630 // `zerodds_ast_layout::union_negative_matches_nothing`.
2631 if suppress_negative && value < 0 {
2632 continue;
2633 }
2634 let test = T::binary(
2635 "==",
2636 T::discriminant("v"),
2637 T::case_label(ast, discriminator, value),
2638 );
2639 condition = Some(match condition {
2640 None => test,
2641 Some(previous) => T::binary("||", previous, test),
2642 });
2643 }
2644 }
2645 }
2646 if is_default {
2647 fallback = Some(read.clone());
2648 }
2649 if let Some(condition) = condition {
2650 arms.push((condition, read));
2651 }
2652 }
2653
2654 let mut chain: Option<T::Block> = fallback;
2655 for (condition, block) in arms.into_iter().rev() {
2656 chain = Some(T::block(vec![T::when(condition, block, chain)]));
2657 }
2658 if let Some(chain) = chain {
2659 statements.extend(T::into_statements(chain));
2660 }
2661
2662 // The C probe is built with `-Wall -Wextra -Werror`, so a variable
2663 // that is set and never read fails the build. Under a framing that
2664 // writes no sentinel nothing reads `zd_sent`, and `(void)` is what
2665 // says so out loud rather than leaving the declaration out and the
2666 // assignment dangling.
2667 if takes_parameter_headers && !layout.needs_sentinel {
2668 statements.extend(T::mark_used("zd_sent"));
2669 }
2670
2671 // Rule (28): the list ends at its sentinel, selected member or not
2672 // — unless the member's own read already consumed it, which is what
2673 // says the writer wrote no member at all.
2674 if layout.needs_sentinel {
2675 statements.push(T::when(
2676 T::unary("!", name::<T>("zd_sent")),
2677 T::block(vec![
2678 T::declare(
2679 T::handle(Handle::PidRead),
2680 "zd_pe",
2681 Some(call_expr::<T>("read_pid", Vec::new())),
2682 ),
2683 T::when(
2684 T::binary(
2685 "==",
2686 T::header_field(name::<T>("zd_pe"), "end"),
2687 literal::<T>("false"),
2688 ),
2689 T::block(vec![
2690 refuse::<T>(Constant::StatusOutOfRange),
2691 T::give_back(None),
2692 ]),
2693 None,
2694 ),
2695 ]),
2696 None,
2697 ));
2698 }
2699
2700 if layout.needs_dheader {
2701 statements.extend(close_delimiter::<T>());
2702 }
2703 if packed {
2704 statements.push(finish_packed::<T>());
2705 }
2706 Ok(statements)
2707}
2708
2709/// The member a union's discriminator selects, or its default where the
2710/// stream does not carry one.
2711///
2712/// M11. §7.2.4.4.8, Table 19, the UNION_TYPE row: "If the discriminator
2713/// value does not select any member in O2, then: If the discriminator
2714/// value selects a member m1 in O1, then m1 is set to its default value
2715/// according to its type." Two versions of a union whose newer one gave
2716/// an existing discriminator value a case is exactly that, and RTI is
2717/// the witness — `evo-union-unselected`, `{"_d":2,"b":0}` in both
2718/// encoding versions.
2719///
2720/// Which of the two the stream is saying is read off the **framing** and
2721/// never guessed. Under XCDR1 a `PID_LIST_END` where the member's
2722/// parameter would be is the writer saying it wrote none, and the
2723/// sentinel is consumed by reading it, so `zd_sent` tells the caller not
2724/// to read it a second time. Under XCDR2 the delimiter's extent says the
2725/// union is over. A `@final` union has neither, which is right: Table 19
2726/// requires two final unions to have an identical set of labels, so a
2727/// stream missing the member is a type mismatch and not an evolution.
2728fn read_union_member<T: CodecTarget>(
2729 header: Option<MemberHeader>,
2730 member_id: u32,
2731 handle: &str,
2732 value: T::Stmt,
2733 default: T::Stmt,
2734) -> Vec<T::Stmt> {
2735 match header {
2736 // Rule (28), the parameter list.
2737 Some(MemberHeader::PidCdr1 { .. }) => vec![
2738 T::declare(
2739 T::handle(Handle::PidRead),
2740 handle,
2741 Some(call_expr::<T>("read_pid", Vec::new())),
2742 ),
2743 T::when(
2744 T::header_field(name::<T>(handle), "end"),
2745 T::block(vec![
2746 default.clone(),
2747 assign::<T>(name::<T>("zd_sent"), literal::<T>("true")),
2748 ]),
2749 Some(T::block(vec![
2750 T::when(
2751 T::binary(
2752 "!=",
2753 T::header_field(name::<T>(handle), "pid"),
2754 T::unsigned_int(i128::from(member_id)),
2755 ),
2756 T::block(vec![
2757 refuse::<T>(Constant::StatusOutOfRange),
2758 T::give_back(None),
2759 ]),
2760 None,
2761 ),
2762 value.clone(),
2763 T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(handle)])),
2764 ])),
2765 ),
2766 ],
2767 // Rule (27) under a DHEADER.
2768 Some(MemberHeader::EmHeader { .. }) => vec![T::when(
2769 T::binary(">=", position::<T>(), name::<T>("zd_end")),
2770 T::block(vec![default]),
2771 Some(T::block(read_mmember::<T>(
2772 header, member_id, handle, value,
2773 ))),
2774 )],
2775 // No framing to read an absence off, so there is none.
2776 _ => read_mmember::<T>(header, member_id, handle, value),
2777 }
2778}
2779
2780/// One value behind the `MMEMBER` header the plan gave it, or bare.
2781///
2782/// `None` is a union of any extensibility but `@mutable`. The id is
2783/// checked and not trusted: a header for another member is not one this
2784/// decoder can step past, because the value behind it belongs to
2785/// something else and every byte after it would come out of the wrong
2786/// place.
2787fn read_mmember<T: CodecTarget>(
2788 header: Option<MemberHeader>,
2789 member_id: u32,
2790 handle: &str,
2791 value: T::Stmt,
2792) -> Vec<T::Stmt> {
2793 let wrong_id = |mine: T::Expr| {
2794 T::when(
2795 T::unary("!", T::paren(mine)),
2796 T::block(vec![
2797 refuse::<T>(Constant::StatusOutOfRange),
2798 T::give_back(None),
2799 ]),
2800 None,
2801 )
2802 };
2803 match header {
2804 None | Some(MemberHeader::NoHeader) => vec![value],
2805 Some(MemberHeader::PidCdr1 { .. }) => vec![
2806 T::declare(
2807 T::handle(Handle::PidRead),
2808 handle,
2809 Some(call_expr::<T>("read_pid", Vec::new())),
2810 ),
2811 wrong_id(T::binary(
2812 "&&",
2813 T::paren(T::binary(
2814 "==",
2815 T::header_field(name::<T>(handle), "pid"),
2816 T::unsigned_int(i128::from(member_id)),
2817 )),
2818 T::paren(T::binary(
2819 "==",
2820 T::header_field(name::<T>(handle), "end"),
2821 literal::<T>("false"),
2822 )),
2823 )),
2824 value,
2825 T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(handle)])),
2826 ],
2827 Some(MemberHeader::EmHeader { .. }) => vec![
2828 T::declare(
2829 T::handle(Handle::EmHeaderRead),
2830 handle,
2831 Some(call_expr::<T>("read_emheader", Vec::new())),
2832 ),
2833 wrong_id(T::binary(
2834 "==",
2835 T::header_field(name::<T>(handle), "member_id"),
2836 T::unsigned_int(i128::from(member_id)),
2837 )),
2838 value,
2839 ],
2840 // Neither `OPT_FMEMBER` form reaches a union: a case member is
2841 // selected by the discriminator and is never `@optional`.
2842 Some(MemberHeader::OptionalCdr1 { .. } | MemberHeader::OptionalFlag) => vec![value],
2843 }
2844}
2845
2846/// The call one scalar position reads with.
2847///
2848/// Every policy is already answered: `zerodds_ast_layout::plan::scalar_write`
2849/// resolved them against the key and the position, so nothing here
2850/// reads an axis and nothing here can pass the wrong argument to one.
2851/// The same instruction the writer takes, which is what makes the two
2852/// directions answerable against each other.
2853fn primitive_read_at<T: CodecTarget>(write: ScalarWrite) -> T::Expr {
2854 match write {
2855 ScalarWrite::Bool => call_expr::<T>("read_bool", Vec::new()),
2856 ScalarWrite::U8 => call_expr::<T>("read_u8", Vec::new()),
2857 ScalarWrite::I8 => call_expr::<T>("read_i8", Vec::new()),
2858 // The runtime hands back a code point, which is wider than the
2859 // C storage. The narrowing is written out so that it is a
2860 // decision in the generated code and not a conversion the
2861 // compiler makes quietly — and an IDL `char` is one octet, so
2862 // there is nothing above the low eight bits to lose.
2863 ScalarWrite::Char => cast::<T>(
2864 T::scalar(Scalar::Char),
2865 call_expr::<T>("read_char", Vec::new()),
2866 ),
2867 ScalarWrite::Wchar {
2868 width,
2869 length_octet,
2870 order,
2871 } => cast::<T>(
2872 T::scalar(Scalar::UInt16),
2873 call_expr::<T>(
2874 "read_wchar",
2875 vec![
2876 T::constant(match width {
2877 WcharWidth::Two => Constant::WcharWidth2,
2878 WcharWidth::Four => Constant::WcharWidth4,
2879 }),
2880 T::constant(if length_octet {
2881 Constant::WcharGiopLengthPrefixed
2882 } else {
2883 Constant::WcharFixed
2884 }),
2885 T::constant(wstring_unit_order(order)),
2886 ],
2887 ),
2888 ),
2889 ScalarWrite::I16 => call_expr::<T>("read_i16", Vec::new()),
2890 ScalarWrite::U16 => call_expr::<T>("read_u16", Vec::new()),
2891 ScalarWrite::I32 => call_expr::<T>("read_i32", Vec::new()),
2892 ScalarWrite::U32 => call_expr::<T>("read_u32", Vec::new()),
2893 ScalarWrite::I64 => call_expr::<T>("read_i64", Vec::new()),
2894 ScalarWrite::U64 => call_expr::<T>("read_u64", Vec::new()),
2895 ScalarWrite::F32 => call_expr::<T>("read_f32", Vec::new()),
2896 ScalarWrite::F64 => call_expr::<T>("read_f64", Vec::new()),
2897 ScalarWrite::F128 => call_expr::<T>("read_f128", Vec::new()),
2898 // The reader frames these itself: a string allocates, a fixed
2899 // takes its digits from the type, an enumeration reads a holder
2900 // and maps it back. They reach this function through their own
2901 // arms in `body_of`.
2902 ScalarWrite::Str
2903 | ScalarWrite::Wstr { .. }
2904 | ScalarWrite::Fixed { .. }
2905 | ScalarWrite::Enum { .. }
2906 | ScalarWrite::Bitmask { .. } => call_expr::<T>("read_u8", Vec::new()),
2907 }
2908}
2909
2910/// The C name of a `wstring` length policy.
2911const fn wstring_policy(policy: WstringLengthPolicy) -> Constant {
2912 match policy {
2913 WstringLengthPolicy::SPECIFICATION => Constant::WstringSpecification,
2914 WstringLengthPolicy::CHARACTER_COUNT => Constant::WstringCharacterCount,
2915 WstringLengthPolicy::CHARACTER_COUNT_TERMINATED => {
2916 Constant::WstringCharacterCountTerminated
2917 }
2918 WstringLengthPolicy::SPECIFICATION_WIDE_UNITS => Constant::WstringSpecificationWideUnits,
2919 WstringLengthPolicy::ROS_TYPE_SUPPORT => Constant::WstringRosTypeSupport,
2920 // The three cells no measured implementation writes. Named on
2921 // both sides so the C emitter can reach every cell the Rust
2922 // model can; a mapping that stopped at the measured five would
2923 // put the emitters back where the enumeration had them.
2924 _ => match (policy.counts, policy.terminator, policy.unit) {
2925 (WstringLengthCounts::Octets, _, WstringUnitWidth::Two) => {
2926 Constant::WstringOctetsTerminated2
2927 }
2928 (WstringLengthCounts::Octets, _, WstringUnitWidth::Four) => {
2929 Constant::WstringOctetsTerminated4
2930 }
2931 _ => Constant::WstringCharactersTerminated2,
2932 },
2933 }
2934}
2935
2936/// The C name of a `wstring` unit order.
2937const fn wstring_unit_order(order: WstringUnitOrder) -> Constant {
2938 match order {
2939 WstringUnitOrder::Stream => Constant::WstringUnitsStream,
2940 WstringUnitOrder::BigEndianAlways => Constant::WstringUnitsBigEndianAlways,
2941 WstringUnitOrder::ByteOrderMark => Constant::WstringUnitsByteOrderMark,
2942 WstringUnitOrder::HostUnswapped => Constant::WstringUnitsHostUnswapped,
2943 }
2944}
2945
2946// -- small builders ------------------------------------------------------
2947
2948/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2949/// is `T::name`; `dds_bounded_recursion` compares only the last
2950/// segment of a call path, so it reads as a self-call.
2951fn name<T: CodecTarget>(text: &str) -> T::Expr {
2952 T::name(text)
2953}
2954
2955/// An integer whose written form matters — a `u` suffix, a cast.
2956///
2957/// The suffixed forms are C's and are what this file has always
2958/// emitted; they stay here rather than in the backend because a target
2959/// that does not need them would not know what to drop.
2960///
2961/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2962/// is `T::literal`; `dds_bounded_recursion` compares only the last
2963/// segment of a call path, so it reads as a self-call.
2964fn literal<T: CodecTarget>(text: &str) -> T::Expr {
2965 T::literal(Literal::Int(text.to_string()))
2966}
2967
2968/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2969/// is `T::int`; `dds_bounded_recursion` compares only the last
2970/// segment of a call path, so it reads as a self-call.
2971fn int<T: CodecTarget>(value: i128) -> T::Expr {
2972 T::int(value)
2973}
2974
2975fn deref<T: CodecTarget>(text: &str) -> T::Expr {
2976 T::whole(text)
2977}
2978
2979fn arrow<T: CodecTarget>(value: &str, field: &str) -> T::Expr {
2980 T::member(value, field)
2981}
2982
2983/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2984/// is `T::cast`; `dds_bounded_recursion` compares only the last
2985/// segment of a call path, so it reads as a self-call.
2986fn cast<T: CodecTarget>(type_: T::Type, value: T::Expr) -> T::Expr {
2987 T::cast(type_, value)
2988}
2989
2990fn assign<T: CodecTarget>(place: T::Expr, value: T::Expr) -> T::Stmt {
2991 T::perform(T::binary("=", place, value))
2992}
2993
2994/// A runtime call on the reader.
2995///
2996/// `function_name` is the operation without the runtime's prefix —
2997/// `read_u32`, not `zdw_read_u32` — and the receiver is prepended by
2998/// the backend.
2999fn call_expr<T: CodecTarget>(function_name: &str, args: Vec<T::Expr>) -> T::Expr {
3000 T::runtime("r", function_name, args)
3001}
3002
3003/// The reader's offset into the stream.
3004///
3005/// # The offset domain
3006///
3007/// [`position`], [`remaining`] and the four locals this walk declares
3008/// with [`Scalar::Size`] — `zd_end`, `zd_ends`, `zd_at` and a map's
3009/// `zd_end` — are one width, and it is the target's own size type:
3010/// `size_t` in C, `int` in the C#, Go, Java and F# runtimes, `usize` in
3011/// Zig's, `Long` in Kotlin's. Nothing else is.
3012///
3013/// A DHEADER is a `uint32`, an EMHEADER's extent is a `uint32`, a PID's
3014/// length is a `uint16` and a sentinel's size is a literal — none of the
3015/// four is in the domain, and each has to be brought into it before it
3016/// is put beside one that is. Fifteen of the twenty columns widen the
3017/// narrower side themselves and never notice; F# and Kotlin convert
3018/// nothing implicitly and name a type error.
3019///
3020/// So the domain has exactly three entrances and no fourth:
3021///
3022/// - [`offset_plus`] for an addition — it casts the count;
3023/// - [`cast`] to [`Scalar::Size`] for a comparison against a count;
3024/// - [`CodecTarget::int`] and **not** [`CodecTarget::unsigned_int`] for a
3025/// literal, because `4u` is not a token in every language and is a
3026/// second width in the ones where it is.
3027///
3028/// Measured: `zd_ends <- r.ReaderPosition() + zd_h.Extent` was
3029/// `error FS0001` ("uint32 does not match int") in every F# run and
3030/// `assignment type mismatch: actual type is 'Long', but 'Int' was
3031/// expected` in every Kotlin XCDR2 run, and
3032/// `if r.ReaderRemaining() < 4u then` was the same error a second time
3033/// in the PL_CDR1 arm.
3034fn position<T: CodecTarget>() -> T::Expr {
3035 call_expr::<T>("reader_position", Vec::new())
3036}
3037
3038/// How many octets the reader has left, in the offset domain.
3039///
3040/// See [`position`] for what may be put beside it.
3041fn remaining<T: CodecTarget>() -> T::Expr {
3042 call_expr::<T>("reader_remaining", Vec::new())
3043}
3044
3045/// `base + count`, where `base` is in the offset domain and `count` is a
3046/// length the stream carried.
3047///
3048/// The cast is the whole point: see [`position`].
3049fn offset_plus<T: CodecTarget>(base: T::Expr, count: T::Expr) -> T::Expr {
3050 T::binary("+", base, cast::<T>(T::scalar(Scalar::Size), count))
3051}
3052
3053fn refuse<T: CodecTarget>(status: Constant) -> T::Stmt {
3054 T::perform(call_expr::<T>("reader_refuse", vec![T::constant(status)]))
3055}
3056
3057fn status_is_ok<T: CodecTarget>() -> T::Expr {
3058 T::binary(
3059 "==",
3060 call_expr::<T>("reader_status", Vec::new()),
3061 T::constant(Constant::StatusOk),
3062 )
3063}
3064
3065/// A DHEADER read and not kept.
3066///
3067/// A collection's delimiter says how long it is and the element count
3068/// says how many there are; the count is what the loop runs on, so the
3069/// length is read to move the cursor and nothing else. The same thing
3070/// the Rust reader does with it.
3071fn discard_dheader<T: CodecTarget>() -> T::Stmt {
3072 T::drop_value(call_expr::<T>("read_dheader", Vec::new()))
3073}
3074
3075/// A loop from zero to `count`, exclusive.
3076///
3077/// The backend's, and the same one the encoder uses. The reader spelled
3078/// its bounds `0u` and `1u` where the writer spelled them `0` and `1`;
3079/// two spellings of one loop in one language is a difference nothing
3080/// depended on and nobody chose.
3081fn counted_loop<T: CodecTarget>(index: &str, count: T::Expr, body: T::Block) -> T::Stmt {
3082 T::count_up(index, count, body)
3083}