Skip to main content

zerodds_lang_rules/emit/
lua.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Lua 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` and the rest do, and none of them calls another.
7//! Where two columns disagree on an octet, one has misread the
8//! specification — which is the only reason to have another.
9//!
10//! # The shapes this column holds a value in
11//!
12//! `adapter/lua.rs` is the authority and this file spells nothing it
13//! does not declare:
14//!
15//! - a struct, a union, a bitset, a sequence, an array and a map are
16//!   all **tables** — a sequence 1-based, a map an array of
17//!   `{ key = …, value = … }` pairs;
18//! - a `char` and a `wchar` are **integers** holding the code point,
19//!   which is why [`Lua::code_point`] is the identity and a character
20//!   case label stays the number it arrived as;
21//! - an `unsigned long long` is the 64-bit two's-complement **bit
22//!   pattern** in a Lua integer, which is why [`Lua::unsigned_less`]
23//!   answers `math.ult` at eight octets — Lua's `<` on two patterns is
24//!   signed and sorted `16045690984833335998` after `7`;
25//! - a `long double` and a `fixed` are decimal **strings**, converted
26//!   at the octet by `runtime/lua`;
27//! - an absent `@optional` member is an absent **key**: `v.m ~= nil`
28//!   is the presence test and `v.m = nil` the removal, because in Lua
29//!   the absence of a value is the absence of the key.
30//!
31//! # The four decisions this column turns on
32//!
33//! **Codecs live in one table.** Every generated routine is
34//! `function ZD.zd_enc_T(w, v) … end` and every call is `ZD.zd_enc_T`,
35//! because a chunk holds at most 200 locals and a corpus case's codecs,
36//! defaulters and dumpers together would cross that as `local function`s.
37//! A table lookup also resolves at the call and not at the definition,
38//! so a recursive or mutually recursive type needs no forward
39//! declaration — [`Lua::routine`] answers `None` for one, exactly as
40//! the Python backend does. The caller binds `local ZD = {}` before the
41//! declarations, the same contract `adapter::lua::lua_items` has for `M`.
42//!
43//! **An assignment is a statement.** `a = b` is not a Lua expression.
44//! The shared walk builds one through `T::perform(T::binary("=", …))`
45//! at every assignment site, so [`Lua::perform`] recognises that shape
46//! and answers [`lua::Stat::Assign`]; everything else it receives is a
47//! call, which is the one expression Lua's grammar takes as a statement.
48//!
49//! **A counted loop stays zero-based and the subscript adds one.** The
50//! walk counts `0 .. bound` exclusive and compares counters against
51//! ranks; a backend that moved the loop to `1 .. bound` would have to
52//! move the rank arithmetic with it. So [`Lua::count_up`] writes
53//! `for i = 0, bound - 1` and [`Lua::index`] writes `t[(i) + 1]`, and
54//! the 1-based convention is decided in exactly one method.
55//!
56//! **The boolean spellings are Lua's.** `&&`, `||`, `!` and `!=` become
57//! `and`, `or`, `not` and `~=`; `false` before true — the raw token the
58//! walk escapes through `Literal::Int` — is rewritten to the literal,
59//! as the Python backend does for `False`.
60//!
61//! # What Lua cannot check, said plainly
62//!
63//! Nothing in a generated chunk declares a type, so a wrong shape here
64//! is not a compile error anywhere — it is a wrong octet in the
65//! measurement or an error at run time. That is why this backend leans
66//! on the two loud failure modes Lua does have: a call through a name
67//! `ZD` does not hold reports the name at the line, and `runtime/lua`
68//! range-checks every value at the octet.
69
70use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
71use crate::adapter::lua::declared_place;
72use crate::encode::EncodeError;
73use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
74use zerodds_ast_core::Id;
75use zerodds_lang_cst::common::{Doc, Ident, Literal};
76use zerodds_lang_cst::lua::{self, Expr, Place, Stat};
77
78/// The Lua backend.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct Lua;
81
82/// The table the generated codecs, defaulters and dumpers live in.
83///
84/// The caller binds `local ZD = {}` ahead of the declarations — the
85/// same contract `adapter::lua::lua_items` states for `M`.
86pub const CODECS: &str = "ZD";
87
88/// The table the runtime is required into: `local zdw =
89/// require("zerodds_wire")`. Prefixed like every other generated name,
90/// so no IDL identifier the corpus declares reaches it.
91pub const RUNTIME: &str = "zdw";
92
93/// How deep a chain of typedefs and collections is followed.
94const MAX_TYPE_DEPTH: usize = 64;
95
96/// The type name [`CodecTarget::named`] is handed for C's signed
97/// `char`. `emit/encode.rs`'s `map_key_less` writes it where a `map`'s
98/// key is a `char` under `MapKeyOrdering::SignedInteger`; this column
99/// holds the code point as an integer, so the reading becomes the
100/// arithmetic `((cp + 128) % 256) - 128` — Lua's `%` is a floored
101/// modulo, so the wrap is exact for the whole octet.
102const SIGNED_CHAR: &str = "signed char";
103
104/// What [`CodecTarget::map_entry_name`] answers with: not a type this
105/// column declares but a marker [`Lua::storage`] matches on, because a
106/// map entry is a plain `{ key = …, value = … }` table.
107const MAP_ENTRY: &str = "_zd_map_entry";
108
109/// The nominal type a local is annotated with — which in Lua is
110/// nothing at all, so the type is a tag this backend keeps for its own
111/// two decisions: whether a cast target is `boolean` (a union label
112/// becomes `true`/`false`), and whether a storage request is the map
113/// entry marker. Everything else is carried through untouched.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct LuaType(pub String);
116
117impl Lua {
118    /// A possibly dotted name as a place.
119    fn place_from_name(text: &str) -> Place {
120        let mut segments = text.split('.');
121        let mut place = Place::name(segments.next().unwrap_or(text));
122        for segment in segments {
123            place = place.field(segment);
124        }
125        place
126    }
127
128    /// An expression read back as the place it names.
129    ///
130    /// The walk only ever indexes and dereferences values it built as
131    /// names, subscripts and fields — all places — so the fallback arm
132    /// is the grammar's own escape: a parenthesised expression is a
133    /// legal base for a field or an index (Lua 5.4 reference §3.4).
134    fn place_of(value: Expr) -> Place {
135        match value {
136            Expr::Place(place) => place,
137            Expr::Paren(inner) => match *inner {
138                Expr::Place(place) => place,
139                other => Place::Paren(Box::new(other)),
140            },
141            other => Place::Paren(Box::new(other)),
142        }
143    }
144
145    /// A call on the runtime table: `zdw.<method>(args…)`.
146    fn runtime_call(method: &str, args: Vec<Expr>) -> Expr {
147        Expr::Call {
148            func: Box::new(Place::name(RUNTIME).field(method)),
149            args,
150        }
151    }
152
153    /// A call on the codec table: `ZD.<callee>(args…)`.
154    fn codec_call(callee: &str, args: Vec<Expr>) -> Expr {
155        Expr::Call {
156            func: Box::new(Place::name(CODECS).field(callee)),
157            args,
158        }
159    }
160
161    /// A string literal.
162    fn text(value: &str) -> Expr {
163        Expr::string(value)
164    }
165
166    /// A reader's body, with the sample it answers with around it:
167    /// `local v = ZD.zd_def_T()` … `return v`. Table 9's value through
168    /// the generated defaulter, so every position the stream does not
169    /// carry is already what the clause says before an octet is read;
170    /// the defaulter and the decoder are keyed the same way and differ
171    /// in three letters.
172    fn reading_body(name: &str, body: Vec<Stat>) -> Vec<Stat> {
173        let defaulter = crate::default_lua::defaulter_from_decoder(name);
174        let mut statements = vec![Stat::Local {
175            doc: Doc::none(),
176            name: Ident::new("v"),
177            value: Some(Self::codec_call(&defaulter, Vec::new())),
178        }];
179        statements.extend(body);
180        statements.push(Stat::Return(Some(Expr::place(Place::name("v")))));
181        statements
182    }
183}
184
185impl CodecTarget for Lua {
186    type Expr = Expr;
187    type Stmt = Stat;
188    type Block = Vec<Stat>;
189    type Type = LuaType;
190    type Decl = Stat;
191
192    // -- types --------------------------------------------------------
193
194    /// The tag and nothing else: Lua declares no types. `boolean` is
195    /// the one tag [`Self::cast`] acts on.
196    fn scalar(kind: Scalar) -> Self::Type {
197        LuaType(
198            match kind {
199                Scalar::Bool => "boolean",
200                // A char member holds the code point as an integer —
201                // `adapter/lua.rs`'s mapping.
202                Scalar::Char => "integer",
203                Scalar::Int8
204                | Scalar::UInt8
205                | Scalar::Int16
206                | Scalar::UInt16
207                | Scalar::Int32
208                | Scalar::UInt32
209                | Scalar::Int64
210                | Scalar::UInt64
211                | Scalar::Index
212                | Scalar::Size => "integer",
213                Scalar::Float | Scalar::Double => "number",
214                // The decimal text; `runtime/lua` converts at the octet.
215                Scalar::LongDouble => "string",
216            }
217            .to_string(),
218        )
219    }
220
221    fn named(name: &str) -> Self::Type {
222        LuaType(name.to_string())
223    }
224
225    fn nothing() -> Self::Type {
226        LuaType("nil".to_string())
227    }
228
229    /// Every runtime handle is a plain table.
230    fn handle(_kind: Handle) -> Self::Type {
231        LuaType("table".to_string())
232    }
233
234    /// The member holds the code point already, so there is nothing to
235    /// convert in either direction.
236    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
237        value
238    }
239
240    /// The runtime's policy values, in the spelling `runtime/lua`
241    /// matches — the same strings `runtime/python` takes, because both
242    /// spell `WstringLengthPolicy::as_str` and its siblings. One arm
243    /// per constant and no catch-all, so a policy this column has not
244    /// implemented is a compile error here rather than a plausible
245    /// value.
246    fn constant(which: Constant) -> Self::Expr {
247        match which {
248            // The octet count itself: `write_wchar` takes the number.
249            Constant::WcharWidth2 => Expr::int(2),
250            Constant::WcharWidth4 => Expr::int(4),
251            Constant::WcharFixed => Self::text("fixed"),
252            Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
253            Constant::WstringUnitsStream => Self::text("stream"),
254            Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
255            Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
256            Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
257            Constant::WstringSpecification => Self::text("specification"),
258            Constant::WstringCharacterCount => Self::text("character-count"),
259            Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
260            Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
261            Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
262            Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
263            Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
264            Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
265            Constant::FixedDeclared => Self::text("declared"),
266            Constant::FixedValuePrecision => Self::text("value-precision"),
267            Constant::FixedValueStringDigits => Self::text("value-string-digits"),
268            Constant::ParameterMember => Self::text("member"),
269            Constant::ParameterOptional => Self::text("optional"),
270            Constant::TryConstructDiscard => Self::text("discard"),
271            Constant::TryConstructTrim => Self::text("trim"),
272            Constant::TryConstructUseDefault => Self::text("use-default"),
273            Constant::StatusOk => Self::text("ok"),
274            Constant::StatusDiscarded => Self::text("discarded"),
275            Constant::StatusImpossibleLength => Self::text("impossible-length"),
276            Constant::StatusOutOfRange => Self::text("out-of-range"),
277            Constant::StatusTruncated => Self::text("truncated"),
278        }
279    }
280
281    /// A Lua integer, at every bit bound: 64 bits hold every
282    /// `@bit_bound` a set can declare.
283    fn unsigned_holder(_bits: u16) -> Self::Type {
284        LuaType("integer".to_string())
285    }
286
287    /// Whether `left` is less than `right` as unsigned integers.
288    ///
289    /// At eight octets the held value is the two's-complement bit
290    /// pattern and Lua's `<` is signed, so the answer is `math.ult` —
291    /// Lua's own unsigned comparison. Narrower widths hold the value
292    /// itself, non-negative, and the default `<` is right.
293    fn unsigned_less(octets: u8, left: Self::Expr, right: Self::Expr) -> Self::Expr {
294        if octets < 8 {
295            return Self::binary("<", left, right);
296        }
297        Expr::Call {
298            func: Box::new(Place::name("math").field("ult")),
299            args: vec![left, right],
300        }
301    }
302
303    /// `false` before `true`. Lua's `<` refuses two booleans outright —
304    /// "attempt to compare two boolean values" — so the order is
305    /// spelled as `not (left) and (right)`, which is false-before-true
306    /// exactly.
307    fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
308        Expr::Binary {
309            op: "and",
310            lhs: Box::new(Expr::Unary {
311                op: "not",
312                value: Box::new(Expr::Paren(Box::new(left))),
313            }),
314            rhs: Box::new(Expr::Paren(Box::new(right))),
315        }
316    }
317
318    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
319        value_type_of(ast, id, 0)
320    }
321
322    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
323        type_tag(ast, id, 0)
324    }
325
326    /// A map entry is a plain `{ key = …, value = … }` table; the name
327    /// is a marker [`Self::storage`] matches on and nothing declares.
328    fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
329        MAP_ENTRY.to_string()
330    }
331
332    /// Nothing: the declared paths this backend writes go through
333    /// [`Self::enumerator_constant`] and the defaulters, both rooted at
334    /// `M`, and a prefix would be a second answer to the same question.
335    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
336        String::new()
337    }
338
339    /// The identifier, unchanged. Lua renames nothing: a keyword member
340    /// keeps its name and only the syntax it is reached by changes —
341    /// `t["end"]` instead of `t.end` — which `lua::Place::field` and
342    /// `lua::table_key` decide at the use.
343    fn escape(name: &str) -> String {
344        name.to_string()
345    }
346
347    // -- expressions ---------------------------------------------------
348
349    fn name(text: &str) -> Self::Expr {
350        Expr::place(Self::place_from_name(text))
351    }
352
353    fn int(value: i128) -> Self::Expr {
354        Expr::int(value)
355    }
356
357    /// An integer literal that survives its own width. A Lua decimal
358    /// numeral above 2^63 − 1 denotes a *float* (reference §3.1) and a
359    /// hexadecimal one wraps to the exact bit pattern, so a union label
360    /// like `16045690984833335998` is written `0xDEADBEEFCAFEBABE`-style
361    /// — the same reading the member itself holds. The same care covers
362    /// −2^63 itself: `-9223372036854775808` is unary minus over a
363    /// numeral that is already a *float* by the time the minus applies,
364    /// so the minimum goes out as its own hex pattern too.
365    fn wide_int(value: i128) -> Self::Expr {
366        if value == i128::from(i64::MIN) {
367            return Expr::Literal(Literal::Int("0x8000000000000000".to_string()));
368        }
369        if i64::try_from(value).is_ok() {
370            return Expr::int(value);
371        }
372        if let Ok(unsigned) = u64::try_from(value) {
373            return Expr::Literal(Literal::Int(format!("0x{unsigned:016X}")));
374        }
375        // No Lua integer holds it; the masked pattern is what the
376        // 64-bit member the label is compared against would hold.
377        #[allow(clippy::cast_sign_loss)]
378        let masked = (value as u64).wrapping_add(0);
379        Expr::Literal(Literal::Int(format!("0x{masked:016X}")))
380    }
381
382    /// As [`Self::wide_int`], from the unsigned side.
383    fn wide_unsigned(value: u64) -> Self::Expr {
384        if i64::try_from(value).is_ok() {
385            return Expr::int(i128::from(value));
386        }
387        Expr::Literal(Literal::Int(format!("0x{value:016X}")))
388    }
389
390    fn wide_signed(value: i128) -> Self::Expr {
391        Self::wide_int(value)
392    }
393
394    /// The number, with no suffix: `2u` is not a Lua numeral.
395    fn unsigned_int(value: i128) -> Self::Expr {
396        Expr::int(value)
397    }
398
399    /// A literal, with the two raw tokens the walk escapes through
400    /// [`Literal::Int`] read back as the booleans they are. Lua spells
401    /// them the same way C does, so this is for the tree's sake rather
402    /// than the renderer's: a `Literal::Bool` cannot be mistaken for a
403    /// number by anything that walks the statements later.
404    fn literal(value: Literal) -> Self::Expr {
405        if let Literal::Int(text) = &value {
406            match text.as_str() {
407                "true" => return Expr::bool(true),
408                "false" => return Expr::bool(false),
409                _ => {}
410            }
411        }
412        Expr::Literal(value)
413    }
414
415    /// A call to a generated codec — or, for the one C name the walk
416    /// hands here, to the runtime's byte-wise `strcmp`: Lua's own `<`
417    /// on strings goes through `strcoll` and the process's locale, and
418    /// a map's entry order must not.
419    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
420        if callee == "strcmp" {
421            return Self::runtime_call("strcmp", args);
422        }
423        Self::codec_call(callee, args)
424    }
425
426    /// `zdw.write_u32(w, x)` — the runtime is a table of functions
427    /// taking the writer or reader first, so the receiver becomes the
428    /// first argument.
429    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
430        // `read_pid()` is the walk's name for "read a header in the
431        // MEMBER role and open its value block": §7.4.1.2 counts a
432        // member's internal alignment from the first octet after its
433        // header, and the walk closes the block through
434        // `finish_pid_read`. `read_pid_as` is the runtime entry that
435        // opens it. The same rewrite the Python backend makes, for the
436        // same wire reason.
437        if method == "read_pid" && args.is_empty() {
438            return Self::runtime(
439                receiver,
440                "read_pid_as",
441                vec![Self::constant(Constant::ParameterMember)],
442            );
443        }
444        let mut all = vec![Self::name(receiver)];
445        all.extend(args);
446        Self::runtime_call(method, all)
447    }
448
449    /// A free function of the runtime — the same table, because the
450    /// runtime is one table: `zdw.no_dheader()`,
451    /// `zdw.pid_is_user_member(zd_p)`, `zdw.utf8_octets(v)`.
452    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
453        Self::runtime_call(method, args)
454    }
455
456    /// `runtime/lua` carries the three HDDS axes: `begin_packed_element` /
457    /// `finish_packed_element` on the writer and
458    /// `reader_begin_packed_element` / `reader_finish_packed_element` on the
459    /// reader, the DHEADER pair for a sequence element's own length, and
460    /// `write_wstring_utf8_octets` / `read_wstring_utf8_octets`.
461    fn carries_hdds_axes() -> bool {
462        true
463    }
464
465    /// A field name with dots is a path through the members that hold a base
466    /// (`parent.x`, XTypes 1.3 §7.5.1.4.1.3): one field step per segment.
467    fn member(base: &str, field: &str) -> Self::Expr {
468        let mut place = Place::name(base);
469        for segment in field.split('.') {
470            place = place.field(segment);
471        }
472        Expr::place(place)
473    }
474
475    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
476    fn inherited_path(depth: usize) -> Option<String> {
477        Some("parent.".repeat(depth))
478    }
479
480    fn whole(base: &str) -> Self::Expr {
481        Expr::place(Place::name(base))
482    }
483
484    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
485        Expr::place(Self::place_of(value).field(field))
486    }
487
488    /// `t[(i) + 1]` — the one place the 0-based walk meets Lua's
489    /// 1-based tables. Every counter stays 0-based (see
490    /// [`Self::count_up`]), so every subscript adds one, here and
491    /// nowhere else.
492    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
493        Expr::place(Place::Index {
494            base: Box::new(Self::place_of(value)),
495            key: Box::new(Expr::Binary {
496                op: "+",
497                lhs: Box::new(Expr::Paren(Box::new(at))),
498                rhs: Box::new(Expr::int(1)),
499            }),
500        })
501    }
502
503    /// `#v`, or the bound where the materialiser fixed one. The length
504    /// operator counts the array part's border, which for the
505    /// contiguous tables this column builds is the element count.
506    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
507        match fixed {
508            Some(bound) => Expr::int(i128::from(bound)),
509            None => Expr::Unary {
510                op: "#",
511                value: Box::new(Expr::place(Place::name(base))),
512            },
513        }
514    }
515
516    fn elements(base: &str) -> Self::Expr {
517        Expr::place(Place::name(base))
518    }
519
520    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
521        Expr::Binary {
522            op: match op {
523                "&&" => "and",
524                "||" => "or",
525                "!=" => "~=",
526                other => other,
527            },
528            lhs: Box::new(lhs),
529            rhs: Box::new(rhs),
530        }
531    }
532
533    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
534        Expr::Unary {
535            op: match op {
536                "!" => "not",
537                other => other,
538            },
539            value: Box::new(value),
540        }
541    }
542
543    fn paren(value: Self::Expr) -> Self::Expr {
544        Expr::Paren(Box::new(value))
545    }
546
547    /// The identity on every integer — Lua's integer holds every IDL
548    /// width's pattern and `runtime/lua` range-checks at the octet —
549    /// and a rewrite on two things that are not integers:
550    ///
551    /// - a **boolean** discriminator holds `true`/`false`, and
552    ///   `v._d == 1` is never true against a boolean, so an integer
553    ///   label becomes the literal;
554    /// - **`signed char`**, C's reading of a `map`'s `char` key, which
555    ///   this column computes as `((cp + 128) % 256) - 128` — Lua's `%`
556    ///   is floored, so the two's-complement wrap is exact.
557    ///
558    /// A `char` label needs nothing: the member holds the code point as
559    /// an integer already.
560    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
561        if type_.0 == SIGNED_CHAR {
562            return Expr::Paren(Box::new(Expr::Binary {
563                op: "-",
564                lhs: Box::new(Expr::Paren(Box::new(Expr::Binary {
565                    op: "%",
566                    lhs: Box::new(Expr::Paren(Box::new(Expr::Binary {
567                        op: "+",
568                        lhs: Box::new(value),
569                        rhs: Box::new(Expr::int(128)),
570                    }))),
571                    rhs: Box::new(Expr::int(256)),
572                }))),
573                rhs: Box::new(Expr::int(128)),
574            }));
575        }
576        if type_.0 == "boolean" {
577            if let Expr::Literal(Literal::Int(text)) = &value {
578                if text.chars().all(|c| c.is_ascii_digit() || c == '-') && !text.is_empty() {
579                    return Expr::bool(text != "0");
580                }
581            }
582        }
583        value
584    }
585
586    /// The value, unchanged: Lua passes every table by reference and
587    /// takes no address.
588    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
589        value
590    }
591
592    // -- statements ----------------------------------------------------
593
594    /// `local name = value`, or `local name`. The type is a tag Lua has
595    /// no syntax for and is dropped.
596    fn declare(_type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
597        Stat::Local {
598            doc: Doc::none(),
599            name: Ident::new(name),
600            value,
601        }
602    }
603
604    /// An assignment where the walk built one, the call statement
605    /// everywhere else — the only expression Lua's grammar takes as a
606    /// statement, and the only other thing the walk performs.
607    fn perform(value: Self::Expr) -> Self::Stmt {
608        if let Expr::Binary { op: "=", lhs, rhs } = value {
609            return Stat::Assign {
610                doc: Doc::none(),
611                target: Self::place_of(*lhs),
612                value: *rhs,
613            };
614        }
615        Stat::Perform(value)
616    }
617
618    /// `return v`: this column's decoders answer with the sample —
619    /// there is no out-parameter to fill — so the walk's bare
620    /// `give_back(None)` returns the local the body built.
621    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
622        Stat::Return(Some(value.unwrap_or_else(|| Expr::place(Place::name("v")))))
623    }
624
625    fn when(
626        condition: Self::Expr,
627        then: Self::Block,
628        otherwise: Option<Self::Block>,
629    ) -> Self::Stmt {
630        Stat::If {
631            condition,
632            then,
633            otherwise: otherwise.filter(|body| !body.is_empty()),
634        }
635    }
636
637    /// `for counter = 0, bound - 1 do … end`. The counter stays
638    /// 0-based — [`Self::index`] carries the whole 1-based convention —
639    /// and Lua's inclusive upper bound takes the minus one.
640    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
641        Stat::NumericFor {
642            counter: Ident::new(counter),
643            from: Expr::int(0),
644            to: Expr::Binary {
645                op: "-",
646                lhs: Box::new(bound),
647                rhs: Box::new(Expr::int(1)),
648            },
649            body,
650        }
651    }
652
653    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
654        Stat::While { condition, body }
655    }
656
657    fn leave() -> Self::Stmt {
658        Stat::Break
659    }
660
661    fn note(doc: Doc) -> Self::Stmt {
662        Stat::Comment(doc)
663    }
664
665    /// The call, alone: Lua warns about nothing and `(nil) call` is not
666    /// a statement.
667    fn drop_value(value: Self::Expr) -> Self::Stmt {
668        Stat::Perform(value)
669    }
670
671    /// Nothing: an unread parameter is not a diagnostic Lua has, and a
672    /// bare name is not a statement.
673    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
674        Vec::new()
675    }
676
677    /// `place = ZD.zd_dec_T(r, zd_tc)`: decoders answer with the value
678    /// and the destination is the assignment's left-hand side.
679    fn read_call(
680        callee: &str,
681        place: Self::Expr,
682        _form: Handoff,
683        behaviour: Self::Expr,
684    ) -> Self::Stmt {
685        Stat::Assign {
686            doc: Doc::none(),
687            target: Self::place_of(place),
688            value: Self::codec_call(callee, vec![Self::name("r"), behaviour]),
689        }
690    }
691
692    /// `v = ZD.zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on.
693    fn read_forward(callee: &str) -> Self::Stmt {
694        Self::read_call(
695            callee,
696            Expr::place(Place::name("v")),
697            Handoff::Value,
698            Self::name("zd_tc"),
699        )
700    }
701
702    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
703        statements
704    }
705
706    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
707        block
708    }
709
710    /// Room for `count` values: a fresh table, and — for a `map`, whose
711    /// entries are written half at a time — one `{}` per slot ahead of
712    /// the fill, because `v[i].key = …` needs the slot table to exist.
713    /// A sequence's element decoder assigns the slot outright and needs
714    /// only the outer table; Lua tables grow as they are written.
715    fn make_room(
716        base: &str,
717        _element: Self::Type,
718        count: Self::Expr,
719        assigned_whole: bool,
720        fill: Vec<Self::Stmt>,
721    ) -> Vec<Self::Stmt> {
722        let mut out = vec![Stat::Assign {
723            doc: Doc::none(),
724            target: Self::place_from_name(base),
725            value: Expr::Table(lua::Table::default()),
726        }];
727        if !assigned_whole {
728            out.push(Stat::NumericFor {
729                counter: Ident::new("zd_e"),
730                from: Expr::int(0),
731                to: Expr::Binary {
732                    op: "-",
733                    lhs: Box::new(count),
734                    rhs: Box::new(Expr::int(1)),
735                },
736                body: vec![Stat::Assign {
737                    doc: Doc::none(),
738                    target: Place::Index {
739                        base: Box::new(Place::name(base)),
740                        key: Box::new(Expr::Binary {
741                            op: "+",
742                            lhs: Box::new(Expr::place(Place::name("zd_e"))),
743                            rhs: Box::new(Expr::int(1)),
744                        }),
745                    },
746                    value: Expr::Table(lua::Table::default()),
747                }],
748            });
749        }
750        out.extend(fill);
751        out
752    }
753
754    /// Drop everything past `count`: shorten from the border, one
755    /// element at a time — `t[#t] = nil` moves the border down — until
756    /// the bound holds. §7.3.1.2.1.12.3's TRIM.
757    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
758        vec![Stat::While {
759            condition: Expr::Binary {
760                op: ">",
761                lhs: Box::new(Expr::Unary {
762                    op: "#",
763                    value: Box::new(Expr::place(Place::name(base))),
764                }),
765                rhs: Box::new(count),
766            },
767            body: vec![Stat::Assign {
768                doc: Doc::none(),
769                target: Place::Index {
770                    base: Box::new(Place::name(base)),
771                    key: Box::new(Expr::Unary {
772                        op: "#",
773                        value: Box::new(Expr::place(Place::name(base))),
774                    }),
775                },
776                value: Expr::Nil,
777            }],
778        }]
779    }
780
781    /// The subscript, with nothing before or after it: a Lua table
782    /// element is an ordinary place at every element type.
783    fn sequence_element(
784        base: &str,
785        index: Self::Expr,
786        _element: Self::Type,
787    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
788        (
789            Vec::new(),
790            Self::index(Self::elements(base), index),
791            Vec::new(),
792        )
793    }
794
795    /// `v.field ~= nil` — in Lua the absence of a value is the absence
796    /// of the key, so presence is the key being there.
797    fn present(base: &str, field: &str) -> Self::Expr {
798        Expr::Binary {
799            op: "~=",
800            lhs: Box::new(Self::member(base, field)),
801            rhs: Box::new(Expr::Nil),
802        }
803    }
804
805    /// Nothing on the way in — writing the member is what makes it
806    /// present — and `v.field = nil` on the way out, which removes the
807    /// key.
808    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
809        if present {
810            return Vec::new();
811        }
812        vec![Stat::Assign {
813            doc: Doc::none(),
814            target: Place::name(base).field(field),
815            value: Expr::Nil,
816        }]
817    }
818
819    /// `v.field = nil` for every case: a union's branches are keys beside
820    /// `_d` on one table, and a branch the discriminator does not select
821    /// is a key that is not there. The same statement as an absent
822    /// `@optional` member, [`Self::set_present`] with `false`.
823    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
824        fields
825            .iter()
826            .flat_map(|field| Self::set_present(base, field, false))
827            .collect()
828    }
829
830    /// The member, with nothing to do first: there is no wrapper to
831    /// engage before a table key can be written.
832    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
833        (Vec::new(), Self::member(base, field))
834    }
835
836    /// The member itself: the key holds the value or is absent.
837    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
838        Self::member(base, field)
839    }
840
841    /// `v.field` — a union's branches sit beside `_d` on one table.
842    fn case_member(base: &str, field: &str) -> Self::Expr {
843        Self::member(base, field)
844    }
845
846    /// `entry.key` and `entry.value` — a map entry is a table with the
847    /// two keys `adapter/lua.rs` names.
848    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
849        Self::field_of(entry, if key { "key" } else { "value" })
850    }
851
852    fn absent() -> Self::Expr {
853        Expr::Nil
854    }
855
856    /// Nothing: a Lua string owns its storage and a read that ran out
857    /// of memory raised long before this question.
858    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
859        None
860    }
861
862    /// A local the body writes into: for the one type this is asked
863    /// for — the map entry marker — a fresh table, so `zd_scratch.key`
864    /// has somewhere to land.
865    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
866        if type_.0 == MAP_ENTRY {
867            return Stat::Local {
868                doc: Doc::none(),
869                name: Ident::new(name),
870                value: Some(Expr::Table(lua::Table::default())),
871            };
872        }
873        Stat::Local {
874            doc: Doc::none(),
875            name: Ident::new(name),
876            value: None,
877        }
878    }
879
880    /// `function ZD.zd_enc_T(w, v) … end`, or the reading form that
881    /// opens from Table 9 and returns the sample. A forward declaration
882    /// is answered with nothing: `ZD.<name>` resolves at the call, so a
883    /// recursive or mutually recursive type needs none.
884    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
885        let body = spec.body?;
886        let mut params = vec![Ident::new(if spec.writing { "w" } else { "r" })];
887        if spec.writing {
888            params.push(Ident::new("v"));
889        }
890        for (_, name) in &spec.extra {
891            params.push(Ident::new(name));
892        }
893        let body = if spec.writing {
894            body
895        } else {
896            Self::reading_body(&spec.name, body)
897        };
898        Some(Stat::Function(lua::Function {
899            doc: spec.doc,
900            target: Place::name(CODECS).field(&spec.name),
901            params,
902            body,
903        }))
904    }
905
906    // -- the materialiser's boundary ----------------------------------
907
908    /// # Errors
909    /// Never; the signature matches the emitters around it.
910    fn default_into(
911        ast: &Ast,
912        id: Id<TypeNode>,
913        place: Self::Expr,
914    ) -> Result<Self::Stmt, EncodeError> {
915        Ok(crate::default_lua::default_into(ast, id, place))
916    }
917
918    /// # Errors
919    /// As [`crate::default_lua::member_default`]: a `@default` this
920    /// column cannot spell as a Lua expression is refused rather than
921    /// silently replaced by Table 9's zero.
922    fn member_default(
923        ast: &Ast,
924        node: &MemberNode,
925        type_: Id<TypeNode>,
926        place: Self::Expr,
927    ) -> Result<Vec<Self::Stmt>, EncodeError> {
928        crate::default_lua::member_default(ast, node, type_, place)
929    }
930
931    /// `M.a.b.Colour.values.RED` — the declared value out of the
932    /// descriptor the materialiser wrote, through its own path
933    /// derivation, so the codec cannot part from it over a spelling.
934    /// An enumeration member is an integer here, so the constant
935    /// compares directly against what the member holds.
936    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
937        match declared_place(ast, id) {
938            Ok(place) => Expr::place(place.field("values").field(enumerator)),
939            Err(_) => Self::name(enumerator),
940        }
941    }
942}
943
944/// The Lua holder of a value, as a tag.
945///
946/// Only `boolean` does work — [`Lua::cast`] rewrites a union label
947/// against it — and the rest are carried for the reader of the
948/// generated tree. The mapping is `adapter/lua.rs`'s.
949///
950/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
951fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> LuaType {
952    use zerodds_ast::PrimitiveKind;
953    if depth > MAX_TYPE_DEPTH {
954        return LuaType("table".to_string());
955    }
956    let Ok(node) = ast.type_(id) else {
957        return LuaType("table".to_string());
958    };
959    LuaType(
960        match &node.kind {
961            TypeKind::Primitive(kind) => match kind {
962                PrimitiveKind::Boolean => "boolean",
963                PrimitiveKind::Float32 | PrimitiveKind::Float64 => "number",
964                // The decimal text.
965                PrimitiveKind::Float128 => "string",
966                // A char is the code point; every other width is an
967                // integer too.
968                _ => "integer",
969            },
970            TypeKind::String { .. } | TypeKind::Fixed { .. } => "string",
971            TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => "integer",
972            TypeKind::Alias { aliased } => {
973                return aliased.resolved().map_or_else(
974                    || LuaType("table".to_string()),
975                    |target| value_type_of(ast, target, depth + 1),
976                );
977            }
978            TypeKind::ValueBox { boxed } => {
979                return boxed.resolved().map_or_else(
980                    || LuaType("table".to_string()),
981                    |target| value_type_of(ast, target, depth + 1),
982                );
983            }
984            _ => "table",
985        }
986        .to_string(),
987    )
988}
989
990/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
991/// that keeps two of them apart.
992///
993/// The tag names the routine a hoisted collection's codec is emitted
994/// under, and one body is emitted per name. With the bound left out,
995/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
996/// was declared first decides: measured on
997/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
998/// generated Lua held no `zd_over` check at all, so `a` accepted a
999/// stream of any length; with the members the other way round both got
1000/// the bounded body, so `b` refused — and truncated — a length the IDL
1001/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
1002/// beside `wstring` collapse the same way, onto the trimming call.
1003/// Either way the reader answered with a value the IDL does not
1004/// describe and said nothing, which is why the bound is part of the
1005/// name. The same suffix the Zig column carries.
1006///
1007/// An `array` needs nothing here: its `dims` are already in the tag,
1008/// under the `Array` arm below.
1009fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1010    match bound.expr() {
1011        None => String::new(),
1012        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1013        // arm answers for an array of four `long`, and the two are
1014        // different wire forms.
1015        Some(expr) => match ast.eval_integral(expr) {
1016            Ok(limit) => format!("_b{limit}"),
1017            // As the array's `_x`: a bound that does not evaluate is
1018            // still not the unbounded form, and the materialiser
1019            // reports the expression that would not evaluate.
1020            Err(_) => "_bx".to_string(),
1021        },
1022    }
1023}
1024
1025/// The tag a codec function is named after, from the IDL kind and never
1026/// from the Lua spelling — Lua's spelling is `integer` for ten of the
1027/// fifteen primitives, and a tag built from it would give them one
1028/// codec. The same scheme `adapter::python::type_tag` uses, without the
1029/// escape: Lua renames nothing, and the tag lands behind `ZD.zd_enc_`
1030/// where no keyword can collide.
1031///
1032/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1033fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1034    if depth > MAX_TYPE_DEPTH {
1035        return "unknown".to_string();
1036    }
1037    let Ok(node) = ast.type_(id) else {
1038        return "unknown".to_string();
1039    };
1040    match &node.kind {
1041        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1042        TypeKind::String {
1043            width: CharWidth::Char,
1044            bound,
1045        } => format!("string{}", bound_tag(ast, bound)),
1046        TypeKind::String {
1047            width: CharWidth::WChar,
1048            bound,
1049        } => format!("wstring{}", bound_tag(ast, bound)),
1050        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1051        TypeKind::Sequence { element, bound } => match element.resolved() {
1052            Some(element) => format!(
1053                "seq{}_{}",
1054                bound_tag(ast, bound),
1055                type_tag(ast, element, depth + 1)
1056            ),
1057            None => format!("seq_unknown{}", bound_tag(ast, bound)),
1058        },
1059        TypeKind::Array { element, dims } => {
1060            let mut name = match element.resolved() {
1061                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1062                None => "arr_unknown".to_string(),
1063            };
1064            for dim in dims {
1065                match ast.eval_integral(*dim) {
1066                    Ok(length) => name.push_str(&format!("_{length}")),
1067                    Err(_) => name.push_str("_x"),
1068                }
1069            }
1070            name
1071        }
1072        TypeKind::Map { key, value, bound } => {
1073            let half = |half: Option<Id<TypeNode>>| match half {
1074                Some(target) => type_tag(ast, target, depth + 1),
1075                None => "unknown".to_string(),
1076            };
1077            format!(
1078                "map{}_{}_{}",
1079                bound_tag(ast, bound),
1080                half(key.resolved()),
1081                half(value.resolved())
1082            )
1083        }
1084        // A **named** typedef keeps a tag of its own, unlike the Python
1085        // column's, and the difference is measured: `ordered_types_from`
1086        // can visit an alias ahead of a forward-declared target —
1087        // `recursive-typedef-sequence` does exactly that — and two
1088        // types sharing one tag means the alias's forwarding body takes
1089        // the name and the target's real body is never emitted, so the
1090        // forwarder called itself until the stack ran out. A distinct
1091        // tag makes the alias an ordinary one-line forwarder whichever
1092        // of the two is visited first. An anonymous alias is the
1093        // frontend's own wrapper for an inline type and stays
1094        // transparent.
1095        TypeKind::Alias { aliased } => {
1096            if !node.name.is_empty() {
1097                let scoped = crate::adapter::lua::scoped_name(ast, id);
1098                return scoped.trim_start_matches(':').replace("::", "_");
1099            }
1100            match aliased.resolved() {
1101                Some(target) => type_tag(ast, target, depth + 1),
1102                None => "unknown".to_string(),
1103            }
1104        }
1105        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1106            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1107            None => "box_unknown".to_string(),
1108        },
1109        TypeKind::Structure { .. }
1110        | TypeKind::Union { .. }
1111        | TypeKind::Enumeration { .. }
1112        | TypeKind::Bitmask { .. }
1113        | TypeKind::Bitset { .. }
1114        | TypeKind::Valuetype { .. }
1115        | TypeKind::Native => {
1116            let scoped = crate::adapter::lua::scoped_name(ast, id);
1117            scoped
1118                .trim_start_matches(':')
1119                .replace("::", "_")
1120                .replace(['<', '>'], "_")
1121        }
1122        TypeKind::Any => "any".to_string(),
1123        TypeKind::ValueBase => "valuebase".to_string(),
1124        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1125        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1126    }
1127}