zerodds_lang_rules/emit/encode.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Encoder bodies, built from the layout plan. Spec §5, §6.2.
4//!
5//! One walk, emitted through a [`CodecTarget`]. Which member is written,
6//! in what order, under which header and with which length code is
7//! decided here and once; how that is spelled is the backend's.
8//!
9//! The counterpart of [`crate::encode`], which does the same for Rust
10//! against the Rust CST directly and has not been brought over.
11//! Every framing decision is read from `zerodds-ast-layout` and none is
12//! re-derived: the plan is where the length-code policy and the XCDR2
13//! alignment rule live, and a second derivation would be a second place
14//! to get them wrong.
15//!
16//! What the emitted code does *not* delegate is the wire rule itself.
17//! Alignment, the string length convention, the UTF-16 count, the BCD
18//! packing, the enumerator's signed holder and the DHEADER, EMHEADER and
19//! PID framing are the target runtime's — `runtime/c`, `runtime/java`
20//! and the rest each implement them from the clause rather than calling
21//! the Rust one. That is what makes a column a second reading of the
22//! specification: where two disagree, one of them has misread it.
23//!
24//! ## What this walk assumes of every target
25//!
26//! - **A routine per type.** Rust's emitter names a writer method on the
27//! element type; C has no methods, so every type gets its own routine
28//! and collections call it by name. A target that would rather emit a
29//! method says so in [`CodecTarget::routine`].
30//! - **Forward declarations are the target's business.** C needs them
31//! ahead of the bodies so a recursive type does not depend on visit
32//! order; a target with no such thing answers `None` and the loop
33//! emits only bodies.
34//! - **A `switch` is an if-chain.** The union's branch is selected by
35//! comparing the discriminator against each case's labels, which every
36//! target can hold and which sidesteps the languages whose `switch`
37//! will not take a 64-bit label.
38
39use crate::adapter::c::{ordered_types, ordered_types_from, ordered_types_from_pruning};
40use crate::emit::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
41use crate::encode::EncodeError;
42use zerodds_ast::{Ast, Bound, CaseLabel, PrimitiveKind, TypeKind, TypeNode};
43use zerodds_ast_core::{Id, NodeId};
44use zerodds_ast_layout::key::{
45 ElementPadding, LayoutKey, PlCdr1HeaderPolicy, Purpose, WcharPosition, WcharWidth,
46 WstringCodeUnits, WstringLengthCounts, WstringLengthPolicy, WstringUnitOrder, WstringUnitWidth,
47};
48use zerodds_ast_layout::key::{FixedDigitCount, MapKeyOrdering};
49use zerodds_ast_layout::plan::{Extent, MemberHeader, ScalarWrite, scalar_write};
50use zerodds_ast_layout::xcdr::DepCollector;
51use zerodds_lang_cst::common::{Doc, Literal};
52
53/// The one framing question the layout plan does not answer.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct CodecOptions {
56 /// Whether a delimited `map` also carries its entry count.
57 ///
58 /// XTypes 1.3 says it twice and differently: §7.4.1.1.4 defines a
59 /// map by equivalence to a sequence, which has both a `DHEADER` and
60 /// an `O.length`, while rule (15) of §7.4.3.5.3 writes the
61 /// `DHEADER` and the pairs with no count. Both are decodable, so
62 /// neither can be ruled out from the stream alone, and the caller
63 /// chooses. `true` is the equivalence reading.
64 pub map_length_with_delimiter: bool,
65}
66
67impl Default for CodecOptions {
68 fn default() -> Self {
69 Self {
70 map_length_with_delimiter: true,
71 }
72 }
73}
74
75/// The parameter that carries a sample's RTI PL_CDR1 step decision down
76/// to a nested type's encoder.
77///
78/// RTI frames a sample under XCDR1 with the short parameter header until
79/// its type's maximum serialized size passes a step, and from there
80/// gives `PID_EXTENDED` to every member whose type is not a primitive or
81/// an enumeration. The step is a property of the **sample's** type and
82/// not of the type that declares a member, so a generated type cannot
83/// bake the answer in: the same nested `@mutable` type is framed short
84/// inside a small sample and extended inside a large one
85/// (`crate::emit::encode`'s `body_name` doc and
86/// `zerodds_ast_layout::key::PlCdr1HeaderPolicy::ExtendedForConstructedInLargeSamples`).
87/// The flag is threaded at run time; the sample's root supplies it.
88const EXTENDED_PARAM: &str = "zd_extended";
89
90/// Whether the plan's PL_CDR1 header form is decided per sample, so every
91/// generated encoder takes [`EXTENDED_PARAM`] and threads it to the
92/// types it calls.
93fn per_sample(key: LayoutKey) -> bool {
94 key.pl_cdr1_headers.decided_per_sample() && key.encoding.is_v1()
95}
96
97/// The name of the encoder **body** of `name`: the function that takes
98/// [`EXTENDED_PARAM`], or `name` itself when the form is fixed.
99///
100/// Under [`per_sample`] one type needs two functions. The body carries
101/// the sample's decision as a parameter, so one type's codec can frame
102/// its constructed members short in a small sample and extended in a
103/// large one; the public entry under `name` bakes the decision in for
104/// the sample whose root it is and calls the body. Without the split,
105/// the nested `Inner` of `crate::emit::encode`'s `Inner` row could not
106/// serve both its samples.
107fn body_name(name: &str, key: LayoutKey) -> String {
108 if per_sample(key) {
109 format!("{name}_zd_extended")
110 } else {
111 name.to_string()
112 }
113}
114
115/// A call to the encoder of a nested type: its body's name, and the
116/// sample's flag when [`per_sample`] holds.
117fn nested_call<T: CodecTarget>(
118 ast: &Ast,
119 id: Id<TypeNode>,
120 key: LayoutKey,
121 args: Vec<T::Expr>,
122) -> T::Expr {
123 nested_call_with::<T>(ast, id, key, args, T::name(EXTENDED_PARAM))
124}
125
126/// [`nested_call`] with the flag spelled by the caller.
127///
128/// The key holder passes the constant `false`. Under
129/// [`PlCdr1HeaderPolicy::ExtendedForConstructedInLargeSamples`] a key
130/// holder keeps the short form whatever the sample: the choice of
131/// `PID_EXTENDED` for a constructed member is a data-sample decision —
132/// [`LayoutKey::for_sample`] is asked for one — and the reflective
133/// planner keeps a key holder short for that same reason. The data path
134/// resolves the choice once at the root and hands it down; the holder
135/// has no root decision to hand down and states the constant.
136fn nested_call_with<T: CodecTarget>(
137 ast: &Ast,
138 id: Id<TypeNode>,
139 key: LayoutKey,
140 mut args: Vec<T::Expr>,
141 flag: T::Expr,
142) -> T::Expr {
143 let name = body_name(&member_codec_name::<T>(ast, id, key), key);
144 if per_sample(key) {
145 args.push(flag);
146 }
147 T::call(&name, args)
148}
149
150/// Whether a sample of `root` passes RTI's short-header step, the
151/// generation-time half of the per-sample decision.
152///
153/// The same count [`zerodds_ast_layout::key::LayoutKey::for_sample`]
154/// makes: a `decided_per_sample` policy becomes
155/// [`PlCdr1HeaderPolicy::ExtendedForConstructed`] when the sample's type
156/// can exceed the short form's length. `for_sample` is asked rather than
157/// `exceeds_short_length` directly so that the generated entry and the
158/// reflective path read one answer.
159fn sample_past_step(ast: &Ast, root: Id<TypeNode>, key: LayoutKey) -> Result<bool, EncodeError> {
160 let resolved = key
161 .for_sample(ast, root)
162 .map_err(crate::encode::plan_refusal)?;
163 Ok(resolved.pl_cdr1_headers == PlCdr1HeaderPolicy::ExtendedForConstructed)
164}
165
166/// The types a whole-model codec needs a public entry for: the named
167/// structures, unions and value types that are not `@nested`, which are
168/// what a caller can hold a sample of.
169///
170/// A type that is only ever a member is reached through its parent's
171/// body and needs no entry; emitting one would be a `static` function
172/// nothing calls, which every C consumer built with `-Werror` refuses.
173/// [`codec_from`] knows its root and does not ask this.
174pub(crate) fn sample_types(ast: &Ast) -> Vec<Id<TypeNode>> {
175 ast.types()
176 .filter(|(_, node)| {
177 !node.name.is_empty()
178 && matches!(
179 node.kind,
180 TypeKind::Structure { .. }
181 | TypeKind::Union { .. }
182 | TypeKind::Valuetype { .. }
183 )
184 && !node
185 .facets
186 .xtypes
187 .as_ref()
188 .is_some_and(|xtypes| xtypes.is_nested)
189 })
190 .map(|(id, _)| id)
191 .collect()
192}
193
194/// The body with `zd_extended` marked as read.
195///
196/// A primitive's or an enumeration's body has no parameter header and
197/// never reads the flag; the C probe is built with `-Wunused-parameter
198/// -Werror`, where that fails the build. `(void)zd_extended;` is valid
199/// whether or not the body goes on to use it, so it is emitted
200/// unconditionally rather than from a guess about which bodies read it —
201/// the same shape `crate::emit::decode::with_tc_used` uses.
202fn with_extended_used<T: CodecTarget>(body: T::Block) -> T::Block {
203 let mut statements = T::mark_used(EXTENDED_PARAM);
204 statements.extend(T::into_statements(body));
205 T::block(statements)
206}
207
208/// The prefix an encoder function takes, by what the plan is for.
209///
210/// A key holder is a different plan over the same types — only the
211/// `@key` members, big-endian — so it needs its own set of functions
212/// rather than a flag threaded through the data ones.
213const fn prefix(purpose: Purpose) -> &'static str {
214 match purpose {
215 Purpose::Data => "zd_enc_",
216 Purpose::Key => "zd_kenc_",
217 }
218}
219
220/// The encoder a member or element of `member_type` is written by,
221/// under the key its enclosing body was generated with.
222///
223/// Not `codec_name(ast, member_type, key.purpose)`. §7.6.8 Step 1
224/// removes the non-key members of a nested aggregate only "if there are
225/// any key members", and one dialect reads the consequence differently:
226/// under [`KeyHolderNesting::KeylessNestedWhole`] — RTI under XCDR1 —
227/// the subtree below a keyless aggregate stops being filtered at all
228/// and is taken as the data has it. `keyhash::member_key` is where that
229/// rule lives; asking it here is what makes the holder call
230/// `zd_enc_Middle` rather than `zd_kenc_Middle` for a keyless `Middle`,
231/// and everything under it follows because the key threads down.
232///
233/// Inert outside a key holder: the rule only ever turns `Purpose::Key`
234/// into `Purpose::Data`, so a payload run gets the name it always had.
235fn member_codec_name<T: CodecTarget>(
236 ast: &Ast,
237 member_type: Id<TypeNode>,
238 key: LayoutKey,
239) -> String {
240 codec_name::<T>(
241 ast,
242 member_type,
243 zerodds_ast_layout::keyhash::member_key(ast, member_type, key).purpose,
244 )
245}
246
247/// The encoder function name for a type.
248#[must_use]
249pub fn codec_name<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, purpose: Purpose) -> String {
250 let head = prefix(purpose);
251 let name = match ast.type_(id).map(|node| node.kind.clone()) {
252 // Named by the IDL kind and not by the target's spelling:
253 // `double` and `long double` share a C type and do not share a
254 // wire form, and a name taken from the target type would give
255 // them one function.
256 Ok(TypeKind::Primitive(kind)) => {
257 format!("{head}p_{}", kind.as_str().replace(' ', "_"))
258 }
259 _ => format!("{head}{}", T::type_name(ast, id)),
260 };
261 // The published spelling, once and here. See
262 // [`CodecTarget::published_name`].
263 T::published_name(name)
264}
265
266/// Build the encoder for every type in the model, under one plan.
267///
268/// Under `per_sample` the named non-`@nested` aggregates additionally
269/// get a public entry that bakes their own sample decision in; every
270/// other type is reached through the body of the type that declares it.
271///
272/// # Errors
273/// [`EncodeError`] when the layout cannot be planned or a construct has
274/// no C writer — never a body that leaves a member out.
275pub fn codec<T: CodecTarget>(
276 ast: &Ast,
277 key: LayoutKey,
278 options: CodecOptions,
279) -> Result<Vec<T::Decl>, EncodeError> {
280 codec_over::<T>(ast, &ordered_types(ast), key, options, &sample_types(ast))
281}
282
283/// [`codec`] without the types in `excluded`, and with what the types in
284/// `also_reaching` reach.
285///
286/// For a model whose declarations of some types are provided elsewhere —
287/// DDS-RPC 1.0 §7.5.1.1.1's common types come with the runtime — and whose
288/// request and reply types (§7.3.1.4) are encoded by a hand of the RPC
289/// binding that writes the header the runtime's way.
290///
291/// # Errors
292/// As [`codec`].
293pub fn codec_excluding<T: CodecTarget>(
294 ast: &Ast,
295 key: LayoutKey,
296 options: CodecOptions,
297 excluded: &[Id<TypeNode>],
298 also_reaching: &[Id<TypeNode>],
299) -> Result<Vec<T::Decl>, EncodeError> {
300 if excluded.is_empty() {
301 return codec::<T>(ast, key, options);
302 }
303 let (types, entries) = kept_after_excluding(ast, excluded, also_reaching);
304 codec_over::<T>(ast, &types, key, options, &entries)
305}
306
307/// The types, and the sample types among them, that remain once `excluded`
308/// is left out: what the other sample types reach. A helper only an
309/// excluded type needed is a `static` function nothing calls.
310pub(crate) fn kept_after_excluding(
311 ast: &Ast,
312 excluded: &[Id<TypeNode>],
313 also_reaching: &[Id<TypeNode>],
314) -> (Vec<Id<TypeNode>>, Vec<Id<TypeNode>>) {
315 let mut entries: Vec<Id<TypeNode>> = sample_types(ast)
316 .into_iter()
317 .filter(|id| !excluded.contains(id))
318 .collect();
319 for id in also_reaching {
320 if !entries.contains(id) {
321 entries.push(*id);
322 }
323 }
324 let types = ordered_types_from_pruning(ast, &entries, excluded);
325 (types, entries)
326}
327
328/// The encoder for the types one root reaches, and no others.
329///
330/// What a probe or a topic's translation unit wants: an encoder for a
331/// type the root cannot reach is a `static` function nothing calls, and
332/// a generated file that warns is one whose warnings stop being read.
333/// Under `per_sample` the root gets the public entry and every type it
334/// reaches gets a body.
335///
336/// # Errors
337/// As [`codec`].
338pub fn codec_from<T: CodecTarget>(
339 ast: &Ast,
340 root: Id<TypeNode>,
341 key: LayoutKey,
342 options: CodecOptions,
343) -> Result<Vec<T::Decl>, EncodeError> {
344 codec_over::<T>(
345 ast,
346 &ordered_types_from(ast, &[root]),
347 key,
348 options,
349 &[root],
350 )
351}
352
353/// The encoder for the types several roots reach, each function once.
354///
355/// What a translation unit that encodes members of several types wants:
356/// one call per root would define the functions the roots share twice.
357///
358/// # Errors
359/// As [`codec`].
360pub fn codec_from_many<T: CodecTarget>(
361 ast: &Ast,
362 roots: &[Id<TypeNode>],
363 key: LayoutKey,
364 options: CodecOptions,
365) -> Result<Vec<T::Decl>, EncodeError> {
366 codec_over::<T>(ast, &ordered_types_from(ast, roots), key, options, roots)
367}
368
369/// [`codec_from_many`] without the types in `excluded`, whose codecs are
370/// written by hand elsewhere (DDS-RPC 1.0 §7.5.1.1.1's common types come
371/// with the runtime in a shape of their own).
372///
373/// # Errors
374/// As [`codec`].
375pub fn codec_from_many_excluding<T: CodecTarget>(
376 ast: &Ast,
377 roots: &[Id<TypeNode>],
378 key: LayoutKey,
379 options: CodecOptions,
380 excluded: &[Id<TypeNode>],
381) -> Result<Vec<T::Decl>, EncodeError> {
382 let types = ordered_types_from_pruning(ast, roots, excluded);
383 codec_over::<T>(ast, &types, key, options, roots)
384}
385
386fn codec_over<T: CodecTarget>(
387 ast: &Ast,
388 types: &[Id<TypeNode>],
389 key: LayoutKey,
390 options: CodecOptions,
391 entries: &[Id<TypeNode>],
392) -> Result<Vec<T::Decl>, EncodeError> {
393 crate::encode::axes_the_codecs_carry_for::<T>(key)?;
394 let mut prototypes = Vec::new();
395 let mut bodies = Vec::new();
396 let mut emitted: Vec<String> = Vec::new();
397 let framed = per_sample(key);
398 let uncalled = called_only_in_bulk(
399 ast,
400 types,
401 entries,
402 T::bulk_octets_write("v", T::name("zd_n")).is_some(),
403 );
404
405 for id in types.iter().copied() {
406 if uncalled.contains(&id) {
407 continue;
408 }
409 let name = codec_name::<T>(ast, id, key.purpose);
410 if emitted.contains(&name) {
411 continue;
412 }
413 let body = body_name(&name, key);
414 let Some(statements) = body_of::<T>(ast, id, key, options, &body)? else {
415 continue;
416 };
417 emitted.push(name.clone());
418 // A target with no forward declarations answers `None` to the
419 // prototype and the loop emits only the body.
420 if !framed {
421 prototypes.extend(function::<T>(ast, id, key, &name, None, false));
422 bodies.extend(function::<T>(ast, id, key, &name, Some(statements), false));
423 continue;
424 }
425 prototypes.extend(function::<T>(ast, id, key, &body, None, true));
426 bodies.extend(function::<T>(ast, id, key, &body, Some(statements), true));
427 // The public entry, for the samples whose root this type is:
428 // it bakes the type's own step decision in and hands it to the
429 // body, which threads it to every type the body calls.
430 if entries.contains(&id) {
431 let past = sample_past_step(ast, id, key)?;
432 let call = T::perform(T::call(
433 &body,
434 vec![T::name("w"), T::name("v"), T::literal(Literal::Bool(past))],
435 ));
436 let entry = T::block(vec![call]);
437 prototypes.extend(entry_function::<T>(ast, id, &name, None));
438 bodies.extend(entry_function::<T>(ast, id, &name, Some(entry)));
439 }
440 }
441
442 prototypes.extend(bodies);
443 Ok(prototypes)
444}
445
446/// The key holder for a keyed type, and nothing for one with no `@key`.
447///
448/// The holder is the `@key` members alone, in member-id order, and it is
449/// big-endian whatever the sample is: two peers that disagree about
450/// either compute different hashes for the same instance.
451/// DDSI-RTPS 2.5 §9.6.3.8, XTypes 1.3 §7.6.8.
452///
453/// `key` is the holder's own layout key, and it is taken rather than
454/// assembled here because it carries the two things this function
455/// cannot work out for itself: `key_holder_nesting`, which decides how
456/// far below a keyless aggregate the key set reaches, and the purpose
457/// each member's encoder is named under.
458///
459/// # Errors
460/// [`EncodeError::NoLayout`] when the key set cannot be worked out.
461pub fn key_holder<T: CodecTarget>(
462 ast: &Ast,
463 root: Id<TypeNode>,
464 key: LayoutKey,
465) -> Result<Option<T::Decl>, EncodeError> {
466 let keys = zerodds_ast_layout::keyhash::key_members_with(
467 ast,
468 root,
469 key.key_holder_nesting,
470 key.key_holder_order,
471 )
472 .map_err(crate::encode::plan_refusal)?;
473 if keys.is_empty() {
474 // §7.6.8 Step 1 over a union keyed by its discriminator: the
475 // branches are not keys, so the holder is the discriminator alone,
476 // as `zerodds_ast_layout::xcdr` plans it. `key_members_with` walks
477 // members and answers the empty list for such a union.
478 let Some(discriminator) = zerodds_ast_layout::keyhash::union_key_discriminator(ast, root)
479 else {
480 return Ok(None);
481 };
482 let statements = vec![T::perform(nested_call_with::<T>(
483 ast,
484 discriminator,
485 key,
486 vec![
487 T::name("w"),
488 argument::<T>(ast, discriminator, T::discriminant("v")),
489 ],
490 T::literal(Literal::Bool(false)),
491 ))];
492 return Ok(key_holder_routine::<T>(
493 ast,
494 root,
495 &[
496 "The key holder: the discriminator of a union keyed by it.",
497 "Big-endian whatever the sample is — DDSI-RTPS 2.5 9.6.3.8.",
498 ],
499 statements,
500 ));
501 }
502
503 let mut statements = Vec::new();
504 for node in &keys {
505 // A key member of a nested aggregate is written by that
506 // aggregate's own key encoder, which the nested plan already
507 // filters; only the root's own members are named here.
508 let Some((name, member_type)) = member_by_node(ast, root, *node) else {
509 continue;
510 };
511 let reach = super::reach::<T>(ast, root, *node, &name);
512 statements.push(T::perform(nested_call_with::<T>(
513 ast,
514 member_type,
515 key,
516 vec![
517 T::name("w"),
518 argument::<T>(ast, member_type, arrow::<T>("v", &reach.field)),
519 ],
520 T::literal(Literal::Bool(false)),
521 )));
522 }
523
524 Ok(key_holder_routine::<T>(
525 ast,
526 root,
527 &[
528 "The key holder: the @key members alone, in member-id order.",
529 "Big-endian whatever the sample is — DDSI-RTPS 2.5 9.6.3.8.",
530 ],
531 statements,
532 ))
533}
534
535/// The routine of a key holder of `root` with the body `statements`.
536fn key_holder_routine<T: CodecTarget>(
537 ast: &Ast,
538 root: Id<TypeNode>,
539 doc: &[&str],
540 statements: Vec<T::Stmt>,
541) -> Option<T::Decl> {
542 T::routine(Routine {
543 doc: Doc::lines(doc),
544 name: format!("zd_keyholder_{}", T::type_name(ast, root)),
545 // The sample's own type, as every codec routine takes it. The
546 // flat `type_name` is a name for routines, not a type: in a
547 // module it read `m_S` where C++ declares `::m::S`, and the
548 // holder did not compile in C++, C# or TypeScript.
549 sample: T::value_type(ast, root),
550 held: Handoff::Value,
551 writing: true,
552 // Always emitted and always called; only the `Char16` codec's
553 // caller depends on a setting. See `maybe_unused`.
554 tolerate_unused: false,
555 extra: Vec::new(),
556 body: Some(T::block(statements)),
557 })
558}
559
560/// The codec routine for a type, with or without a
561/// body.
562///
563/// `framed` says this is a body under [`per_sample`]: it takes
564/// [`EXTENDED_PARAM`] after the sample and marks it read, so a target
565/// that warns on an unread parameter still compiles.
566fn function<T: CodecTarget>(
567 ast: &Ast,
568 id: Id<TypeNode>,
569 key: LayoutKey,
570 name: &str,
571 body: Option<T::Block>,
572 framed: bool,
573) -> Option<T::Decl> {
574 let _ = key.purpose;
575 T::routine(Routine {
576 doc: Doc::none(),
577 name: name.to_string(),
578 sample: T::value_type(ast, id),
579 held: if is_array(ast, id, 0) {
580 Handoff::Array
581 } else {
582 Handoff::Value
583 },
584 writing: true,
585 // A `Char16` whose element form is written inline at the call
586 // site has no caller left when the type graph reaches one only
587 // through a collection. Whether this function is used is then a
588 // property of the dialect, not of the model, and the generated
589 // code is compiled with an uncalled function as an error.
590 tolerate_unused: maybe_unused(ast, id, key),
591 extra: if framed {
592 vec![(T::scalar(Scalar::Bool), EXTENDED_PARAM.to_string())]
593 } else {
594 Vec::new()
595 },
596 body: if framed {
597 body.map(with_extended_used::<T>)
598 } else {
599 body
600 },
601 })
602}
603
604/// The public entry of a type under [`per_sample`]: the sample's own
605/// step decision baked in, handed to the body.
606///
607/// The entry takes no [`EXTENDED_PARAM`] because the sample whose root
608/// this type is decides once, at generation time; every nested type is
609/// reached through a body that takes the decision at run time.
610fn entry_function<T: CodecTarget>(
611 ast: &Ast,
612 id: Id<TypeNode>,
613 name: &str,
614 body: Option<T::Block>,
615) -> Option<T::Decl> {
616 T::routine(Routine {
617 doc: Doc::none(),
618 name: name.to_string(),
619 sample: T::value_type(ast, id),
620 held: if is_array(ast, id, 0) {
621 Handoff::Array
622 } else {
623 Handoff::Value
624 },
625 writing: true,
626 tolerate_unused: false,
627 extra: Vec::new(),
628 body,
629 })
630}
631
632/// Whether the codec generated for `id` may legitimately have no caller.
633///
634/// True only for a `Char16` under a framing whose member and element
635/// forms differ, which is TAO's alone. Everywhere else an uncalled
636/// generated codec is a generator defect and the warning must stay a
637/// warning.
638pub(crate) fn maybe_unused(ast: &Ast, id: Id<TypeNode>, key: LayoutKey) -> bool {
639 // The RPC binding of an interface or of a pair of types calls the codecs
640 // of the types it carries and of nothing else (DDS-RPC 1.0 §7.5.1.1,
641 // §7.3.1.4); a type declared beside it keeps its codec and has no caller
642 // in the header.
643 if ast.interfaces().next().is_some() || crate::emit::type_pair_cpp::has_pair_types(ast) {
644 return true;
645 }
646 // A base structure is reached through the members of the structure derived from it
647 // (`v->parent.m`), and its own codec has no caller where nothing else holds it.
648 if is_a_base(ast, id) {
649 return true;
650 }
651 key.wchar_framing.has_length_octet(WcharPosition::Member)
652 != key.wchar_framing.has_length_octet(WcharPosition::Element)
653 && matches!(resolve_primitive(ast, id, 0), Some(PrimitiveKind::WChar))
654}
655
656/// Whether another structure in the model derives from `id`.
657fn is_a_base(ast: &Ast, id: Id<TypeNode>) -> bool {
658 ast.all_nodes().any(|any| match any {
659 zerodds_ast::AnyId::Type(other, _) => {
660 crate::adapter::c::struct_base(ast, other) == Some(id)
661 }
662 _ => false,
663 })
664}
665
666/// Whether a type is a C array once typedefs are seen through.
667///
668/// zerodds-lint: recursion-depth 64 (typedef chains; bounded by the
669/// `depth > 64` check, past which the answer is `false` — the
670/// handoff form a non-array takes)
671fn is_array(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
672 if depth > 64 {
673 return false;
674 }
675 match ast.type_(id).map(|node| node.kind.clone()) {
676 Ok(TypeKind::Array { .. }) => true,
677 Ok(TypeKind::Alias { aliased }) => aliased
678 .resolved()
679 .is_some_and(|target| is_array(ast, target, depth + 1)),
680 _ => false,
681 }
682}
683
684/// How a value of `id` is handed to its encoder: the array itself, or
685/// the address of anything else.
686fn argument<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, value: T::Expr) -> T::Expr {
687 if is_array(ast, id, 0) {
688 value
689 } else {
690 T::handoff(value, Handoff::Value)
691 }
692}
693
694#[allow(clippy::too_many_lines)]
695fn body_of<T: CodecTarget>(
696 ast: &Ast,
697 id: Id<TypeNode>,
698 key: LayoutKey,
699 options: CodecOptions,
700 own_name: &str,
701) -> Result<Option<T::Block>, EncodeError> {
702 let Ok(node) = ast.type_(id) else {
703 return Ok(None);
704 };
705 let statements = match &node.kind {
706 TypeKind::Primitive(_) => {
707 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
708 return Err(EncodeError::NoWriter {
709 member: T::type_name(ast, id),
710 kind: "primitive the layout plan has no scalar write for",
711 });
712 };
713 vec![primitive_write_at::<T>(write, deref::<T>("v"))]
714 }
715
716 // A bounded `string` or `wstring` writes through
717 // `bounded_string_write`, which calls the runtime's own
718 // `write_string_bounded`/`write_wstring_bounded` and lets it
719 // refuse an over-bound value — the same refusal
720 // `crates/wire::Writer::write_string_bounded` raises for Rust,
721 // and the same reason `write_string_bounded`'s own remarks give:
722 // the bound is a promise about the source and not a difference
723 // on the wire, so writing an over-bound value as if it fit, or
724 // cutting it at a character boundary, would put a sample on the
725 // wire nobody wrote.
726 TypeKind::String { bound, .. } => {
727 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
728 return Err(EncodeError::NoWriter {
729 member: own_name.to_string(),
730 kind: "value the layout plan has no scalar write for",
731 });
732 };
733 vec![match bound_limit(ast, bound) {
734 Some(limit) => bounded_string_write::<T>(own_name, write, deref::<T>("v"), limit)?,
735 None => primitive_write_at::<T>(write, deref::<T>("v")),
736 }]
737 }
738
739 TypeKind::Fixed { .. } => {
740 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
741 return Err(EncodeError::NoWriter {
742 member: own_name.to_string(),
743 kind: "value the layout plan has no scalar write for",
744 });
745 };
746 vec![primitive_write_at::<T>(write, deref::<T>("v"))]
747 }
748
749 // Two axes on one construct, and they are separate questions:
750 // `enumeration_bit_bound` says how wide the holder is and
751 // `enumerator_wire_value` says which integer goes into it. The
752 // bitmask arm below is a different clause that neither reaches.
753 TypeKind::Enumeration { .. } => enumeration_write::<T>(ast, id, key)?,
754
755 TypeKind::Bitmask { .. } => {
756 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
757 return Err(EncodeError::NoWriter {
758 member: own_name.to_string(),
759 kind: "bitmask the layout plan has no scalar write for",
760 });
761 };
762 vec![primitive_write_at::<T>(write, deref::<T>("v"))]
763 }
764
765 TypeKind::Bitset { .. } => {
766 // The whole chain, base-first: the planner sizes the anchor
767 // over it and the one implementation that parses an
768 // inherited bitset packs it that way.
769 // `docs/refused-constructs.md` has the measurement.
770 let fields = &ast.all_bitfields_of(id);
771 let mut deps = DepCollector::default();
772 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
773 .map_err(crate::encode::plan_refusal)?;
774 let Extent::Sized(anchor) = layout.extent else {
775 return Err(EncodeError::NoWriter {
776 member: own_name.to_string(),
777 kind: "bitset whose anchor the planner does not size",
778 });
779 };
780 let mut statements = vec![T::declare(
781 T::scalar(Scalar::UInt64),
782 "bits",
783 Some(int::<T>(0)),
784 )];
785 let mut position: u32 = 0;
786 for field in fields {
787 let Ok(field_node) = ast.bitfield(*field) else {
788 continue;
789 };
790 let width = u32::from(field_node.bits);
791 // An anonymous field is padding: it moves the next field
792 // along and carries no value of its own.
793 if let Some(field_name) = field_node.name.as_ref() {
794 statements.push(T::perform(T::binary(
795 "=",
796 T::name("bits"),
797 T::binary(
798 "|",
799 T::name("bits"),
800 call_expr::<T>(
801 "bitfield",
802 vec![
803 cast::<T>(
804 T::scalar(Scalar::UInt64),
805 arrow::<T>("v", &T::escape(field_name)),
806 ),
807 int::<T>(i128::from(width)),
808 int::<T>(i128::from(position)),
809 ],
810 ),
811 ),
812 )));
813 }
814 position = position.saturating_add(width);
815 }
816 statements.push(call::<T>(
817 "write_bitset",
818 vec![int::<T>(i128::from(anchor)), T::name("bits")],
819 ));
820 statements
821 }
822
823 TypeKind::Alias { aliased } => {
824 let Some(target) = aliased.resolved() else {
825 return Err(EncodeError::NoWriter {
826 member: own_name.to_string(),
827 kind: "typedef with an unresolved target",
828 });
829 };
830 // A typedef is transparent on the wire, and the C typedef
831 // makes the two the same type, so the value goes straight
832 // through.
833 let callee = body_name(&member_codec_name::<T>(ast, target, key), key);
834 // Unless the alias and its target are one encoder. A target
835 // that folds a `typedef` into the type it names gives the
836 // two the same encoder name — Zig's `pub const NodeRef =
837 // Node;` is the alias, so `Zig::type_name` answers `m_Node`
838 // for both — and a forwarder to itself is not a forwarder:
839 // `recursive-typedef-sequence` built
840 // `fn zd_enc_m_Node(..) { try zd_enc_m_Node(..); }` and the
841 // probe took SIGSEGV on the stack it ran out of. `None`
842 // leaves the name unclaimed and the aliased type emits its
843 // own body under it.
844 if callee == own_name {
845 return Ok(None);
846 }
847 let mut args = vec![T::name("w"), T::name("v")];
848 if per_sample(key) {
849 args.push(T::name(EXTENDED_PARAM));
850 }
851 vec![T::perform(T::call(&callee, args))]
852 }
853
854 TypeKind::Sequence { element, bound } => {
855 let Some(element) = element.resolved() else {
856 return Err(EncodeError::NoWriter {
857 member: own_name.to_string(),
858 kind: "sequence with an unresolved element",
859 });
860 };
861 let delimited = zerodds_ast_layout::collection_is_delimited(ast, element, key);
862 // Checked before the DHEADER, and not after: `sequence<T,
863 // N>` bounds the count `O.length` puts on the wire, and a
864 // sender that wrote more elements than its own declaration
865 // allows has written a value of a type nobody declared.
866 // `crates/wire::Writer::check_sequence_bound` checks the
867 // same thing at the same point, for the same reason.
868 let mut statements = Vec::new();
869 if let Some(limit) = bound_limit(ast, bound) {
870 statements.push(bound_check::<T>(
871 "check_sequence_bound",
872 T::count_of("v", None),
873 limit,
874 ));
875 }
876 statements.push(dheader_declaration::<T>(delimited));
877 statements.push(call::<T>("write_u32", vec![T::count_of("v", None)]));
878 if let Some(bulk) = bulk_write::<T>(ast, element, T::count_of("v", None)) {
879 statements.push(bulk);
880 statements.push(finish_dheader::<T>());
881 return Ok(Some(T::block(statements)));
882 }
883 // A `sequence` element of a non-`@final` type carries its own
884 // octet length under
885 // `SequenceElementDelimiter::BeforeEachNonFinalElement`, and
886 // every element counts its alignment from its own first octet
887 // under `ElementPadding::Packed`. The order is the reference's:
888 // the origin is moved first, then the element's length, then
889 // the element, and both are undone in reverse.
890 let packed = key.element_padding == ElementPadding::Packed;
891 let carries_length = sequence_element_carries_length(ast, element, key)?;
892 let at = T::index(T::elements("v"), T::name("i"));
893 let mut body: Vec<T::Stmt> = Vec::new();
894 if packed {
895 body.push(begin_packed::<T>());
896 }
897 if carries_length {
898 body.push(element_length_declaration::<T>());
899 }
900 body.push(
901 element_write::<T>(ast, element, key, at.clone()).unwrap_or_else(|| {
902 T::perform(nested_call::<T>(
903 ast,
904 element,
905 key,
906 vec![T::name("w"), argument::<T>(ast, element, at)],
907 ))
908 }),
909 );
910 if carries_length {
911 body.push(finish_element_length::<T>());
912 }
913 if packed {
914 body.push(finish_packed::<T>());
915 }
916 statements.push(T::count_up("i", T::count_of("v", None), T::block(body)));
917 statements.push(finish_dheader::<T>());
918 statements
919 }
920
921 TypeKind::Array { element, dims } => {
922 let Some(element) = element.resolved() else {
923 return Err(EncodeError::NoWriter {
924 member: own_name.to_string(),
925 kind: "array with an unresolved element",
926 });
927 };
928 let mut lengths = Vec::new();
929 for dim in dims {
930 let length = ast.eval_integral(*dim).map_err(|_| EncodeError::NoWriter {
931 member: own_name.to_string(),
932 kind: "array whose length does not evaluate",
933 })?;
934 lengths.push(length);
935 }
936 let delimited = zerodds_ast_layout::array_is_delimited(ast, element, key);
937 // §7.4.3.5 treats a multi-dimensional array as one flat run
938 // with the first index varying most slowly, which is what
939 // nested loops in written order already are.
940 let mut access = T::name("v");
941 for level in 0..lengths.len() {
942 access = T::index(access, T::name(&format!("i{level}")));
943 }
944 if let [length] = lengths.as_slice() {
945 if let Some(bulk) = bulk_write::<T>(ast, element, int::<T>(*length)) {
946 return Ok(Some(T::block(vec![
947 dheader_declaration::<T>(delimited),
948 bulk,
949 finish_dheader::<T>(),
950 ])));
951 }
952 }
953 let mut inner = T::block(vec![
954 element_write::<T>(ast, element, key, access.clone()).unwrap_or_else(|| {
955 T::perform(nested_call::<T>(
956 ast,
957 element,
958 key,
959 vec![T::name("w"), argument::<T>(ast, element, access)],
960 ))
961 }),
962 ]);
963 // Under `ElementPadding::Packed` each element of every
964 // dimension is its own alignment block, as in the reference's
965 // `write_array_dimension`.
966 let packed = key.element_padding == ElementPadding::Packed;
967 for (level, length) in lengths.iter().enumerate().rev() {
968 let index = format!("i{level}");
969 let mut body: Vec<T::Stmt> = Vec::new();
970 if packed {
971 body.push(begin_packed::<T>());
972 }
973 body.extend(T::into_statements(inner));
974 if packed {
975 body.push(finish_packed::<T>());
976 }
977 inner = T::block(vec![T::count_up(&index, int::<T>(*length), T::block(body))]);
978 }
979 let mut statements = vec![dheader_declaration::<T>(delimited)];
980 statements.extend(T::into_statements(inner));
981 statements.push(finish_dheader::<T>());
982 statements
983 }
984
985 TypeKind::Map {
986 key: k,
987 value,
988 bound,
989 } => {
990 let (Some(k), Some(value)) = (k.resolved(), value.resolved()) else {
991 return Err(EncodeError::NoWriter {
992 member: own_name.to_string(),
993 kind: "map with an unresolved key or value",
994 });
995 };
996 let mut deps = DepCollector::default();
997 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
998 .map_err(crate::encode::plan_refusal)?;
999 let entry = T::map_entry_name(ast, id);
1000 let _ = &entry;
1001 // The order the entries go out in is a setting, and a key
1002 // type the crossing has no order for is a refusal rather
1003 // than a quiet fallback to declaration order.
1004 let Ok(ordering) = zerodds_ast_layout::plan::map_entry_ordering(ast, id, key) else {
1005 return Err(EncodeError::NoWriter {
1006 member: own_name.to_string(),
1007 kind: "a map whose key type has no value order, under \
1008 --map-entries ascending-key",
1009 });
1010 };
1011 // Checked before the DHEADER, as the sequence's own bound
1012 // is: `map<K, V, N>` bounds the **entry** count, and
1013 // `crates/wire::Writer::check_map_bound` checks it at the
1014 // same point and names it in entries and not in halves, for
1015 // the reason its own remarks give.
1016 let mut statements = Vec::new();
1017 if let Some(limit) = bound_limit(ast, bound) {
1018 statements.push(bound_check::<T>(
1019 "check_map_bound",
1020 T::count_of("v", None),
1021 limit,
1022 ));
1023 }
1024 statements.push(dheader_declaration::<T>(layout.needs_dheader));
1025 if !layout.needs_dheader || options.map_length_with_delimiter {
1026 statements.push(call::<T>("write_u32", vec![T::count_of("v", None)]));
1027 }
1028 let element = |key: bool| T::entry_half(T::index(T::elements("v"), T::name("i")), key);
1029 let pair = T::block(vec![
1030 T::perform(nested_call::<T>(
1031 ast,
1032 k,
1033 key,
1034 vec![T::name("w"), argument::<T>(ast, k, element(true))],
1035 )),
1036 T::perform(nested_call::<T>(
1037 ast,
1038 value,
1039 key,
1040 vec![T::name("w"), argument::<T>(ast, value, element(false))],
1041 )),
1042 ]);
1043 statements.push(match ordering {
1044 None => over_the_entries::<T>("i", pair),
1045 Some(ordering) => ascending_by_key::<T>(ast, k, ordering, pair),
1046 });
1047 statements.push(finish_dheader::<T>());
1048 statements
1049 }
1050
1051 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
1052 aggregate_body::<T>(ast, id, key)?
1053 }
1054
1055 TypeKind::Union { discriminator, .. } => {
1056 union_body::<T>(ast, id, key, discriminator.resolved(), own_name)?
1057 }
1058
1059 // No wire form at all — `zerodds_ast_layout` refuses to plan
1060 // one — so there is nothing to emit and nothing is claimed.
1061 TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
1062 return Ok(None);
1063 }
1064 TypeKind::ValueBox { .. } => {
1065 return Err(EncodeError::NoWriter {
1066 member: own_name.to_string(),
1067 kind: "boxed value, whose presence flag has no settled wire form",
1068 });
1069 }
1070 // Refused by name rather than passed over. `any` is in the model
1071 // because IDL 4.2 has it; DDS-XTypes has no `any` and no
1072 // encoding for one, and only CORBA CDR writes it — as a TypeCode
1073 // ahead of the value. Returning `Ok(None)` here would generate a
1074 // codec that silently omitted the member.
1075 TypeKind::Any => {
1076 return Err(EncodeError::NoWriter {
1077 member: own_name.to_string(),
1078 kind: "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
1079 as a TypeCode ahead of the value",
1080 });
1081 }
1082 TypeKind::ValueBase => {
1083 return Err(EncodeError::NoWriter {
1084 member: own_name.to_string(),
1085 kind: "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
1086 one, as a chunked value with its repository id",
1087 });
1088 }
1089 };
1090 Ok(Some(T::block(statements)))
1091}
1092
1093fn aggregate_body<T: CodecTarget>(
1094 ast: &Ast,
1095 id: Id<TypeNode>,
1096 key: LayoutKey,
1097) -> Result<Vec<T::Stmt>, EncodeError> {
1098 let mut deps = DepCollector::default();
1099 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1100 .map_err(crate::encode::plan_refusal)?;
1101
1102 // `ElementPadding::Packed`: an aggregate is its own alignment block,
1103 // so its members count from its own first octet and it is not padded
1104 // up to its own alignment before it starts. Measured on
1105 // `typedef-of-struct`, whose second `Cell` puts its `long` at offset
1106 // six where the clause puts it at eight. `saved` is the whole body's,
1107 // as in the reference's `write_aggregate`.
1108 let packed = key.element_padding == ElementPadding::Packed;
1109 let mut statements = Vec::new();
1110 if packed {
1111 statements.push(begin_packed::<T>());
1112 }
1113 statements.push(dheader_declaration::<T>(layout.needs_dheader));
1114 for (index, member) in layout.members.iter().enumerate() {
1115 let Some((name, member_type)) = member_by_node(ast, id, member.member) else {
1116 continue;
1117 };
1118 let reach = super::reach::<T>(ast, id, member.member, &name);
1119 let field = reach.field.clone();
1120 let node = ast
1121 .all_members_of(id)
1122 .into_iter()
1123 .filter_map(|m| ast.member(m).ok())
1124 .find(|m| m.node_id == member.member);
1125 let optional = node.as_ref().is_some_and(|m| m.facets.is_optional());
1126 // Table 21: `@external` changes no octet, only where the
1127 // binding keeps the value — which is exactly what the seam it
1128 // routes through decides.
1129 let external = node
1130 .as_ref()
1131 .is_some_and(|m| super::reached_externally::<T>(ast, m));
1132
1133 // Whether the member's value has room inside `id` at all. A
1134 // column whose member is the value itself has none where the
1135 // member's type reaches `id` back over inline edges, and the
1136 // seam is where it says what it keeps instead.
1137 let indirect =
1138 super::behind_pointer::<T>(ast, reach.holder, member_type, external, optional);
1139 // And whether the column keeps the member behind a reference of
1140 // its own making. `CodecTarget::holds_by_reference`.
1141 let by_reference =
1142 T::holds_by_reference(ast, reach.holder, member_type, optional, external);
1143
1144 // XTypes 1.3 §7.5.1.2.3.1.1 and §7.5.1.2.3.2.1: an `@external`
1145 // member that is not `@optional` may not be unset at the time of
1146 // a write. Only the columns that have an unset state say
1147 // anything here.
1148 if external && !optional && !by_reference {
1149 statements.extend(T::refuse_unset_external(T::external_present("v", &field)));
1150 }
1151
1152 statements.extend(framed_member::<T>(
1153 ast,
1154 member,
1155 index,
1156 member_type,
1157 &field,
1158 key,
1159 optional,
1160 external,
1161 indirect,
1162 by_reference,
1163 )?);
1164 }
1165 if layout.needs_sentinel {
1166 statements.push(call::<T>("write_sentinel", Vec::new()));
1167 }
1168 statements.push(finish_dheader::<T>());
1169 if packed {
1170 statements.push(finish_packed::<T>());
1171 }
1172 Ok(statements)
1173}
1174
1175/// One member with the header the plan gave it.
1176///
1177/// `optional` is the model's answer and the header is the plan's, and
1178/// the two together decide what an absent member looks like. A
1179/// `@mutable` member that is absent contributes nothing — the parameter
1180/// list is what says which members are there. An `OPT_FMEMBER` that is
1181/// absent contributes its marker: a `<is_present>` of zero under XCDR2
1182/// (rule 20) or a parameter header declaring a length of zero under
1183/// XCDR1 (§7.4.1.2). All three implementations write both.
1184#[allow(clippy::too_many_arguments)]
1185fn framed_member<T: CodecTarget>(
1186 ast: &Ast,
1187 member: &zerodds_ast_layout::plan::MemberPlan,
1188 index: usize,
1189 member_type: Id<TypeNode>,
1190 field: &str,
1191 key: LayoutKey,
1192 optional: bool,
1193 external: bool,
1194 indirect: bool,
1195 by_reference: bool,
1196) -> Result<Vec<T::Stmt>, EncodeError> {
1197 let present = if by_reference {
1198 T::reference_present("v", field)
1199 } else if external {
1200 T::external_present("v", field)
1201 } else {
1202 T::present("v", field)
1203 };
1204 /// A member written only when the generated `_has_` flag is set.
1205 fn guarded<B: CodecTarget>(present: B::Expr, statements: Vec<B::Stmt>) -> B::Stmt {
1206 B::when(present, B::block(statements), None)
1207 }
1208 // An `@optional` member is reached through the target's own
1209 // accessor: C's is a plain `T` beside a flag and C++'s is a
1210 // `std::optional<T>` the codec cannot take whole. An `@external`
1211 // one is reached through its own seam — an indirection in the
1212 // column that keeps one, the member itself everywhere else — and
1213 // `@optional @external` is the external form: the indirection
1214 // already carries the absence.
1215 let held = if by_reference {
1216 T::reference_value(ast, member_type, "v", field)
1217 } else if external {
1218 T::external_value(ast, member_type, "v", field, optional)
1219 } else if optional {
1220 T::optional_value(ast, member_type, "v", field)
1221 } else {
1222 arrow::<T>("v", field)
1223 };
1224 // And through the indirection where the column had to put one:
1225 // `member_held_inline_closes_a_cycle` said the value has no room in
1226 // its own aggregate, so what the accessor above reaches is the
1227 // indirection and not the value.
1228 let held = if indirect && !by_reference {
1229 T::behind_indirection(ast, member_type, held)
1230 } else {
1231 held
1232 };
1233 let value = T::perform(nested_call::<T>(
1234 ast,
1235 member_type,
1236 key,
1237 vec![T::name("w"), argument::<T>(ast, member_type, held)],
1238 ));
1239
1240 // A distinct name per member: two headers at the same block scope
1241 // cannot share one, and the plan may give every member a header.
1242 let handle = format!("h{index}");
1243 Ok(match member.header {
1244 MemberHeader::NoHeader => vec![value],
1245 MemberHeader::PidCdr1 {
1246 pid,
1247 extended,
1248 must_understand,
1249 } => {
1250 let mut body = open_pid_header::<T>(
1251 ast,
1252 key,
1253 &handle,
1254 pid,
1255 member.member_id,
1256 extended,
1257 Constant::ParameterMember,
1258 must_understand,
1259 member_type,
1260 )?;
1261 body.push(value);
1262 body.push(call::<T>("finish_pid", vec![T::name(&handle)]));
1263 if optional {
1264 vec![guarded::<T>(present, body)]
1265 } else {
1266 body
1267 }
1268 }
1269 MemberHeader::EmHeader {
1270 lc,
1271 must_understand,
1272 } => {
1273 let body = vec![
1274 T::declare(
1275 T::handle(Handle::EmHeader),
1276 &handle,
1277 Some(call_expr::<T>(
1278 "write_emheader",
1279 vec![
1280 int::<T>(i128::from(member.member_id)),
1281 int::<T>(i128::from(lc.bits())),
1282 T::literal(Literal::Bool(must_understand)),
1283 ],
1284 )),
1285 ),
1286 value,
1287 call::<T>("finish_emheader", vec![T::name(&handle)]),
1288 ];
1289 if optional {
1290 vec![guarded::<T>(present, body)]
1291 } else {
1292 body
1293 }
1294 }
1295 // Rule (19) and §7.4.1.2: the header goes out either way, and a
1296 // declared length of zero is what says the member did not.
1297 MemberHeader::OptionalCdr1 {
1298 pid,
1299 extended,
1300 must_understand,
1301 } => {
1302 let mut body = open_pid_header::<T>(
1303 ast,
1304 key,
1305 &handle,
1306 pid,
1307 member.member_id,
1308 extended,
1309 Constant::ParameterOptional,
1310 must_understand,
1311 member_type,
1312 )?;
1313 body.push(guarded::<T>(present, vec![value]));
1314 body.push(call::<T>("finish_pid", vec![T::name(&handle)]));
1315 body
1316 }
1317 // Rule (20), §7.4.3.5.2: the BOOLEAN `<is_present>`, then the
1318 // value behind it.
1319 MemberHeader::OptionalFlag => vec![
1320 call::<T>("write_bool", vec![present.clone()]),
1321 guarded::<T>(present, vec![value]),
1322 ],
1323 })
1324}
1325
1326fn union_body<T: CodecTarget>(
1327 ast: &Ast,
1328 id: Id<TypeNode>,
1329 key: LayoutKey,
1330 discriminator: Option<Id<TypeNode>>,
1331 own_name: &str,
1332) -> Result<Vec<T::Stmt>, EncodeError> {
1333 let Some(discriminator) = discriminator else {
1334 return Err(EncodeError::NoWriter {
1335 member: own_name.to_string(),
1336 kind: "union with an unresolved discriminator",
1337 });
1338 };
1339 let mut deps = DepCollector::default();
1340 let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1341 .map_err(crate::encode::plan_refusal)?;
1342 // The one crossing [`UnionLabelMatch`] has. Under the default it is
1343 // false for every discriminator and the arm chain below is the
1344 // clause's, unchanged.
1345 let suppress_negative = zerodds_ast_layout::plan::union_negative_selects_nothing(ast, id, key);
1346
1347 // `ElementPadding::Packed`: a union is its own alignment block too.
1348 // `annotation-external-union-branch` records one that starts where
1349 // the previous member ended.
1350 let packed = key.element_padding == ElementPadding::Packed;
1351 let mut statements = Vec::new();
1352 if packed {
1353 statements.push(begin_packed::<T>());
1354 }
1355 statements.push(dheader_declaration::<T>(layout.needs_dheader));
1356 let write_discriminator = T::perform(nested_call::<T>(
1357 ast,
1358 discriminator,
1359 key,
1360 vec![
1361 T::name("w"),
1362 argument::<T>(ast, discriminator, T::discriminant("v")),
1363 ],
1364 ));
1365 // Rules (27) and (28): a `@mutable` union frames its discriminator
1366 // as an `MMEMBER` at member id 0.
1367 statements.extend(mmember::<T>(
1368 ast,
1369 key,
1370 layout.discriminator,
1371 discriminator,
1372 0,
1373 "hd",
1374 write_discriminator,
1375 )?);
1376
1377 // The branch, selected by comparing the discriminator against each
1378 // case's labels. `default:` is the else of the chain, so a value
1379 // that matches no label and has no default writes nothing after the
1380 // discriminator — which is what the type says.
1381 let mut arms: Vec<(Option<T::Expr>, T::Block)> = Vec::new();
1382 let mut fallback: Option<T::Block> = None;
1383 // The labels the setting says select nothing, with the member each
1384 // one would have selected: the overlay below still needs them.
1385 let mut suppressed: Vec<(i128, Id<TypeNode>, String, bool)> = Vec::new();
1386 let mut default_case: Option<(Id<TypeNode>, NodeId)> = None;
1387 for case in ast.cases_of(id) {
1388 let Ok(case_node) = ast.case(*case) else {
1389 continue;
1390 };
1391 let Ok(member) = ast.member(case_node.member) else {
1392 continue;
1393 };
1394 let Some(member_type) = member.type_.resolved() else {
1395 return Err(EncodeError::NoWriter {
1396 member: member.name.clone(),
1397 kind: "union case with an unresolved type",
1398 });
1399 };
1400 let field = T::escape(&member.name);
1401 // Table 21: `@external` changes no octet, only where the binding
1402 // keeps the value — the same seam a structure member routes
1403 // through, with the branch accessor instead of the member one.
1404 let external = super::reached_externally::<T>(ast, member);
1405 let held = if external {
1406 T::external_case_value(ast, member_type, "v", &field)
1407 } else {
1408 T::case_member("v", &field)
1409 };
1410 // A branch closes a cycle the same way a member does, and the
1411 // column keeps it the same way. §7.2.2.4.4.4.7 makes no union
1412 // member `@optional`, so a branch on a cycle is one the
1413 // enclosing type reaches back through some *other* type's
1414 // `@optional` member.
1415 let held = if super::behind_pointer::<T>(ast, id, member_type, external, false) {
1416 T::behind_indirection(ast, member_type, held)
1417 } else {
1418 held
1419 };
1420 let value = T::perform(nested_call::<T>(
1421 ast,
1422 member_type,
1423 key,
1424 vec![T::name("w"), argument::<T>(ast, member_type, held)],
1425 ));
1426 // The selected member takes its own `MMEMBER` header under
1427 // `@mutable`, and nothing under any other extensibility.
1428 let planned = layout
1429 .members
1430 .iter()
1431 .find(|plan| plan.member == member.node_id);
1432 let write = T::block(mmember::<T>(
1433 ast,
1434 key,
1435 planned.map(|plan| plan.header),
1436 member_type,
1437 planned.map_or(0, |plan| plan.member_id),
1438 &format!("hu{}", planned.map_or(0, |plan| plan.member_id)),
1439 value,
1440 )?);
1441
1442 let mut condition: Option<T::Expr> = None;
1443 let mut is_default = false;
1444 for case_label in &case_node.labels {
1445 match case_label {
1446 CaseLabel::Default => is_default = true,
1447 CaseLabel::Value(expr) => {
1448 let value = ast
1449 .eval_integral(*expr)
1450 .map_err(|_| EncodeError::NoWriter {
1451 member: member.name.clone(),
1452 kind: "union label that does not evaluate",
1453 })?;
1454 // A label a setting says selects nothing gets no arm
1455 // at all, so the chain falls through to `default:`
1456 // where the union has one and past the whole union
1457 // where it has none — which is what the C peer this
1458 // reproduces does with its own real union.
1459 // `zerodds_ast_layout::union_negative_matches_nothing`.
1460 if suppress_negative && value < 0 {
1461 suppressed.push((value, member_type, field.clone(), external));
1462 continue;
1463 }
1464 let test = T::binary(
1465 "==",
1466 T::discriminant("v"),
1467 T::case_label(ast, discriminator, value),
1468 );
1469 condition = Some(match condition {
1470 None => test,
1471 Some(previous) => T::binary("||", previous, test),
1472 });
1473 }
1474 }
1475 }
1476 if is_default {
1477 fallback = Some(write.clone());
1478 default_case = Some((member_type, member.node_id));
1479 }
1480 if let Some(condition) = condition {
1481 arms.push((Some(condition), write));
1482 }
1483 }
1484
1485 // The overlay the suppression owes. The peer this reproduces holds
1486 // its arms in one C storage: a discriminator carrying a suppressed
1487 // label falls through to `default:` there and writes the *storage*
1488 // as the default arm — the selected member's octets, reinterpreted.
1489 // A target with one field per arm would write the never-filled
1490 // default field instead, so each suppressed label gets a branch
1491 // ahead of the default write that puts the selected member's bit
1492 // pattern out at the default arm's width, through the runtime's
1493 // `write_union_overlay`. Mirrors `write_selected_branch` in
1494 // `zerodds-wire-codec`; a union without `default:` writes nothing
1495 // after the discriminator, exactly as the plain suppression does.
1496 if let (Some((default_type, default_node)), false) = (default_case, suppressed.is_empty()) {
1497 let width = overlay_width(ast, default_type, key, "default:")?;
1498 let planned = layout
1499 .members
1500 .iter()
1501 .find(|plan| plan.member == default_node);
1502 let mut chain = fallback;
1503 for (value, source_type, source_field, source_external) in suppressed.into_iter().rev() {
1504 let source = bit_pattern_source(ast, source_type, key, &source_field)?;
1505 // The overlay reproduces the peer's *storage*, which in the
1506 // peer's C union is the value itself — an `@external` arm's
1507 // pointer bytes are this process's addresses, not octets any
1508 // wire form has, so the dereference is taken first.
1509 let member = if source_external {
1510 T::external_case_value(ast, source_type, "v", &source_field)
1511 } else {
1512 T::case_member("v", &source_field)
1513 };
1514 // And behind the indirection where the branch has one, for
1515 // the reason the selected write gives.
1516 let member = if super::behind_pointer::<T>(ast, id, source_type, source_external, false)
1517 {
1518 T::behind_indirection(ast, source_type, member)
1519 } else {
1520 member
1521 };
1522 // An integer form crosses the target's `Int64` cast first —
1523 // the seam the enumeration codec reads its value through —
1524 // so an enum arm needs no spelling of its own and a target
1525 // with no unsigned types receives its widest integer.
1526 let member = match source {
1527 crate::emit::BitPatternSource::Int { .. } => cast::<T>(
1528 T::scalar(Scalar::Int64),
1529 T::enumerator_ordinal(ast, source_type, member),
1530 ),
1531 _ => member,
1532 };
1533 let pattern = T::bit_pattern(source, member);
1534 let write = T::perform(T::runtime(
1535 "w",
1536 "write_union_overlay",
1537 vec![pattern, int::<T>(i128::from(width))],
1538 ));
1539 let overlay = T::block(mmember::<T>(
1540 ast,
1541 key,
1542 planned.map(|plan| plan.header),
1543 source_type,
1544 planned.map_or(0, |plan| plan.member_id),
1545 &format!("ho{}", planned.map_or(0, |plan| plan.member_id)),
1546 write,
1547 )?);
1548 let test = T::binary(
1549 "==",
1550 T::discriminant("v"),
1551 T::case_label(ast, discriminator, value),
1552 );
1553 chain = Some(T::block(vec![T::when(test, overlay, chain)]));
1554 }
1555 fallback = chain;
1556 }
1557
1558 // Built from the last arm backwards, so the chain nests the way it
1559 // was declared.
1560 let mut chain: Option<T::Block> = fallback;
1561 for (condition, block) in arms.into_iter().rev() {
1562 let Some(condition) = condition else {
1563 continue;
1564 };
1565 chain = Some(T::block(vec![T::when(condition, block, chain)]));
1566 }
1567 if let Some(chain) = chain {
1568 statements.extend(T::into_statements(chain));
1569 }
1570
1571 // Rule (28): the parameter list a `@mutable` union writes under
1572 // XCDR1 ends at its sentinel, the same as a structure's.
1573 if layout.needs_sentinel {
1574 statements.push(call::<T>("write_sentinel", Vec::new()));
1575 }
1576 statements.push(finish_dheader::<T>());
1577 if packed {
1578 statements.push(finish_packed::<T>());
1579 }
1580 Ok(statements)
1581}
1582
1583/// The width the union overlay writes at — the arm's wire extent,
1584/// where it is fixed at 1, 2, 4 or 8 octets.
1585///
1586/// `zerodds-wire-codec`'s `storage_width`, asked of the same plan. The
1587/// refusal is loud rather than silent because
1588/// `union_negative_selects_nothing` answers `true` only for unions
1589/// whose arms all have such a width; reaching the error means that
1590/// crossing and this reading of it have diverged.
1591fn overlay_width(
1592 ast: &Ast,
1593 type_: Id<TypeNode>,
1594 key: LayoutKey,
1595 member: &str,
1596) -> Result<u8, EncodeError> {
1597 let mut deps = DepCollector::default();
1598 let width = zerodds_ast_layout::xcdr::plan(ast, type_, key, &mut deps)
1599 .ok()
1600 .and_then(|layout| layout.extent.sized())
1601 .filter(|width| matches!(width, 1 | 2 | 4 | 8));
1602 match width {
1603 #[allow(clippy::cast_possible_truncation)]
1604 Some(width) => Ok(width as u8),
1605 None => Err(EncodeError::NoWriter {
1606 member: member.to_string(),
1607 kind: "a union branch with no fixed-width storage to overlay",
1608 }),
1609 }
1610}
1611
1612/// What the overlay reads the suppressed arm's bits through.
1613///
1614/// The scalar instruction decides: an integer, character, boolean,
1615/// enumeration or bitmask holder has storage bits, a float has its
1616/// IEEE 754 bits, and anything else — a string, a `fixed`, a `wchar`
1617/// that carries GIOP's length octet — has no fixed-width pattern and
1618/// is refused with the same sentence `overlay_width` uses.
1619fn bit_pattern_source(
1620 ast: &Ast,
1621 type_: Id<TypeNode>,
1622 key: LayoutKey,
1623 member: &str,
1624) -> Result<crate::emit::BitPatternSource, EncodeError> {
1625 use crate::emit::BitPatternSource as Source;
1626 let no_pattern = || EncodeError::NoWriter {
1627 member: member.to_string(),
1628 kind: "a union branch with no fixed-width storage to overlay",
1629 };
1630 let octets_of = |bits: u16| match bits {
1631 1..=8 => 1,
1632 9..=16 => 2,
1633 17..=32 => 4,
1634 _ => 8,
1635 };
1636 match scalar_write(ast, type_, key, WcharPosition::Member).ok_or_else(no_pattern)? {
1637 ScalarWrite::Bool => Ok(Source::Bool),
1638 ScalarWrite::U8 | ScalarWrite::I8 | ScalarWrite::Char => Ok(Source::Int { octets: 1 }),
1639 ScalarWrite::I16 | ScalarWrite::U16 => Ok(Source::Int { octets: 2 }),
1640 ScalarWrite::I32 | ScalarWrite::U32 => Ok(Source::Int { octets: 4 }),
1641 ScalarWrite::I64 | ScalarWrite::U64 => Ok(Source::Int { octets: 8 }),
1642 ScalarWrite::F32 => Ok(Source::Float32),
1643 ScalarWrite::F64 => Ok(Source::Float64),
1644 ScalarWrite::Wchar {
1645 width,
1646 length_octet: false,
1647 ..
1648 } => Ok(Source::Int {
1649 octets: match width {
1650 WcharWidth::Two => 2,
1651 WcharWidth::Four => 4,
1652 },
1653 }),
1654 ScalarWrite::Enum { bits } | ScalarWrite::Bitmask { bits } => Ok(Source::Int {
1655 octets: octets_of(bits),
1656 }),
1657 _ => Err(no_pattern()),
1658 }
1659}
1660
1661/// One value behind the `MMEMBER` header the plan gave it, or bare.
1662///
1663/// `None` is a union of any extensibility but `@mutable`, where rules
1664/// (26) and (30) frame nothing per member.
1665///
1666/// `member_type` is the member's type, which the per-sample header form
1667/// needs: a constructed member takes `PID_EXTENDED` in a sample past
1668/// RTI's step and the short form in one below it.
1669#[allow(clippy::too_many_arguments)]
1670fn mmember<T: CodecTarget>(
1671 ast: &Ast,
1672 key: LayoutKey,
1673 header: Option<MemberHeader>,
1674 member_type: Id<TypeNode>,
1675 member_id: u32,
1676 handle: &str,
1677 value: T::Stmt,
1678) -> Result<Vec<T::Stmt>, EncodeError> {
1679 Ok(match header {
1680 None | Some(MemberHeader::NoHeader) => vec![value],
1681 Some(MemberHeader::PidCdr1 {
1682 pid,
1683 extended,
1684 must_understand,
1685 }) => {
1686 let mut statements = open_pid_header::<T>(
1687 ast,
1688 key,
1689 handle,
1690 pid,
1691 member_id,
1692 extended,
1693 Constant::ParameterMember,
1694 must_understand,
1695 member_type,
1696 )?;
1697 statements.push(value);
1698 statements.push(call::<T>("finish_pid", vec![T::name(handle)]));
1699 statements
1700 }
1701 Some(MemberHeader::EmHeader {
1702 lc,
1703 must_understand,
1704 }) => vec![
1705 T::declare(
1706 T::handle(Handle::EmHeader),
1707 handle,
1708 Some(call_expr::<T>(
1709 "write_emheader",
1710 vec![
1711 int::<T>(i128::from(member_id)),
1712 int::<T>(i128::from(lc.bits())),
1713 T::literal(Literal::Bool(must_understand)),
1714 ],
1715 )),
1716 ),
1717 value,
1718 call::<T>("finish_emheader", vec![T::name(handle)]),
1719 ],
1720 // Neither `OPT_FMEMBER` form reaches a union: a case member is
1721 // selected by the discriminator and is never `@optional`.
1722 Some(MemberHeader::OptionalCdr1 { .. } | MemberHeader::OptionalFlag) => vec![value],
1723 })
1724}
1725
1726/// The statements that open a PL_CDR1 parameter header for a member.
1727///
1728/// The form is fixed for every plan but
1729/// [`per_sample`], and under that plan it is fixed too for a member whose
1730/// id or length forces the extended form and for one whose type keeps the
1731/// short form ([`zerodds_ast_layout::keeps_short_header`]). The remaining
1732/// case — a constructed member of a sample whose type decides — writes a
1733/// run-time branch on [`EXTENDED_PARAM`], which is the decision the
1734/// sample's root handed down.
1735#[allow(clippy::too_many_arguments)]
1736fn open_pid_header<T: CodecTarget>(
1737 ast: &Ast,
1738 key: LayoutKey,
1739 handle: &str,
1740 pid: u16,
1741 member_id: u32,
1742 extended: bool,
1743 role: Constant,
1744 must_understand: bool,
1745 member_type: Id<TypeNode>,
1746) -> Result<Vec<T::Stmt>, EncodeError> {
1747 let flag = T::literal(Literal::Bool(must_understand));
1748 let extended_call = call_expr::<T>(
1749 "write_pid_extended",
1750 vec![
1751 int::<T>(i128::from(member_id)),
1752 T::constant(role),
1753 flag.clone(),
1754 ],
1755 );
1756 let short_call = call_expr::<T>(
1757 "write_pid",
1758 vec![int::<T>(i128::from(pid)), T::constant(role), flag],
1759 );
1760 if !per_sample(key) || extended {
1761 return Ok(vec![T::declare(
1762 T::handle(Handle::Pid),
1763 handle,
1764 Some(if extended { extended_call } else { short_call }),
1765 )]);
1766 }
1767 if zerodds_ast_layout::keeps_short_header(ast, member_type)
1768 .map_err(crate::encode::plan_refusal)?
1769 {
1770 return Ok(vec![T::declare(
1771 T::handle(Handle::Pid),
1772 handle,
1773 Some(short_call),
1774 )]);
1775 }
1776 let choose = |call: T::Expr| T::block(vec![T::perform(T::binary("=", T::name(handle), call))]);
1777 Ok(vec![
1778 // A local the body **assigns** before it reads, in both arms of
1779 // the `when` below — not [`CodecTarget::storage`], which a Java
1780 // local the body writes *into* needs and which allocates. A
1781 // Java handle class is constructed by the runtime and carries no
1782 // accessible no-argument constructor: `runtime/java`'s
1783 // `Writer.Pid(int, boolean, int, ParameterRole, int, boolean)` is
1784 // package-private, so `new Writer.Pid()` is not a program javac
1785 // accepts, and the `declare` spelling is what leaves it
1786 // definitely assigned here.
1787 T::declare(T::handle(Handle::Pid), handle, None),
1788 T::when(
1789 T::name(EXTENDED_PARAM),
1790 choose(extended_call),
1791 Some(choose(short_call)),
1792 ),
1793 ])
1794}
1795
1796/// The name, and resolved type, of the member with this identity.
1797///
1798/// `all_members_of`, so a member the aggregate inherited is found here
1799/// and not skipped as though the plan had never named it.
1800fn member_by_node(
1801 ast: &Ast,
1802 aggregate: Id<TypeNode>,
1803 node: NodeId,
1804) -> Option<(String, Id<TypeNode>)> {
1805 let id = ast
1806 .all_members_of(aggregate)
1807 .into_iter()
1808 .find(|id| ast.member(*id).is_ok_and(|m| m.node_id == node))?;
1809 let member = ast.member(id).ok()?;
1810 Some((member.name.clone(), member.type_.resolved()?))
1811}
1812
1813/// The handle a `DHEADER` is opened into, or the no-op form.
1814///
1815/// Declared either way so that the body's shape does not depend on the
1816/// plan's answer; the no-op handle writes nothing and finishes nothing.
1817fn dheader_declaration<T: CodecTarget>(present: bool) -> T::Stmt {
1818 T::declare(
1819 T::handle(Handle::DHeader),
1820 "d",
1821 Some(if present {
1822 T::runtime("w", "write_dheader", Vec::new())
1823 } else {
1824 T::runtime_static("no_dheader", Vec::new())
1825 }),
1826 )
1827}
1828
1829fn finish_dheader<T: CodecTarget>() -> T::Stmt {
1830 call::<T>("finish_dheader", vec![T::name("d")])
1831}
1832
1833/// The local a body's packed-element origin is saved in.
1834///
1835/// A distinct name from the DHEADER handle `d` and the element-length
1836/// handle `zd_el`, because all three can sit in one body: a packed
1837/// sequence writes an element length inside a packed origin.
1838const PACKED_SAVED: &str = "zd_saved";
1839
1840/// `size_t zd_saved = begin_packed_element(w);`
1841///
1842/// [`ElementPadding::Packed`] is HDDS's measured departure from §7.4.1:
1843/// a collection element and an aggregate value count their alignment
1844/// from their own first octet instead of from the encapsulation. The
1845/// generated codec holds the displaced origin and restores it with
1846/// [`finish_packed`].
1847fn begin_packed<T: CodecTarget>() -> T::Stmt {
1848 T::declare(
1849 T::scalar(Scalar::Size),
1850 PACKED_SAVED,
1851 Some(T::runtime("w", "begin_packed_element", Vec::new())),
1852 )
1853}
1854
1855/// `finish_packed_element(w, zd_saved);`
1856fn finish_packed<T: CodecTarget>() -> T::Stmt {
1857 T::perform(T::runtime(
1858 "w",
1859 "finish_packed_element",
1860 vec![T::name(PACKED_SAVED)],
1861 ))
1862}
1863
1864/// The handle a sequence element's own octet length is opened into.
1865///
1866/// [`zerodds_ast_layout::key::SequenceElementDelimiter::BeforeEachNonFinalElement`]
1867/// writes a four-octet `UInt32` in front of every element of a
1868/// `sequence` whose element type is not `@final`, under both encoding
1869/// versions. It is a DHEADER in form — the length is the element's own
1870/// octet count — so the runtime's `write_dheader` / `finish_dheader`
1871/// pair writes it.
1872fn element_length_declaration<T: CodecTarget>() -> T::Stmt {
1873 T::declare(
1874 T::handle(Handle::DHeader),
1875 "zd_el",
1876 Some(T::runtime("w", "write_dheader", Vec::new())),
1877 )
1878}
1879
1880fn finish_element_length<T: CodecTarget>() -> T::Stmt {
1881 T::perform(T::runtime("w", "finish_dheader", vec![T::name("zd_el")]))
1882}
1883
1884/// Whether an element of a `sequence` is preceded by its own octet
1885/// length under `key`.
1886///
1887/// `zerodds_ast_layout::xcdr::sequence_element_carries_length`, the rule
1888/// `zerodds_wire_codec` and the generated Rust ask too: a four-octet `UInt32` in front of every
1889/// element whose type is not `@final`. A type with no extensibility of
1890/// its own — a primitive, a string, an enumeration, a `typedef` — is
1891/// `@final` by construction and takes none; only the four aggregate
1892/// kinds can be `@appendable` or `@mutable`.
1893///
1894/// # Errors
1895/// [`EncodeError::NoLayout`] for an element id the model does not hold:
1896/// whether the element carries a length cannot be told, and guessing
1897/// either way frames every element of the sequence wrong.
1898pub(crate) fn sequence_element_carries_length(
1899 ast: &Ast,
1900 element: Id<TypeNode>,
1901 key: LayoutKey,
1902) -> Result<bool, EncodeError> {
1903 // The planner's rule, called rather than restated, so the generated
1904 // codecs, `zerodds-wire-codec` and the generated Rust cannot answer
1905 // differently about the same element.
1906 zerodds_ast_layout::xcdr::sequence_element_carries_length(ast, element, key).ok_or_else(|| {
1907 EncodeError::NoLayout {
1908 detail: format!(
1909 "the sequence element {element:?} is not a type the model holds, so whether it \
1910 carries its own length cannot be told"
1911 ),
1912 }
1913 })
1914}
1915
1916/// The runtime call one scalar position asks for.
1917///
1918/// Every policy is already answered: `zerodds_ast_layout::scalar_write`
1919/// resolved them against the key and the position, so nothing here
1920/// reads an axis and nothing here can pass the wrong argument to one.
1921fn primitive_write_at<T: CodecTarget>(write: ScalarWrite, value: T::Expr) -> T::Stmt {
1922 match write {
1923 ScalarWrite::Bool => call::<T>("write_bool", vec![value]),
1924 ScalarWrite::U8 => call::<T>("write_u8", vec![value]),
1925 ScalarWrite::I8 => call::<T>("write_i8", vec![value]),
1926 // The runtime takes a code point and keeps the low byte, which
1927 // is where the narrowing belongs: a caller that truncated first
1928 // would put a different character on the wire.
1929 ScalarWrite::Char => call::<T>("write_char", vec![T::code_point(value, true)]),
1930 ScalarWrite::Wchar {
1931 width,
1932 length_octet,
1933 order,
1934 } => call::<T>(
1935 "write_wchar",
1936 vec![
1937 T::code_point(value, false),
1938 T::constant(match width {
1939 WcharWidth::Two => Constant::WcharWidth2,
1940 WcharWidth::Four => Constant::WcharWidth4,
1941 }),
1942 T::constant(if length_octet {
1943 Constant::WcharGiopLengthPrefixed
1944 } else {
1945 Constant::WcharFixed
1946 }),
1947 T::constant(wstring_unit_order(order)),
1948 ],
1949 ),
1950 ScalarWrite::I16 => call::<T>("write_i16", vec![value]),
1951 ScalarWrite::U16 => call::<T>("write_u16", vec![value]),
1952 ScalarWrite::I32 => call::<T>("write_i32", vec![value]),
1953 ScalarWrite::U32 => call::<T>("write_u32", vec![value]),
1954 ScalarWrite::I64 => call::<T>("write_i64", vec![value]),
1955 ScalarWrite::U64 => call::<T>("write_u64", vec![value]),
1956 ScalarWrite::F32 => call::<T>("write_f32", vec![value]),
1957 ScalarWrite::F64 => call::<T>("write_f64", vec![value]),
1958 ScalarWrite::F128 => call::<T>("write_f128", vec![value]),
1959 ScalarWrite::Str => call::<T>("write_string", vec![value]),
1960 ScalarWrite::Wstr {
1961 policy,
1962 order,
1963 code_units,
1964 } => match code_units {
1965 // `WstringCodeUnits::Utf8`: the characters' own octets, with
1966 // the length the UTF-8 octet count and only C2 — the
1967 // terminator — still reaching the wire. The order is inert
1968 // and is not passed.
1969 WstringCodeUnits::Utf8 => call::<T>(
1970 "write_wstring_utf8_octets",
1971 vec![value, T::constant(wstring_policy(policy))],
1972 ),
1973 WstringCodeUnits::Utf16 => call::<T>(
1974 "write_wstring_utf8",
1975 vec![
1976 value,
1977 T::constant(wstring_policy(policy)),
1978 T::constant(wstring_unit_order(order)),
1979 ],
1980 ),
1981 },
1982 ScalarWrite::Fixed {
1983 digits,
1984 scale,
1985 count,
1986 } => call::<T>(
1987 "write_fixed",
1988 vec![
1989 value,
1990 int::<T>(i128::from(digits)),
1991 int::<T>(i128::from(scale)),
1992 T::constant(fixed_digit_count(count)),
1993 ],
1994 ),
1995 ScalarWrite::Enum { bits } => {
1996 call::<T>("write_enum", vec![int::<T>(i128::from(bits)), value])
1997 }
1998 ScalarWrite::Bitmask { bits } => call::<T>(
1999 "write_bitmask",
2000 vec![
2001 int::<T>(i128::from(bits)),
2002 cast::<T>(T::scalar(Scalar::UInt64), value),
2003 ],
2004 ),
2005 }
2006}
2007
2008const fn wstring_policy(policy: WstringLengthPolicy) -> Constant {
2009 match policy {
2010 WstringLengthPolicy::SPECIFICATION => Constant::WstringSpecification,
2011 WstringLengthPolicy::CHARACTER_COUNT => Constant::WstringCharacterCount,
2012 WstringLengthPolicy::CHARACTER_COUNT_TERMINATED => {
2013 Constant::WstringCharacterCountTerminated
2014 }
2015 WstringLengthPolicy::SPECIFICATION_WIDE_UNITS => Constant::WstringSpecificationWideUnits,
2016 WstringLengthPolicy::ROS_TYPE_SUPPORT => Constant::WstringRosTypeSupport,
2017 // The three cells no measured implementation writes. Named on
2018 // both sides so the C emitter can reach every cell the Rust
2019 // model can; a mapping that stopped at the measured five would
2020 // put the emitters back where the enumeration had them.
2021 _ => match (policy.counts, policy.terminator, policy.unit) {
2022 (WstringLengthCounts::Octets, _, WstringUnitWidth::Two) => {
2023 Constant::WstringOctetsTerminated2
2024 }
2025 (WstringLengthCounts::Octets, _, WstringUnitWidth::Four) => {
2026 Constant::WstringOctetsTerminated4
2027 }
2028 _ => Constant::WstringCharactersTerminated2,
2029 },
2030 }
2031}
2032
2033/// Whether a codec call for `element` has to be replaced by an inline
2034/// write because the element's `wchar` framing is not the member's.
2035///
2036/// The generated codec for a type is one function, named for the type
2037/// and not for where it is called from, so a `Char16` element under
2038/// [`WcharFraming::GiopLengthPrefixedMemberOnly`] cannot reach the right
2039/// framing through `zd_enc_p_wchar`. Rather than emit a second function
2040/// for the one primitive that needs one, the collection emitters write
2041/// the call out here with the element's own constant.
2042///
2043/// `None` for every other element and every other framing, so the
2044/// generated code is unchanged wherever the two positions agree — which
2045/// is every dialect but TAO's.
2046fn element_write<T: CodecTarget>(
2047 ast: &Ast,
2048 element: Id<TypeNode>,
2049 key: LayoutKey,
2050 value: T::Expr,
2051) -> Option<T::Stmt> {
2052 if key.wchar_framing.has_length_octet(WcharPosition::Member)
2053 == key.wchar_framing.has_length_octet(WcharPosition::Element)
2054 {
2055 return None;
2056 }
2057 if !matches!(
2058 resolve_primitive(ast, element, 0),
2059 Some(PrimitiveKind::WChar)
2060 ) {
2061 return None;
2062 }
2063 let write = scalar_write(ast, element, key, WcharPosition::Element)?;
2064 Some(primitive_write_at::<T>(write, value))
2065}
2066
2067/// Whether the elements of a collection of `element` are octets a target
2068/// may copy as one run: `octet` or `uint8`, through typedefs. `int8` is
2069/// left to the loop, since its storage is a signed type in the targets
2070/// that have one.
2071pub(crate) fn octet_elements(ast: &Ast, element: Id<TypeNode>) -> bool {
2072 matches!(
2073 resolve_primitive(ast, element, 0),
2074 Some(PrimitiveKind::Octet | PrimitiveKind::UInt8)
2075 )
2076}
2077
2078/// [`CodecTarget::bulk_octets_write`] for `count` elements of `v`, when
2079/// they are octets ([`octet_elements`]) and the target has the call.
2080/// `None` keeps the element loop.
2081fn bulk_write<T: CodecTarget>(ast: &Ast, element: Id<TypeNode>, count: T::Expr) -> Option<T::Stmt> {
2082 if octet_elements(ast, element) {
2083 T::bulk_octets_write("v", count)
2084 } else {
2085 None
2086 }
2087}
2088
2089/// The read side of [`bulk_write`], for `crate::emit::decode`.
2090pub(crate) fn bulk_read<T: CodecTarget>(
2091 ast: &Ast,
2092 element: Id<TypeNode>,
2093 count: T::Expr,
2094) -> Option<T::Stmt> {
2095 if octet_elements(ast, element) {
2096 T::bulk_octets_read("v", count)
2097 } else {
2098 None
2099 }
2100}
2101
2102/// The octet types among `types` whose codec nothing calls once `bulk`
2103/// holds: an `octet` or `uint8`, or a typedef of one, reached only as
2104/// the elements of a `sequence` or a one-dimensional array that the
2105/// target copies in one piece ([`CodecTarget::bulk_octets_write`]).
2106///
2107/// Such a codec is left out rather than emitted and excused: a `static`
2108/// function nothing calls is `-Wunused-function` under clang, in a
2109/// header too, and [`maybe_unused`] is the one codec that may have no
2110/// caller. The walk starts from every type that is not an octet type —
2111/// each of those is emitted as before — and follows the edges a codec
2112/// calls along ([`crate::adapter::c::dependencies`]), minus the ones a
2113/// bulk copy replaced. An octet type it reaches is called; the rest are
2114/// the answer. Empty when `bulk` does not hold.
2115pub(crate) fn called_only_in_bulk(
2116 ast: &Ast,
2117 types: &[Id<TypeNode>],
2118 entries: &[Id<TypeNode>],
2119 bulk: bool,
2120) -> Vec<Id<TypeNode>> {
2121 if !bulk {
2122 return Vec::new();
2123 }
2124 let octets: Vec<Id<TypeNode>> = types
2125 .iter()
2126 .copied()
2127 .filter(|id| octet_elements(ast, *id))
2128 .collect();
2129 if octets.is_empty() {
2130 return Vec::new();
2131 }
2132 // An octet type the caller of the codec names (`entries`) is called by
2133 // that caller: the RPC binding calls the codec of an `octet` parameter.
2134 let mut called: Vec<Id<TypeNode>> = octets
2135 .iter()
2136 .copied()
2137 .filter(|id| entries.contains(id))
2138 .collect();
2139 let mut pending: Vec<Id<TypeNode>> = types
2140 .iter()
2141 .copied()
2142 .filter(|id| !octets.contains(id))
2143 .collect();
2144 while let Some(caller) = pending.pop() {
2145 let copied_in_bulk = match ast.type_(caller).map(|node| &node.kind) {
2146 Ok(TypeKind::Sequence { element, .. }) => element
2147 .resolved()
2148 .is_some_and(|element| octet_elements(ast, element)),
2149 Ok(TypeKind::Array { element, dims }) => {
2150 dims.len() == 1
2151 && element
2152 .resolved()
2153 .is_some_and(|element| octet_elements(ast, element))
2154 }
2155 _ => false,
2156 };
2157 if copied_in_bulk {
2158 continue;
2159 }
2160 for callee in crate::adapter::c::dependencies(ast, caller) {
2161 if octets.contains(&callee) && !called.contains(&callee) {
2162 called.push(callee);
2163 pending.push(callee);
2164 }
2165 }
2166 }
2167 octets
2168 .into_iter()
2169 .filter(|id| !called.contains(id))
2170 .collect()
2171}
2172
2173/// The primitive kind `id` is once typedefs are seen through, if it is
2174/// one.
2175///
2176/// zerodds-lint: recursion-depth 32 (typedef chains; bounded by the
2177/// `depth > 32` check, past which the answer is `None` — not a
2178/// primitive)
2179pub(crate) fn resolve_primitive(
2180 ast: &Ast,
2181 id: Id<TypeNode>,
2182 depth: usize,
2183) -> Option<PrimitiveKind> {
2184 if depth > 32 {
2185 return None;
2186 }
2187 match ast.type_(id).map(|node| node.kind.clone()) {
2188 Ok(TypeKind::Primitive(kind)) => Some(kind),
2189 Ok(TypeKind::Alias { aliased }) => aliased
2190 .resolved()
2191 .and_then(|target| resolve_primitive(ast, target, depth + 1)),
2192 _ => None,
2193 }
2194}
2195
2196/// The C name of a `wstring` unit order.
2197const fn wstring_unit_order(order: WstringUnitOrder) -> Constant {
2198 match order {
2199 WstringUnitOrder::Stream => Constant::WstringUnitsStream,
2200 WstringUnitOrder::BigEndianAlways => Constant::WstringUnitsBigEndianAlways,
2201 WstringUnitOrder::ByteOrderMark => Constant::WstringUnitsByteOrderMark,
2202 WstringUnitOrder::HostUnswapped => Constant::WstringUnitsHostUnswapped,
2203 }
2204}
2205
2206/// The runtime's name for a [`FixedDigitCount`].
2207///
2208/// The C runtime implements the count from [CDR] §9.3.2.8 itself, the
2209/// same way it implements the packing; this only names which of the three
2210/// the caller asked for.
2211const fn fixed_digit_count(policy: FixedDigitCount) -> Constant {
2212 match policy {
2213 FixedDigitCount::Declared => Constant::FixedDeclared,
2214 FixedDigitCount::ValuePrecision => Constant::FixedValuePrecision,
2215 FixedDigitCount::ValueStringDigits => Constant::FixedValueStringDigits,
2216 }
2217}
2218
2219// -- a map's entry order -------------------------------------------------
2220
2221/// `for (uint32_t <index> = 0; <index> < v->_length; ...) { body }`.
2222///
2223/// One builder, because the entry order emits three of these nested and
2224/// three literal copies is how the three would come to disagree about
2225/// the bound.
2226fn over_the_entries<T: CodecTarget>(index: &str, body: T::Block) -> T::Stmt {
2227 T::count_up(index, T::count_of("v", None), body)
2228}
2229
2230/// The entries in ascending key order, without a scratch array.
2231///
2232/// ```text
2233/// for (uint32_t zd_rank = 0; zd_rank < v->_length; …)
2234/// for (uint32_t i = 0; i < v->_length; …) {
2235/// uint32_t zd_before = 0;
2236/// for (uint32_t zd_k = 0; zd_k < v->_length; …)
2237/// if (<entry zd_k sorts before entry i>) zd_before = zd_before + 1;
2238/// if (zd_before == zd_rank) { <write the pair at i> }
2239/// }
2240/// ```
2241///
2242/// A rank and not a sort. The generated writer has no allocation and no
2243/// variable-length array — `runtime/c` allocates nothing in the write
2244/// path and an embedded target links it — so there is nowhere to put a
2245/// permutation. Computing each entry's rank in place needs no storage at
2246/// all, and a corpus map has at most three entries.
2247///
2248/// "Sorts before" carries the index as the tie-break, so two entries
2249/// with equal keys keep the order the sample declared: the same stable
2250/// order `encode.rs::map_entry_order` and Python's `sorted` give.
2251fn ascending_by_key<T: CodecTarget>(
2252 ast: &Ast,
2253 key_type: Id<TypeNode>,
2254 ordering: MapKeyOrdering,
2255 pair: T::Block,
2256) -> T::Stmt {
2257 let before = map_key_before::<T>(ast, key_type, ordering, "zd_k", "i");
2258 let count = over_the_entries::<T>(
2259 "zd_k",
2260 T::block(vec![T::when(
2261 before,
2262 T::block(vec![T::perform(increment::<T>("zd_before"))]),
2263 None,
2264 )]),
2265 );
2266 let select = over_the_entries::<T>(
2267 "i",
2268 T::block(vec![
2269 T::declare(T::scalar(Scalar::UInt32), "zd_before", Some(int::<T>(0))),
2270 count,
2271 T::when(
2272 T::binary("==", T::name("zd_before"), T::name("zd_rank")),
2273 pair,
2274 None,
2275 ),
2276 ]),
2277 );
2278 over_the_entries::<T>("zd_rank", T::block(vec![select]))
2279}
2280
2281/// Whether the entry at `left` sorts before the entry at `right`.
2282///
2283/// `less(left, right) || (!less(right, left) && left < right)` — the key
2284/// order with the declared position as the tie-break.
2285fn map_key_before<T: CodecTarget>(
2286 ast: &Ast,
2287 key_type: Id<TypeNode>,
2288 ordering: MapKeyOrdering,
2289 left: &str,
2290 right: &str,
2291) -> T::Expr {
2292 let less = |a: &str, b: &str| map_key_less::<T>(ast, key_type, ordering, a, b);
2293 T::binary(
2294 "||",
2295 T::paren(less(left, right)),
2296 T::paren(T::binary(
2297 "&&",
2298 T::unary("!", T::paren(less(right, left))),
2299 T::paren(T::binary("<", T::name(left), T::name(right))),
2300 )),
2301 )
2302}
2303
2304/// Whether the key at `left` is less than the key at `right`.
2305///
2306/// The comparison says the signedness out loud rather than taking the C
2307/// type's. An IDL `char` is a C `char`, whose signedness is
2308/// implementation-defined; the corpus was recorded on a host where it is
2309/// signed, and the cast pins that answer so the three columns still
2310/// agree on a host where it is not. Every other key type has a C
2311/// counterpart whose own `<` is the order the crossing named — `uint8_t`
2312/// and `uint16_t` and `bool` unsigned, the sized signed integers signed,
2313/// a generated `enum` by its declared values. A `string` and a `wstring`
2314/// are both a `const char *` holding UTF-8, and `strcmp` compares as
2315/// `unsigned char` (C11 §7.24.4), which is `std::string`'s order and,
2316/// UTF-8 being order preserving, `std::wstring`'s too.
2317fn map_key_less<T: CodecTarget>(
2318 ast: &Ast,
2319 key_type: Id<TypeNode>,
2320 ordering: MapKeyOrdering,
2321 left: &str,
2322 right: &str,
2323) -> T::Expr {
2324 let key_at = |index: &str| {
2325 let half = T::entry_half(T::index(T::elements("v"), T::name(index)), true);
2326 // An enumeration key is ordered by its enumerator *value*, and
2327 // a column whose enumeration is a string at the language
2328 // surface would otherwise compare the names. The identity
2329 // default leaves every other column's comparison where it was.
2330 if ordering == MapKeyOrdering::EnumeratorValue {
2331 return T::enumerator_ordinal(ast, key_type, half);
2332 }
2333 half
2334 };
2335 if ordering == MapKeyOrdering::CodeUnits {
2336 return T::binary(
2337 "<",
2338 T::call("strcmp", vec![key_at(left), key_at(right)]),
2339 int::<T>(0),
2340 );
2341 }
2342 let signed_char = ordering == MapKeyOrdering::SignedInteger
2343 && matches!(
2344 resolve_primitive(ast, key_type, 0),
2345 Some(PrimitiveKind::Char)
2346 );
2347 let read = |index: &str| {
2348 if signed_char {
2349 T::paren(cast::<T>(T::named("signed char"), key_at(index)))
2350 } else {
2351 key_at(index)
2352 }
2353 };
2354 // An eight-octet unsigned key crosses the target's own seam: a
2355 // column that holds `unsigned long long` as a bit pattern in its
2356 // widest signed type has no `<` that answers the unsigned order.
2357 // `CodecTarget::unsigned_less` says which columns those are.
2358 if ordering == MapKeyOrdering::UnsignedInteger
2359 && matches!(
2360 resolve_primitive(ast, key_type, 0),
2361 Some(PrimitiveKind::UInt64)
2362 )
2363 {
2364 return T::unsigned_less(8, read(left), read(right));
2365 }
2366 // A `boolean` key crosses the same kind of seam as the eight-octet
2367 // unsigned one: not every target's `<` takes two booleans.
2368 // `CodecTarget::bool_less` says how each column spells the order.
2369 if matches!(
2370 resolve_primitive(ast, key_type, 0),
2371 Some(PrimitiveKind::Boolean)
2372 ) {
2373 return T::bool_less(read(left), read(right));
2374 }
2375 T::binary("<", read(left), read(right))
2376}
2377
2378/// An enumeration's write body: the holder's width, and the integer that
2379/// goes into it.
2380///
2381/// The generated C `enum` keeps its **declared** constants — that is the
2382/// language mapping and not a wire question — so under
2383/// [`zerodds_ast_layout::EnumeratorValue::Ordinal`] the value in the
2384/// variable and the value on the wire are different integers, and a
2385/// chain maps one to the other. Under `Declared` the chain is the
2386/// identity and is not emitted at all, so the default column's C is
2387/// unchanged.
2388///
2389/// The reverse chain is `decode_c.rs::enumeration_body`, which compares
2390/// the wire pattern and assigns the declared constant.
2391///
2392/// # Why an enumerator this cannot answer for is an error
2393///
2394/// `zd_wire` is initialised to the **declared** value and each arm
2395/// overwrites it, so an enumerator that reaches no arm is written with
2396/// the value the variable already holds. Under `Declared` that is the
2397/// right integer and the chain is not emitted at all; under `Ordinal`
2398/// it is a different integer, and the generated C would write the
2399/// declared value, return success, and disagree with every other column
2400/// on the same setting. The reader's chain in
2401/// `decode_c.rs::enumeration_body` can skip an arm because a pattern
2402/// with no arm is refused by name — an absent arm there is a refusal
2403/// and here it is a wrong value, which is why the two sides answer
2404/// differently.
2405///
2406/// # Errors
2407/// [`EncodeError::NoEnumeratorValue`] for an enumerator whose handle the
2408/// model no longer holds, or whose wire value the setting cannot
2409/// compute.
2410fn enumeration_write<T: CodecTarget>(
2411 ast: &Ast,
2412 id: Id<TypeNode>,
2413 key: LayoutKey,
2414) -> Result<Vec<T::Stmt>, EncodeError> {
2415 let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
2416 return Err(EncodeError::NoWriter {
2417 member: T::type_name(ast, id),
2418 kind: "enumeration the layout plan has no scalar write for",
2419 });
2420 };
2421 // Through the enumerator seam: a column whose enumeration is not a
2422 // number at the language surface has no integer in the variable,
2423 // and the identity default leaves every other column's output where
2424 // it was. `CodecTarget::enumerator_ordinal`.
2425 let holder = cast::<T>(
2426 T::scalar(Scalar::Int64),
2427 T::enumerator_ordinal(ast, id, deref::<T>("v")),
2428 );
2429 // The declared values reach the wire unchanged under the
2430 // specification's setting, so the generated code is one call and
2431 // the chain below is what a front end that drops `@value` needs.
2432 if zerodds_ast_layout::plan::enumerators_are_declared(key) {
2433 return Ok(vec![primitive_write_at::<T>(write, holder)]);
2434 }
2435
2436 let enumeration = ast
2437 .type_(id)
2438 .map_err(|error| EncodeError::NoLayout {
2439 detail: format!("{error}"),
2440 })?
2441 .name
2442 .clone();
2443 // The mapping and not the policy: `enumerator_writes` resolved every
2444 // enumerator against the key, so nothing here can pass the wrong
2445 // setting or answer the axis by omission.
2446 let arms = zerodds_ast_layout::plan::enumerator_writes(ast, id, key).map_err(|enumerator| {
2447 EncodeError::NoEnumeratorValue {
2448 enumeration: enumeration.clone(),
2449 enumerator,
2450 detail: "its wire value does not evaluate under this --enumerator-value setting",
2451 }
2452 })?;
2453
2454 let mut statements = vec![T::declare(
2455 T::scalar(Scalar::Int64),
2456 "zd_wire",
2457 Some(holder),
2458 )];
2459 // Built from the last enumerator backwards so the chain reads in
2460 // declaration order, which is the order the reader's chain takes
2461 // too: where two enumerators share a declared value the first one
2462 // wins in both directions.
2463 let mut chain: Option<T::Block> = None;
2464 for (name, on_wire) in arms.into_iter().rev() {
2465 // Through the enumerator seam and not through
2466 // `module_prefix` plus `escape`, which was the same leak the
2467 // C++ reader found in the other direction. The two are one
2468 // string in C, where an enumerator sits in the enclosing scope
2469 // and is not folded; they are two different strings in every
2470 // column that scopes its enumerations or renames its members.
2471 // The C# column is both — `global::a.Colour.RED` against a
2472 // member fold that would have written `Red`.
2473 let constant = T::enumerator_constant(ast, id, &name);
2474 chain = Some(T::block(vec![T::when(
2475 T::binary("==", deref::<T>("v"), constant),
2476 // `wide_int` and not `int`: `zd_wire` is the Int64 holder,
2477 // and the assigned literal has to be one of that width —
2478 // TypeScript's `bigint` takes `0n` and not `0` (TS2322 on
2479 // every ordinal-mapped enumeration), and a declared value
2480 // above `i32::MAX` needs the suffix in Java, C and C++
2481 // too. For every value inside `i32` each of those columns
2482 // spells `wide_int` exactly as it spelt `int`, so their
2483 // generated files do not move.
2484 T::block(vec![T::perform(T::binary(
2485 "=",
2486 T::name("zd_wire"),
2487 T::wide_int(on_wire),
2488 ))]),
2489 chain,
2490 )]));
2491 }
2492 if let Some(chain) = chain {
2493 statements.extend(T::into_statements(chain));
2494 }
2495 statements.push(primitive_write_at::<T>(write, T::name("zd_wire")));
2496 Ok(statements)
2497}
2498
2499// -- small builders ------------------------------------------------------
2500//
2501// Each of these is now one call into the C backend of
2502// [`crate::emit::CodecTarget`]. They stay as free functions because the
2503// walk above reads better for it and because the argument order is this
2504// file's, not the trait's; what they no longer do is build a C node.
2505// When the walk itself becomes generic these become the trait's methods
2506// at their use sites and disappear.
2507
2508/// A runtime call, evaluated for its effect.
2509///
2510/// `name` is the operation without the runtime's prefix — `write_u32`,
2511/// not `zdw_write_u32` — and the receiver is prepended by the backend.
2512fn call<T: CodecTarget>(name: &str, args: Vec<T::Expr>) -> T::Stmt {
2513 T::perform(T::runtime("w", name, args))
2514}
2515
2516/// The same call where the result is kept.
2517///
2518/// A header-opening operation hands back the position it wrote at, which
2519/// the matching close needs; those bind rather than discard.
2520fn call_expr<T: CodecTarget>(name: &str, args: Vec<T::Expr>) -> T::Expr {
2521 T::runtime("w", name, args)
2522}
2523
2524/// The limit a bound states, where it states one this column can write.
2525///
2526/// `None` for the unbounded form and for a bound that does not
2527/// evaluate; a negative one is not a length and is refused the same
2528/// way. The decoder's own copy is
2529/// [`crate::emit::decode::bound_limit`] — one function because a
2530/// `sequence`, a `string`, a `wstring` and a `map` all read their bound
2531/// the same, and three copies in this file would be three chances for
2532/// one collection to stop being checked while the others were.
2533fn bound_limit(ast: &Ast, bound: &Bound) -> Option<i128> {
2534 bound
2535 .expr()
2536 .and_then(|expr| ast.eval_integral(expr).ok())
2537 .filter(|limit| *limit >= 0)
2538}
2539
2540/// The write for a bounded `string` or a bounded `wstring`.
2541///
2542/// The runtime's `write_string_bounded`/`write_wstring_bounded` takes
2543/// the bound as an argument and refuses a value that overruns it —
2544/// [`crate::encode::MemberWrite::String`] emits the same call for Rust,
2545/// against [`crate::encode`]'s own `write_string_bounded`, and
2546/// `crates/wire::Writer::write_string_bounded`'s remarks are why: the
2547/// bound is a promise about the source and not a difference on the
2548/// wire, so accepting an over-bound value and writing it as if it fit
2549/// would put a sample on the wire nobody wrote, and cutting it at a
2550/// character boundary would put a different one there with nothing to
2551/// say so.
2552///
2553/// # Errors
2554/// [`EncodeError::NoWriter`] for a bound on a scalar write that is
2555/// neither a string nor a wstring — never reached while
2556/// [`zerodds_ast_layout::plan::scalar_write`] answers a `string` type
2557/// with one of the two, which is the only way this is called.
2558fn bounded_string_write<T: CodecTarget>(
2559 own_name: &str,
2560 write: ScalarWrite,
2561 value: T::Expr,
2562 limit: i128,
2563) -> Result<T::Stmt, EncodeError> {
2564 match write {
2565 ScalarWrite::Str => Ok(call::<T>(
2566 "write_string_bounded",
2567 vec![value, T::unsigned_int(limit)],
2568 )),
2569 ScalarWrite::Wstr {
2570 policy,
2571 order,
2572 code_units,
2573 } => Ok(match code_units {
2574 // The bound still counts UTF-16 code units, whatever the wire
2575 // carries; the runtime's octet form measures them and writes
2576 // the UTF-8 octets. See `write_wstring_utf8_octets_bounded`.
2577 WstringCodeUnits::Utf8 => call::<T>(
2578 "write_wstring_utf8_octets_bounded",
2579 vec![
2580 value,
2581 T::unsigned_int(limit),
2582 T::constant(wstring_policy(policy)),
2583 ],
2584 ),
2585 WstringCodeUnits::Utf16 => call::<T>(
2586 "write_wstring_bounded",
2587 vec![
2588 value,
2589 T::unsigned_int(limit),
2590 T::constant(wstring_policy(policy)),
2591 T::constant(wstring_unit_order(order)),
2592 ],
2593 ),
2594 }),
2595 _ => Err(EncodeError::NoWriter {
2596 member: own_name.to_string(),
2597 kind: "a bound on a scalar write that is not a string or a wstring",
2598 }),
2599 }
2600}
2601
2602/// A collection's count, checked against its bound before anything of
2603/// the collection is written.
2604///
2605/// `method` is `check_sequence_bound` or `check_map_bound` — the
2606/// runtime's own names, matching
2607/// [`crate::encode`]'s Rust codegen and `crates/wire::Writer`'s two
2608/// checks of the same shape. Both take the count and the bound and
2609/// refuse loudly, in whatever their language's own mechanism is,
2610/// before the DHEADER or the length word goes out — a sender that
2611/// wrote more than its own declaration allows has written a value of a
2612/// type nobody declared, and stopping after the length word would
2613/// leave that word on the wire ahead of the refusal.
2614fn bound_check<T: CodecTarget>(method: &str, count: T::Expr, limit: i128) -> T::Stmt {
2615 call::<T>(method, vec![count, T::unsigned_int(limit)])
2616}
2617
2618fn deref<T: CodecTarget>(name: &str) -> T::Expr {
2619 T::whole(name)
2620}
2621
2622fn arrow<T: CodecTarget>(value: &str, field: &str) -> T::Expr {
2623 T::member(value, field)
2624}
2625
2626/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2627/// is `T::cast`; `dds_bounded_recursion` compares only the last
2628/// segment of a call path, so it reads as a self-call.
2629fn cast<T: CodecTarget>(type_: T::Type, value: T::Expr) -> T::Expr {
2630 T::cast(type_, value)
2631}
2632
2633/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2634/// is `T::int`; `dds_bounded_recursion` compares only the last
2635/// segment of a call path, so it reads as a self-call.
2636fn int<T: CodecTarget>(value: i128) -> T::Expr {
2637 T::int(value)
2638}
2639
2640fn increment<T: CodecTarget>(name: &str) -> T::Expr {
2641 T::binary(
2642 "=",
2643 T::name(name),
2644 T::binary("+", T::name(name), int::<T>(1)),
2645 )
2646}