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}