zerodds_lang_rules/emit/julia.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Julia backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as `runtime/c`,
6//! `runtime/python`, `runtime/typescript` and the rest do, and none of
7//! them calls another. Where two columns disagree on an octet, one has
8//! misread the specification — which is the only reason to have another.
9//!
10//! # The three decisions this column turns on
11//!
12//! **The declared types are immutable and the decoder still assigns
13//! fields.** `adapter/julia.rs` declares every aggregate as an immutable
14//! `struct` — a decoded sample is a value, not a handle — and the shared
15//! walk writes `v.field = …` at every member. The two meet in the
16//! runtime's `ZdScratch`: a decoder's `v` starts as the Table 9 scratch
17//! its defaulter answers (`crate::default_julia`), every store lands in
18//! the scratch's dictionary, and each `return` goes through
19//! `zd_freeze(zd_rt, v)`, which builds the immutable value by the
20//! declared field order. `zd_rt` is a local the reading body binds to
21//! the sample's own type, because [`CodecTarget::give_back`] has no type
22//! parameter to name it with.
23//!
24//! **A collection under construction is a `Vector{Any}`.** An IDL array
25//! is an `NTuple` — the one shape Julia's own type system fixes the
26//! length of — and an `NTuple` cannot be written into element by
27//! element. The reader therefore fills `Any`-vectors and `zd_freeze`
28//! converts, dimension by dimension. A `map`'s entry under construction
29//! is a `ZdScratch` with `key` and `value` slots for the same reason,
30//! so [`CodecTarget::entry_half`] is one spelling — `.key` / `.value` —
31//! in both directions: a field access reaches a real `ZdMapEntry_…` on
32//! the way out and the scratch's dictionary on the way in.
33//!
34//! **A subscript adds one.** Julia counts from one and the walk counts
35//! from zero; the counter stays the walk's and the `+ 1` is written at
36//! the subscript, in [`CodecTarget::index`], so an index that is also
37//! arithmetic — a rank, a position — keeps the walk's meaning.
38//!
39//! # Two width traps the spellings guard against
40//!
41//! - `for i in 0:(n - 1)` with an unsigned `n` of zero wraps to
42//! `0:0xffffffff` and runs four billion iterations, so every counted
43//! loop bound goes through `Int64(…)` first.
44//! - a bare `1 << 40` is a platform `Int`, and a small hex literal is a
45//! small unsigned type whose `~` loses the high bits, so every 64-bit
46//! pattern is written `UInt64(…)`.
47
48use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
49use crate::adapter::julia::{
50 julia_map_entry_name, julia_module_prefix, julia_type_name, julia_value_type,
51};
52use crate::encode::EncodeError;
53use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
54use zerodds_ast_core::Id;
55use zerodds_lang_cst::common::{Doc, Ident, Literal};
56use zerodds_lang_cst::julia::{self as jl, escape_field, escape_name};
57
58/// The Julia backend.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct Julia;
61
62/// How deep a chain of typedefs and collections is followed.
63const MAX_TYPE_DEPTH: usize = 64;
64
65/// The type name [`CodecTarget::named`] is handed for C's signed `char`.
66///
67/// `emit/encode.rs`'s `map_key_less` writes `T::named("signed char")`
68/// where a `map`'s key is a `char` and the entry order is
69/// `MapKeyOrdering::SignedInteger`. That is a C spelling in the walk and
70/// not a type Julia has; this column recognises the string and answers
71/// with the two's-complement arithmetic. See [`signed_char`].
72const SIGNED_CHAR: &str = "signed char";
73
74/// What [`CodecTarget::map_entry_name`] answers with.
75///
76/// Not a type this column declares. It is a marker the one seam that
77/// receives it — [`Julia::storage`], through the default
78/// `map_entry_type` — matches on, because a `map` entry under
79/// construction is a runtime `ZdScratch` and not the materialiser's
80/// `ZdMapEntry_…` struct, which is immutable and cannot take one half
81/// at a time.
82const MAP_ENTRY: &str = "_ZdMapEntryScratch";
83
84impl Julia {
85 /// A body's statements, unchanged: Julia accepts an empty block.
86 fn suite(body: Vec<jl::Statement>) -> Vec<jl::Statement> {
87 body
88 }
89
90 /// A reader's body, with the scratch it fills and the freeze that
91 /// answers.
92 ///
93 /// `zd_rt = <SampleType>` first, so that every `return` the walk
94 /// builds — [`Julia::give_back`] has no type to hand — can say
95 /// `return zd_freeze(zd_rt, v)`; then `v = zd_def_…()`, Table 9's
96 /// scratch through the generated defaulter; then the walk's own
97 /// statements; then the freeze.
98 fn reading_body(name: &str, sample: jl::Type, body: Vec<jl::Statement>) -> Vec<jl::Statement> {
99 let defaulter = crate::default_julia::defaulter_from_decoder(name);
100 let mut statements = vec![
101 jl::Statement::Assign {
102 place: jl::Expr::name("zd_rt"),
103 value: jl::Expr::Type(Box::new(sample)),
104 },
105 jl::Statement::Assign {
106 place: jl::Expr::name("v"),
107 value: jl::Expr::free_call(&defaulter, Vec::new()),
108 },
109 ];
110 statements.extend(body);
111 statements.push(Self::give_back(None));
112 statements
113 }
114
115 /// A policy value, as the string the runtime accepts.
116 fn text(value: &str) -> jl::Expr {
117 jl::Expr::Literal(Literal::Str(value.to_string()))
118 }
119}
120
121impl CodecTarget for Julia {
122 type Expr = jl::Expr;
123 type Stmt = jl::Statement;
124 type Block = Vec<jl::Statement>;
125 type Type = jl::Type;
126 type Decl = jl::Decl;
127
128 // -- types --------------------------------------------------------
129
130 /// The fixed-width holders `adapter/julia.rs` declares, and `Int64`
131 /// for the walk's own counters and positions.
132 ///
133 /// Never `Int`: that is `Int64` on this host and `Int32` on a
134 /// 32-bit one, for a wire format that fixes every width. A `char`
135 /// is a `UInt8` code unit and a `long double` a `String` holding
136 /// the decimal — both the materialiser's answers, restated here so
137 /// a codec's local and a struct's field are one type.
138 fn scalar(kind: Scalar) -> Self::Type {
139 jl::Type::Primitive(match kind {
140 Scalar::Bool => jl::Primitive::Bool,
141 Scalar::Char | Scalar::UInt8 => jl::Primitive::UInt8,
142 Scalar::Int8 => jl::Primitive::Int8,
143 Scalar::Int16 => jl::Primitive::Int16,
144 Scalar::UInt16 => jl::Primitive::UInt16,
145 Scalar::Int32 => jl::Primitive::Int32,
146 Scalar::UInt32 => jl::Primitive::UInt32,
147 Scalar::UInt64 => jl::Primitive::UInt64,
148 // The walk's counters, byte positions and the enumeration
149 // holder. Signed on purpose: `0:(Int64(n) - 1)` cannot wrap
150 // where an unsigned zero would.
151 Scalar::Int64 | Scalar::Index | Scalar::Size => jl::Primitive::Int64,
152 Scalar::Float => jl::Primitive::Float32,
153 Scalar::Double => jl::Primitive::Float64,
154 Scalar::LongDouble => jl::Primitive::String,
155 })
156 }
157
158 fn named(name: &str) -> Self::Type {
159 jl::Type::named(name)
160 }
161
162 /// `Nothing`, whose one value is what a Julia function with no
163 /// answer returns.
164 fn nothing() -> Self::Type {
165 jl::Type::Primitive(jl::Primitive::Nothing)
166 }
167
168 /// The runtime's handles, by their exported names.
169 fn handle(kind: Handle) -> Self::Type {
170 jl::Type::named(match kind {
171 Handle::DHeader => "ZdDHeader",
172 Handle::EmHeader => "ZdEmHeader",
173 Handle::Pid => "ZdPid",
174 Handle::TryConstruct => "ZdTryConstruct",
175 Handle::ReaderMark => "ZdReaderMark",
176 Handle::PidRead => "ZdPidRead",
177 Handle::EmHeaderRead => "ZdEmHeaderRead",
178 })
179 }
180
181 /// The code unit, unchanged.
182 ///
183 /// `adapter/julia.rs` holds a `char` in a `UInt8` and a `wchar` in
184 /// a `UInt16`, and `runtime/julia`'s `write_char` and `write_wchar`
185 /// take the integer, so there is nothing to convert on either side.
186 fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
187 value
188 }
189
190 /// The runtime's policy values, in the spelling `runtime/julia`
191 /// accepts.
192 ///
193 /// Strings and integers for the wire-form axes — the same spellings
194 /// `runtime/python` takes, because both are
195 /// `WstringLengthPolicy::as_str` and its siblings in
196 /// `crates/ast-layout/src/key.rs` — and the runtime's `@enum`
197 /// instances for the roles, behaviours and statuses. One arm per
198 /// constant and no catch-all: a policy this column has not
199 /// implemented has to be a compile error here rather than a value
200 /// that looks plausible.
201 fn constant(which: Constant) -> Self::Expr {
202 match which {
203 Constant::WcharWidth2 => jl::Expr::int(2),
204 Constant::WcharWidth4 => jl::Expr::int(4),
205 Constant::WcharFixed => Self::text("fixed"),
206 Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
207 Constant::WstringUnitsStream => Self::text("stream"),
208 Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
209 Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
210 Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
211 Constant::WstringSpecification => Self::text("specification"),
212 Constant::WstringCharacterCount => Self::text("character-count"),
213 Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
214 Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
215 Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
216 Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
217 Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
218 Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
219 Constant::FixedDeclared => Self::text("declared"),
220 Constant::FixedValuePrecision => Self::text("value-precision"),
221 Constant::FixedValueStringDigits => Self::text("value-string-digits"),
222 Constant::ParameterMember => jl::Expr::name("ZD_PARAMETER_MEMBER"),
223 Constant::ParameterOptional => jl::Expr::name("ZD_PARAMETER_OPTIONAL"),
224 Constant::TryConstructDiscard => jl::Expr::name("ZD_TRY_CONSTRUCT_DISCARD"),
225 Constant::TryConstructTrim => jl::Expr::name("ZD_TRY_CONSTRUCT_TRIM"),
226 Constant::TryConstructUseDefault => jl::Expr::name("ZD_TRY_CONSTRUCT_USE_DEFAULT"),
227 Constant::StatusOk => jl::Expr::name("ZD_STATUS_OK"),
228 Constant::StatusDiscarded => jl::Expr::name("ZD_STATUS_DISCARDED"),
229 Constant::StatusImpossibleLength => jl::Expr::name("ZD_STATUS_IMPOSSIBLE_LENGTH"),
230 Constant::StatusOutOfRange => jl::Expr::name("ZD_STATUS_OUT_OF_RANGE"),
231 Constant::StatusTruncated => jl::Expr::name("ZD_STATUS_TRUNCATED"),
232 }
233 }
234
235 /// The smallest unsigned integer that holds `bits`.
236 fn unsigned_holder(bits: u16) -> Self::Type {
237 jl::Type::Primitive(match bits {
238 0..=8 => jl::Primitive::UInt8,
239 9..=16 => jl::Primitive::UInt16,
240 17..=32 => jl::Primitive::UInt32,
241 _ => jl::Primitive::UInt64,
242 })
243 }
244
245 fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
246 julia_value_type(ast, id).unwrap_or(jl::Type::Primitive(jl::Primitive::Any))
247 }
248
249 fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
250 codec_tag(ast, id, 0)
251 }
252
253 /// The scratch marker and never the materialiser's entry struct:
254 /// the struct is immutable and a reader fills an entry one half at
255 /// a time. See `MAP_ENTRY`.
256 fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
257 MAP_ENTRY.to_string()
258 }
259
260 /// The flattened module path, which in this column *is* the scope:
261 /// `adapter/julia.rs` writes no `module` and every declaration
262 /// carries its path in its name.
263 fn module_prefix(ast: &Ast, id: Id<TypeNode>) -> String {
264 julia_module_prefix(ast, id)
265 }
266
267 /// The field spelling, which escapes keywords only. A field lives
268 /// in the struct's own namespace and shadows nothing —
269 /// `zerodds_lang_cst::julia::escape_field` carries the measurement.
270 fn escape(name: &str) -> String {
271 escape_field(name)
272 }
273
274 /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
275 fn inherited_path(depth: usize) -> Option<String> {
276 Some(format!("{}.", escape_field("parent")).repeat(depth))
277 }
278
279 // -- expressions ---------------------------------------------------
280
281 fn name(text: &str) -> Self::Expr {
282 jl::Expr::name(text)
283 }
284
285 fn int(value: i128) -> Self::Expr {
286 jl::Expr::int(value)
287 }
288
289 /// The number, unsuffixed.
290 ///
291 /// Julia parses a decimal past `typemax(Int64)` as the next wider
292 /// signed type, and a comparison or a typed-local assignment
293 /// converts exactly, so `16045690984833335998` needs no suffix and
294 /// no widening call.
295 fn wide_int(value: i128) -> Self::Expr {
296 jl::Expr::int(value)
297 }
298
299 /// `UInt64(<n>)` — converted, never bare.
300 ///
301 /// A bare hex literal takes the smallest unsigned type it fits, so
302 /// `~0x07` is a `UInt8` whose complement has lost fifty-six bits by
303 /// the time it meets a `UInt64`; and a bare decimal is a platform
304 /// `Int`. The conversion is exact for every `u64`.
305 fn wide_unsigned(value: u64) -> Self::Expr {
306 jl::Expr::convert(
307 jl::Type::Primitive(jl::Primitive::UInt64),
308 jl::Expr::int(i128::from(value)),
309 )
310 }
311
312 /// As [`Self::wide_int`]: the value survives its own width without
313 /// a suffix.
314 fn wide_signed(value: i128) -> Self::Expr {
315 jl::Expr::int(value)
316 }
317
318 /// The number, with no suffix: `2u` is not a Julia token.
319 fn unsigned_int(value: i128) -> Self::Expr {
320 jl::Expr::int(value)
321 }
322
323 /// A literal, with the two raw tokens the walk escapes through
324 /// [`Literal::Int`] rewritten.
325 ///
326 /// `emit/decode.rs`'s `literal::<T>("false")` carries a keyword in
327 /// an integer literal because that variant is the tree's raw-token
328 /// escape. Julia spells the booleans the same way C does, so the
329 /// rewrite only moves them into [`Literal::Bool`], where the
330 /// renderer owns the spelling.
331 fn literal(value: Literal) -> Self::Expr {
332 let Literal::Int(text) = &value else {
333 return jl::Expr::Literal(value);
334 };
335 match text.as_str() {
336 "true" => jl::Expr::Literal(Literal::Bool(true)),
337 "false" => jl::Expr::Literal(Literal::Bool(false)),
338 _ => jl::Expr::Literal(value),
339 }
340 }
341
342 /// A call to a module-level function.
343 ///
344 /// `strcmp` is the one name the walk hands here that is not a
345 /// function this column generates, and it is C's. `Base.cmp` on two
346 /// `String`s compares byte-wise — UTF-8, which C11 §7.24.4's
347 /// `unsigned char` order preserves — and answers the same sign.
348 fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
349 if callee == "strcmp" {
350 return jl::Expr::free_call("Base.cmp", args);
351 }
352 jl::Expr::free_call(callee, args)
353 }
354
355 /// `write_u32(w, x)` — the runtime is a set of free functions and
356 /// the receiver is the first argument, which is Julia's own calling
357 /// convention.
358 fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
359 // The walk's bare `read_pid()` names the operation that opens
360 // no value block, because the column it was written for opens
361 // one itself. This walk has no such pair — it reads a header
362 // and later calls `finish_pid_read` — so the block has to be
363 // opened by the read, which is what `read_pid_as` does.
364 // §7.4.1.2 makes the difference a wire question: a member's
365 // internal alignment counts from the first octet after its
366 // header.
367 if method == "read_pid" && args.is_empty() {
368 return Self::runtime(
369 receiver,
370 "read_pid_as",
371 vec![Self::constant(Constant::ParameterMember)],
372 );
373 }
374 let mut all = vec![jl::Expr::name(receiver)];
375 all.extend(args);
376 jl::Expr::free_call(method, all)
377 }
378
379 /// A free function of the runtime — `no_dheader`, `utf8_octets`,
380 /// `pid_is_user_member`, the `bit_pattern_*` family — which is
381 /// exactly what `runtime/julia` exports them as.
382 fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
383 jl::Expr::free_call(method, args)
384 }
385
386 /// `runtime/julia` carries the three HDDS axes:
387 /// `begin_packed_element` / `finish_packed_element` on the writer
388 /// and `reader_begin_packed_element` / `reader_finish_packed_element`
389 /// on the reader, the DHEADER pair for a sequence element's own
390 /// length, and `write_wstring_utf8_octets` /
391 /// `write_wstring_utf8_octets_bounded` / `read_wstring_utf8_octets`.
392 fn carries_hdds_axes() -> bool {
393 true
394 }
395
396 fn member(base: &str, field: &str) -> Self::Expr {
397 jl::Expr::field(jl::Expr::name(base), field)
398 }
399
400 fn whole(base: &str) -> Self::Expr {
401 jl::Expr::name(base)
402 }
403
404 fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
405 jl::Expr::field(value, field)
406 }
407
408 /// `value[(at) + 1]`: the walk counts from zero and Julia from one,
409 /// and the conversion lives at the subscript so the counter itself
410 /// keeps the walk's meaning everywhere else it is read.
411 fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
412 jl::Expr::index(
413 value,
414 jl::Expr::Binary {
415 op: "+",
416 lhs: Box::new(jl::Expr::Paren(Box::new(at))),
417 rhs: Box::new(jl::Expr::int(1)),
418 },
419 )
420 }
421
422 /// `Base.length(v)`, or the bound where the materialiser fixed one.
423 ///
424 /// Qualified, because a generated file binds its declarations at
425 /// the scope this codec runs in and an IDL type named `length`
426 /// would take the bare name over.
427 fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
428 match fixed {
429 Some(bound) => jl::Expr::int(i128::from(bound)),
430 None => jl::Expr::free_call("Base.length", vec![jl::Expr::name(base)]),
431 }
432 }
433
434 fn elements(base: &str) -> Self::Expr {
435 jl::Expr::name(base)
436 }
437
438 /// `v.field` — a union's selected case member. `adapter/julia.rs`
439 /// declares one `Union{T, Nothing}` field per case on the struct.
440 fn case_member(base: &str, field: &str) -> Self::Expr {
441 Self::member(base, field)
442 }
443
444 /// `.key` and `.value` — one spelling for both directions.
445 ///
446 /// The encoder reaches a real `ZdMapEntry_…` field; the decoder
447 /// reaches the scratch's dictionary through the same property
448 /// syntax, which is what `ZdScratch` overloads `getproperty` for.
449 fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
450 jl::Expr::field(entry, if key { "key" } else { "value" })
451 }
452
453 fn absent() -> Self::Expr {
454 jl::Expr::Literal(Literal::Null)
455 }
456
457 /// Nothing: a Julia `String` owns its storage and a read that
458 /// produced one produced it whole.
459 fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
460 None
461 }
462
463 /// The member itself: `Union{T, Nothing}` is the value or
464 /// `nothing`, with no wrapper to unwrap.
465 fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
466 Self::member(base, field)
467 }
468
469 /// `v.field !== nothing`.
470 ///
471 /// `!==` and not `!=`: `!=` calls `==`, which for `missing`-like
472 /// values and overloaded types answers structurally, while identity
473 /// against the one value of `Nothing` is the question being asked.
474 fn present(base: &str, field: &str) -> Self::Expr {
475 jl::Expr::Binary {
476 op: "!==",
477 lhs: Box::new(Self::member(base, field)),
478 rhs: Box::new(Self::absent()),
479 }
480 }
481
482 /// Nothing on the way in, `v.field = nothing` on the way out:
483 /// writing the member is what makes it present.
484 fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
485 if present {
486 return Vec::new();
487 }
488 vec![jl::Statement::Assign {
489 place: Self::member(base, field),
490 value: Self::absent(),
491 }]
492 }
493
494 /// `v.field = nothing` for every case: `adapter/julia.rs` declares
495 /// one `Union{T, Nothing}` field per case, and only the selected one
496 /// holds a value. The same statement as an absent
497 /// `@optional` member, [`Self::set_present`] with `false`.
498 fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
499 fields
500 .iter()
501 .flat_map(|field| Self::set_present(base, field, false))
502 .collect()
503 }
504
505 /// The member, with nothing to do first: the scratch's slot exists
506 /// the moment it is named.
507 fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
508 (Vec::new(), Self::member(base, field))
509 }
510
511 /// The operators, unchanged: Julia spells `&&`, `||`, `==`, `!=`,
512 /// the shifts and the bitwise pair exactly as the walk writes them.
513 /// `=` is left as it is and never reaches a rendered expression:
514 /// [`Self::perform`] is the only thing that receives one and turns
515 /// it into a statement.
516 fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
517 jl::Expr::Binary {
518 op,
519 lhs: Box::new(lhs),
520 rhs: Box::new(rhs),
521 }
522 }
523
524 fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
525 jl::Expr::Unary {
526 op,
527 value: Box::new(value),
528 }
529 }
530
531 fn paren(value: Self::Expr) -> Self::Expr {
532 jl::Expr::Paren(Box::new(value))
533 }
534
535 /// A conversion, written as the constructor call Julia spells one
536 /// with — and three special cases that are not conversions.
537 ///
538 /// - `signed char` is the walk's C spelling of a map key order and
539 /// becomes the two's-complement arithmetic (`signed_char`);
540 /// - `Int64` goes through the runtime's `zd_i64`, because
541 /// `Int64(x::UInt64)` throws above `typemax(Int64)` where the
542 /// walk means the bit pattern — the union overlay reads an
543 /// eight-octet unsigned arm through exactly this seam;
544 /// - a `Bool` target with an integer literal is the label rewrite
545 /// at a boolean discriminator: `true`/`false`, not `Bool(1)`.
546 fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
547 if type_ == jl::Type::named(SIGNED_CHAR) {
548 return signed_char(value);
549 }
550 if type_ == jl::Type::Primitive(jl::Primitive::Int64) {
551 return jl::Expr::free_call("zd_i64", vec![value]);
552 }
553 if type_ == jl::Type::Primitive(jl::Primitive::Bool) {
554 if let jl::Expr::Literal(Literal::Int(text)) = &value {
555 return jl::Expr::Literal(Literal::Bool(text != "0"));
556 }
557 }
558 match &type_ {
559 jl::Type::Primitive(
560 jl::Primitive::String | jl::Primitive::Nothing | jl::Primitive::Any,
561 ) => value,
562 jl::Type::Primitive(_) | jl::Type::Named(_) => jl::Expr::convert(type_, value),
563 // No conversion has a collection or a union target in this
564 // walk; the value stands as it is rather than inventing a
565 // constructor call nothing defines.
566 _ => value,
567 }
568 }
569
570 /// The value, unchanged: Julia passes every argument by reference
571 /// to the same object and takes no address.
572 fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
573 value
574 }
575
576 // -- statements ----------------------------------------------------
577
578 /// `local name::T = value`.
579 ///
580 /// Annotated because the annotation is load-bearing twice over: it
581 /// says the width the value keeps — `local zd_n::UInt32` — and it
582 /// converts a later re-assignment, which is what lets `zd_wire =
583 /// <literal>` land in the `Int64` the holder declared.
584 fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
585 jl::Statement::Local {
586 name: Ident::new(name),
587 type_: Some(type_),
588 value,
589 }
590 }
591
592 /// An expression evaluated for its effect — or, where the walk
593 /// built an assignment, the assignment.
594 ///
595 /// The shared walk's `assign` helper builds `T::binary("=", …)` and
596 /// hands it here, and this is the only place one can arrive. Julia
597 /// would accept `a = b` as an expression, and it is still lifted
598 /// into a statement: an `=` buried inside a larger expression is an
599 /// assignment nobody reads.
600 fn perform(value: Self::Expr) -> Self::Stmt {
601 if let jl::Expr::Binary { op: "=", lhs, rhs } = value {
602 return jl::Statement::Assign {
603 place: *lhs,
604 value: *rhs,
605 };
606 }
607 jl::Statement::Expr(value)
608 }
609
610 /// `return zd_freeze(zd_rt, v)`.
611 ///
612 /// Every `T::give_back` the decode walk builds is `give_back(None)`
613 /// — a `void` decoder in the walk's home column. This column's
614 /// decoders answer with the immutable sample, so every return goes
615 /// through the freeze; `zd_rt` is the local
616 /// `Julia::reading_body` bound to the sample's type, because this
617 /// method has no type parameter of its own.
618 fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
619 jl::Statement::Return(Some(value.unwrap_or_else(|| {
620 jl::Expr::free_call(
621 "zd_freeze",
622 vec![jl::Expr::name("zd_rt"), jl::Expr::name("v")],
623 )
624 })))
625 }
626
627 fn when(
628 condition: Self::Expr,
629 then: Self::Block,
630 otherwise: Option<Self::Block>,
631 ) -> Self::Stmt {
632 jl::Statement::If {
633 condition,
634 then: Self::suite(then),
635 // An `else` the walk built empty is dropped rather than
636 // kept: `else` above `end` says nothing and reads as though
637 // a branch had been forgotten.
638 otherwise: otherwise.filter(|body| !body.is_empty()),
639 }
640 }
641
642 /// `for counter in 0:(Int64(bound) - 1)`.
643 ///
644 /// Through `Int64` first, unconditionally: with an unsigned bound
645 /// of zero, `0:(n - 1)` wraps to `0:0xffffffff` and the loop runs
646 /// four billion times over a stream that said "none".
647 fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
648 jl::Statement::For {
649 counter: Ident::new(counter),
650 iter: jl::Expr::range(
651 jl::Expr::int(0),
652 jl::Expr::Paren(Box::new(jl::Expr::Binary {
653 op: "-",
654 lhs: Box::new(jl::Expr::free_call("Int64", vec![bound])),
655 rhs: Box::new(jl::Expr::int(1)),
656 })),
657 ),
658 body: Self::suite(body),
659 }
660 }
661
662 fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
663 jl::Statement::While {
664 condition,
665 body: Self::suite(body),
666 }
667 }
668
669 fn leave() -> Self::Stmt {
670 jl::Statement::Break
671 }
672
673 fn note(doc: Doc) -> Self::Stmt {
674 jl::Statement::Comment(doc)
675 }
676
677 /// The call, and nothing around it: Julia has no discarded-result
678 /// diagnostic and `(Nothing) expr` is not a statement.
679 fn drop_value(value: Self::Expr) -> Self::Stmt {
680 jl::Statement::Expr(value)
681 }
682
683 /// Nothing: Julia does not warn on an unread parameter, and a bare
684 /// `zd_tc` on a line of its own would be a statement that does
685 /// nothing.
686 fn mark_used(_name: &str) -> Vec<Self::Stmt> {
687 Vec::new()
688 }
689
690 /// `place = zd_dec_T(r, zd_tc)`.
691 ///
692 /// A Julia function has no out-parameter — rebinding a parameter
693 /// rebinds the local — so this column's decoders answer with the
694 /// value and the destination is assigned at the call site.
695 fn read_call(
696 callee: &str,
697 place: Self::Expr,
698 _form: Handoff,
699 behaviour: Self::Expr,
700 ) -> Self::Stmt {
701 jl::Statement::Assign {
702 place,
703 value: jl::Expr::free_call(callee, vec![jl::Expr::name("r"), behaviour]),
704 }
705 }
706
707 /// `v = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on. The
708 /// nested decoder answers a frozen value and the trailing freeze is
709 /// then the identity.
710 fn read_forward(callee: &str) -> Self::Stmt {
711 jl::Statement::Assign {
712 place: jl::Expr::name("v"),
713 value: jl::Expr::free_call(callee, vec![jl::Expr::name("r"), jl::Expr::name("zd_tc")]),
714 }
715 }
716
717 fn block(statements: Vec<Self::Stmt>) -> Self::Block {
718 statements
719 }
720
721 fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
722 block
723 }
724
725 /// Room for `count` values, and the fill inside it.
726 ///
727 /// **A sequence** (`assigned_whole`): `v = zd_nothing_vector(n)`,
728 /// a `Vector{Any}` of `nothing`, because the element decoder
729 /// assigns each slot outright and `zd_freeze` converts the run to
730 /// the declared `Vector{T}` or `NTuple` at the return.
731 ///
732 /// **A `map`** (not `assigned_whole`): `v = zd_entry_vector(n)`,
733 /// one *distinct* `ZdScratch` per entry, because each entry is
734 /// written half at a time — `v[i].key = …` — and a repeated
735 /// reference would make every entry the last one read.
736 fn make_room(
737 base: &str,
738 _element: Self::Type,
739 count: Self::Expr,
740 assigned_whole: bool,
741 fill: Vec<Self::Stmt>,
742 ) -> Vec<Self::Stmt> {
743 let maker = if assigned_whole {
744 "zd_nothing_vector"
745 } else {
746 "zd_entry_vector"
747 };
748 let mut out = vec![jl::Statement::Assign {
749 place: jl::Expr::name(base),
750 value: jl::Expr::free_call(maker, vec![count]),
751 }];
752 out.extend(fill);
753 out
754 }
755
756 /// `v = v[1:Int64(count)]`.
757 ///
758 /// §7.3.1.2.1.12.3's TRIM for a bounded collection, applied after
759 /// every element has been read. One-based and inclusive, which is
760 /// what "the first `count` elements" is in Julia's own counting.
761 fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
762 vec![jl::Statement::Assign {
763 place: jl::Expr::name(base),
764 value: jl::Expr::index(
765 jl::Expr::name(base),
766 jl::Expr::range(jl::Expr::int(1), jl::Expr::free_call("Int64", vec![count])),
767 ),
768 }]
769 }
770
771 /// The subscript, with nothing before or after it: a `Vector{Any}`
772 /// slot is an ordinary place at every element type.
773 fn sequence_element(
774 base: &str,
775 index: Self::Expr,
776 _element: Self::Type,
777 ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
778 (
779 Vec::new(),
780 Self::index(Self::elements(base), index),
781 Vec::new(),
782 )
783 }
784
785 /// A local the body writes *into*.
786 ///
787 /// One caller: `zd_scratch`, the entry of an uncounted `map`, which
788 /// the reader fills a half at a time — a runtime `ZdScratch`, for
789 /// the reason `MAP_ENTRY` gives.
790 fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
791 if type_ == jl::Type::named(MAP_ENTRY) {
792 return jl::Statement::Assign {
793 place: jl::Expr::name(name),
794 value: jl::Expr::free_call("zd_scratch_entry", Vec::new()),
795 };
796 }
797 Self::declare(type_, name, None)
798 }
799
800 /// One field of a header the runtime handed back.
801 ///
802 /// `end` is a Julia keyword and cannot name a struct field, so the
803 /// runtime's `ZdPidRead` spells it `end_` and this seam is where
804 /// the walk's name meets the runtime's.
805 fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
806 let spelled = if field == "end" { "end_" } else { field };
807 jl::Expr::field(value, spelled)
808 }
809
810 // -- assembly ------------------------------------------------------
811
812 /// `function zd_enc_T(w::ZdWriter, v::T) … end` and
813 /// `function zd_dec_T(r::ZdReader, zd_tc::ZdTryConstruct) … end`.
814 ///
815 /// A forward declaration is answered with nothing: Julia resolves a
816 /// global at the call and not at the definition, so a recursive or
817 /// mutually recursive type needs no declaration ahead of its body.
818 fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
819 let body = spec.body?;
820 let mut params = vec![jl::Param {
821 name: Ident::new(if spec.writing { "w" } else { "r" }),
822 type_: Some(jl::Type::named(if spec.writing {
823 "ZdWriter"
824 } else {
825 "ZdReader"
826 })),
827 }];
828 if spec.writing {
829 params.push(jl::Param {
830 name: Ident::new("v"),
831 // Unannotated on purpose. The declared sample type
832 // would be right for every call the walk builds, and
833 // one caller sits outside the walk: the write harness
834 // hands the sample it froze, and an annotation here
835 // would re-convert a value that is already exact.
836 type_: None,
837 });
838 }
839 params.extend(spec.extra.into_iter().map(|(type_, name)| jl::Param {
840 name: Ident::new(&name),
841 type_: Some(type_),
842 }));
843 let body = if spec.writing {
844 Self::suite(body)
845 } else {
846 Self::reading_body(&spec.name, spec.sample, body)
847 };
848 Some(jl::Decl::FunctionDef(jl::FunctionDef {
849 doc: spec.doc,
850 name: Ident::new(&spec.name),
851 params,
852 // No return annotation: a reader's returns already go
853 // through `zd_freeze`, and an annotation would ask Julia to
854 // convert a value that is already the declared type.
855 returns: None,
856 body,
857 }))
858 }
859
860 /// A call to a generated defaulter and not a literal in place;
861 /// [`crate::default_julia`] carries the whole table.
862 ///
863 /// # Errors
864 /// Never; the signature matches the emitters around it.
865 fn default_into(
866 ast: &Ast,
867 id: Id<TypeNode>,
868 place: Self::Expr,
869 ) -> Result<Self::Stmt, EncodeError> {
870 Ok(crate::default_julia::default_into(ast, id, place))
871 }
872
873 /// # Errors
874 /// As [`crate::default_julia::member_default`]: a `@default` this
875 /// column cannot spell as a Julia expression is refused rather than
876 /// silently replaced by Table 9's zero.
877 fn member_default(
878 ast: &Ast,
879 node: &MemberNode,
880 type_: Id<TypeNode>,
881 place: Self::Expr,
882 ) -> Result<Vec<Self::Stmt>, EncodeError> {
883 crate::default_julia::member_default(ast, node, type_, place)
884 }
885
886 /// `a_b_RED` — the flattened constant `adapter/julia.rs` binds an
887 /// enumerator to at module scope.
888 ///
889 /// Through `escape_name` and not [`Self::escape`]: an enumerator is
890 /// a module-scope binding and shadows what a field cannot, so the
891 /// two escapes are different sets and the materialiser used the
892 /// wider one.
893 fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
894 jl::Expr::name(&format!(
895 "{}{}",
896 julia_module_prefix(ast, id),
897 escape_name(enumerator)
898 ))
899 }
900}
901
902/// A code unit read as C's signed `char`.
903///
904/// `((Int64(k) + 128) % 256) - 128`: the two's-complement reading of one
905/// octet, for `MapKeyOrdering::SignedInteger` over an IDL `char` key.
906/// The corpus was recorded on a host where C's `char` is signed, so
907/// `U+00FF` sorts below `U+0000`, and a column comparing unsigned code
908/// units would order the entries differently.
909///
910/// Arithmetic and not a helper in `runtime/julia`, for the reason the
911/// Python column writes down: how wide and how signed C spells a
912/// character is a language-mapping question, and a runtime whose subject
913/// is §7.4 is not where a second column should inherit this column's
914/// host from.
915fn signed_char(value: jl::Expr) -> jl::Expr {
916 let wide = jl::Expr::free_call("Int64", vec![value]);
917 let wrapped = jl::Expr::Binary {
918 op: "%",
919 lhs: Box::new(jl::Expr::Paren(Box::new(jl::Expr::Binary {
920 op: "+",
921 lhs: Box::new(wide),
922 rhs: Box::new(jl::Expr::int(128)),
923 }))),
924 rhs: Box::new(jl::Expr::int(256)),
925 };
926 jl::Expr::Binary {
927 op: "-",
928 lhs: Box::new(jl::Expr::Paren(Box::new(wrapped))),
929 rhs: Box::new(jl::Expr::int(128)),
930 }
931}
932
933/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
934/// that keeps two of them apart.
935///
936/// The tag names the routine a hoisted collection's codec is emitted
937/// under, and one body is emitted per name. With the bound left out,
938/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
939/// was declared first decides: measured on
940/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
941/// generated Julia held no `zd_over` check at all, so `a` accepted a
942/// stream of any length; with the members the other way round both got
943/// the bounded body, so `b` refused — and truncated — a length the IDL
944/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
945/// beside `wstring` collapse the same way, onto the trimming call.
946/// Either way the reader answered with a value the IDL does not
947/// describe and said nothing, which is why the bound is part of the
948/// name. The same suffix the Zig column carries.
949///
950/// An `array` needs nothing here: its `dims` are already in the tag,
951/// under the `Array` arm below.
952fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
953 match bound.expr() {
954 None => String::new(),
955 // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
956 // arm answers for an array of four `long`, and the two are
957 // different wire forms.
958 Some(expr) => match ast.eval_integral(expr) {
959 Ok(limit) => format!("_b{limit}"),
960 // As the array's `_x`: a bound that does not evaluate is
961 // still not the unbounded form, and the materialiser
962 // reports the expression that would not evaluate.
963 Err(_) => "_bx".to_string(),
964 },
965 }
966}
967
968/// The name component a codec function takes for a type.
969///
970/// The IDL kind and never the Julia spelling: `octet` and `char` are one
971/// `UInt8` here and one octet apart on no wire, but `double` and every
972/// integer width map one to one — the tag keeps them apart the same way
973/// the Python column's does. An alias is followed through, so a typedef
974/// and its target share one codec and the walk's first-wins
975/// deduplication keeps the target's body.
976///
977/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
978fn codec_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
979 if depth > MAX_TYPE_DEPTH {
980 return "deep".to_string();
981 }
982 let Ok(node) = ast.type_(id) else {
983 return "unknown".to_string();
984 };
985 match &node.kind {
986 TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
987 TypeKind::String {
988 width: CharWidth::Char,
989 bound,
990 } => format!("string{}", bound_tag(ast, bound)),
991 TypeKind::String {
992 width: CharWidth::WChar,
993 bound,
994 } => format!("wstring{}", bound_tag(ast, bound)),
995 TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
996 TypeKind::Sequence { element, bound } => match element.resolved() {
997 Some(element) => format!(
998 "seq{}_{}",
999 bound_tag(ast, bound),
1000 codec_tag(ast, element, depth + 1)
1001 ),
1002 None => format!("seq_unknown{}", bound_tag(ast, bound)),
1003 },
1004 TypeKind::Array { element, dims } => {
1005 let mut name = match element.resolved() {
1006 Some(element) => format!("arr_{}", codec_tag(ast, element, depth + 1)),
1007 None => "arr_unknown".to_string(),
1008 };
1009 for dim in dims {
1010 match ast.eval_integral(*dim) {
1011 Ok(length) => name.push_str(&format!("_{length}")),
1012 // No number rather than a zero: the name has to
1013 // differ from a `[0]` array's.
1014 Err(_) => name.push_str("_x"),
1015 }
1016 }
1017 name
1018 }
1019 // The entry struct's own name carries no bound — two maps that
1020 // differ only in bound hold the same pairs — but the tag does,
1021 // for the routine-name reason in [`bound_tag`].
1022 TypeKind::Map { bound, .. } => {
1023 {
1024 // The collection's bound goes in front of the entry name:
1025 // after it, a bound on the map and one on its value would
1026 // end in the same suffix.
1027 let entry = julia_map_entry_name(ast, id);
1028 match bound_tag(ast, bound) {
1029 bound if bound.is_empty() => entry,
1030 bound => format!("map{bound}_{entry}"),
1031 }
1032 }
1033 }
1034 TypeKind::Alias { aliased } => match aliased.resolved() {
1035 Some(target) => codec_tag(ast, target, depth + 1),
1036 None => julia_type_name(ast, id),
1037 },
1038 _ => julia_type_name(ast, id),
1039 }
1040}