Skip to main content

zerodds_lang_rules/adapter/
lua.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Lua CST as the other end of a rule. Spec §3.2.
4//!
5//! The counterpart of [`crate::adapter::python`], which asks the same
6//! question, and of [`crate::adapter::go`], which does not have to.
7//!
8//! Values are resolved from the model, never from the overlay — the same
9//! rule the sibling materialisers follow and for the same reason
10//! (spec §2.3).
11//!
12//! # Lua is the hardest of the ten, and it is worth saying why plainly
13//!
14//! Python at least has a place to write an annotation. Lua has none.
15//! There is no type syntax, no declaration, no `struct`, no constant and
16//! no annotation the language itself reads. A struct is a table. So is a
17//! sequence, a map, an array and a union. `math.type` distinguishes an
18//! integer from a float and that is the entire type information a Lua
19//! program can obtain about a value it was handed.
20//!
21//! A column that emitted only constructors would therefore carry **none
22//! of the IDL**: not a width, not a signedness, not a bound, not a
23//! member id — and not the member **order**, which matters here in a way
24//! it does not in Python. A Python class body has an order and
25//! `__annotations__` keeps it. A Lua table has none: `next`, and
26//! therefore `pairs`, "does not specify any order for the traversal"
27//! (Lua 5.4 reference §6.1). DDS-XTypes writes an aggregate's members in
28//! declaration order (DDS-XTypes 1.3 §7.4.1.1.5.1), so an order that is
29//! not written down is an order that is gone.
30//!
31//! So this column emits **two things and both are data**:
32//!
33//! 1. **A constructor per type** — `M.Pose.new()` — returning a table
34//!    with every member at the default of its type, so a program can
35//!    make a value without reading anything.
36//! 2. **A descriptor table per type** — `M.Pose` itself — holding the
37//!    member list **in the array part**, where the index is the order,
38//!    each member with its IDL name, its Lua key, its kind, its width,
39//!    its signedness, its bound, whether it is optional, whether it is a
40//!    key and its member id. That table is what a codec reads. Without
41//!    it this column carries nothing.
42//!
43//! Every descriptor is also registered in `M._descriptors`, keyed by the
44//! IDL scoped name (`::common::util::Location`), which is the one name
45//! that survives every mapping in this file.
46//!
47//! ## `---@class` and `---@field` are comments
48//!
49//! Above each type the file carries a LuaCATS block —
50//! `---@class Pose`, `---@field x integer` — which
51//! `lua-language-server` reads and an editor shows. **Nothing in the Lua
52//! runtime reads it.** It cannot refuse a program, it cannot refuse a
53//! value, and it is discarded by `luac`. It is written because an editor
54//! checking what Lua cannot is worth having, and it is named as a
55//! comment here so that nobody mistakes it for a check.
56//!
57//! ## Modules nest, they do not flatten
58//!
59//! `::common::util::Location` is `M.common.util.Location`. A Lua table
60//! key is a string and any string will do, so the IDL scope survives
61//! whole. The Go column flattens to `Common_Util_Location` because a Go
62//! package is a directory and cannot nest inside one file; that join is
63//! not invertible, since an IDL identifier may itself contain `_`
64//! (IDL 4.2 §7.2.3). Nesting is, so this column takes it.
65//!
66//! ## No identifier is ever renamed
67//!
68//! This is the one thing this column has that the other nine do not.
69//! Lua restricts the **syntax** `t.name` to identifiers that are not
70//! keywords, and not the key: `t.end` does not parse and `t["end"]`
71//! does, and both name the key `"end"` (Lua 5.4 reference §3.4.9). So a
72//! member called `end` keeps the name `end` — only the form it is
73//! written in changes, which [`zerodds_lang_cst::lua::table_key`] and
74//! [`zerodds_lang_cst::lua::Place::field`] decide. Every descriptor
75//! carries `name` and `key` anyway, and in this column they are always
76//! the same string; a codec reads `key`, so if that ever stopped being
77//! true the codec would not change.
78//!
79//! ## The mapping
80//!
81//! | IDL | Lua value | where the rest is written |
82//! |---|---|---|
83//! | `boolean` | `false` / `true` | `kind = "boolean"` |
84//! | `octet`, `uint8`, `int8` … `long long` | integer | `kind`, `bits`, `signed` |
85//! | `unsigned long long` | integer, **two's complement** | `signed = false`, `wraps = true` |
86//! | `char`, `wchar` | integer | `kind = "char"`, `bits = 8` or `16` |
87//! | `float`, `double` | float | `kind = "float"`, `bits` |
88//! | `long double` | **string**, the decimal | `kind = "float"`, `bits = 128` |
89//! | `string`, `wstring` | string | `kind = "string"`, `bits`, `bound` |
90//! | `fixed<P,S>` | string, the decimal | `kind = "decimal"`, `digits`, `scale` |
91//! | `sequence<T,N>` | table, **1-based** | `kind = "sequence"`, `base = 1`, `bound`, `element` |
92//! | `T[N][M]` | nested tables, 1-based | `kind = "array"`, `dims = { N, M }` |
93//! | `map<K,V>` | array of `{ key = …, value = … }` | `kind = "map"`, `key_type`, `value_type` |
94//! | `enum` | integer | `values`, `names`, `enumerators`, `bit_bound` |
95//! | `bitmask` | integer | `values`, `bits`, `bit_bound` |
96//! | `union` | table with `_d` and one key per branch | `discriminator`, `members` |
97//! | `@optional T m` | the key, or no key | `optional = true` |
98//! | `typedef` | nothing | `kind = "alias"`, `target` |
99//! | `native` | opaque | `kind = "native"`, `mapping` |
100//! | `const` | `M.LIMIT = 8` | the `---` line above it |
101//! | `interface` | table of operation descriptors | `operations` |
102//!
103//! ## Which Lua this was measured against
104//!
105//! The target is Lua 5.4 and every clause cited here is from its
106//! reference manual. The interpreter the measurements below were taken
107//! with is the one on the path, which reports `Lua 5.5.0`: integers and
108//! floats are still two subtypes of `number`, an integer is still 64-bit
109//! two's complement, a hexadecimal numeral still wraps, `pairs` still
110//! has no defined order and an array still starts at 1. Where a
111//! measurement is quoted it is that interpreter's output, and it is
112//! quoted rather than described so that a later version disagreeing with
113//! it is visible.
114//!
115//! ### `unsigned long long` is an integer holding the bit pattern
116//!
117//! Lua 5.4 integers are 64-bit two's complement and wrap: measured with
118//! the interpreter on the path,
119//! `lua -e 'print(math.maxinteger + 1 == math.mininteger)'` prints
120//! `true`. So a `unsigned long long` above 2^63 − 1 has no Lua integer
121//! that reads as the number it is — `18446744073709551615` reads back as
122//! `-1`.
123//!
124//! A decimal **string** was considered, as `long double` and `fixed` get.
125//! It is not taken, for two reasons. The eight octets CDR writes are
126//! exactly the eight octets a Lua integer holds, so the integer is
127//! lossless on the wire and only the *reading* of it as a number is
128//! lost; and a string would make one of the ten integers a different
129//! Lua type from the other nine, so a codec would branch on the width it
130//! is already reading from the descriptor. What the descriptor says
131//! instead is `signed = false, wraps = true`, which is the fact, and a
132//! range check is the codec's to make.
133//!
134//! A **constant** is the one place this bites, because it has to be
135//! written as a literal. Lua 5.4 reference §3.1: a decimal numeral whose
136//! value does not fit in an integer "denotes a float", and a hexadecimal
137//! one "wraps around" to fit. Measured:
138//! `lua -e 'print(math.type(18446744073709551615))'` prints `float`, and
139//! `lua -e 'print(math.type(0xFFFFFFFFFFFFFFFF), 0xFFFFFFFFFFFFFFFF)'`
140//! prints `integer` and `-1`. So a constant above 2^63 − 1 is written in
141//! hexadecimal, which is the exact bit pattern; one that does not fit in
142//! 64 bits at all is refused by name.
143//!
144//! ### `long double` is a string and never a Lua float
145//!
146//! A Lua float is a C `double` — 53 bits of significand — and an IDL
147//! `long double` is a binary128 with 113. Writing one into a float loses
148//! sixty bits at the assignment, before any codec runs. The member holds
149//! the decimal text instead, as it does in the Go and Python columns.
150//!
151//! ### A sequence is 1-based and an IDL index is not
152//!
153//! A Lua table used as an array starts at 1: the length operator and
154//! `ipairs` both assume it (Lua 5.4 reference §3.4.7). An IDL sequence
155//! index starts at 0. So element *i* of a `sequence<T>` is `t[i + 1]`,
156//! and this is the single most likely defect in any program written
157//! against this column. The descriptor says `base = 1` on every
158//! sequence, array and map, so a codec reads the convention rather than
159//! assuming it.
160//!
161//! ### `map<K,V>` is an array of pairs and never a Lua table keyed by K
162//!
163//! Three reasons, any one of which decides it.
164//!
165//! `pairs` has no defined order — `next` "does not specify any order for
166//! the traversal" (Lua 5.4 reference §6.1) — and a `map<K,V>` goes on
167//! the wire as an ordered sequence of key/value pairs
168//! (DDS-XTypes 1.3 §7.4.1.1.4). A Lua table would give a different octet
169//! stream for one value depending on nothing the program controls.
170//!
171//! A Lua table cannot hold a `nil` **value** at all: assigning `nil`
172//! removes the key. A map whose value type is `@optional`, or whose
173//! value is simply absent, has nowhere to live.
174//!
175//! And a Lua table cannot hold a `nil` **key** either, nor two keys that
176//! compare equal by value but not by identity — `map<Point, long>` is
177//! ordinary IDL, and two tables holding the same numbers are two
178//! different keys.
179//!
180//! ### `@optional` is the absence of the key, and that is why the
181//! descriptor exists
182//!
183//! DDS-XTypes 1.3 §7.2.2.4.4.4.7 makes **absence** the value. In Lua the
184//! absence of a value is the absence of the key: `t.m = nil` removes it,
185//! `#t` does not see it and `pairs` does not visit it. So a program
186//! holding a value cannot tell "the member is absent" from "this table
187//! was never a Pose" — the *value* carries no member list at all.
188//!
189//! That is exactly why the descriptor carries the member list in order.
190//! `M.Pose.members` is where the members are; the value is where the
191//! ones that are present are.
192//!
193//! ### A union has a `_d` and one key per branch, not a tag and a payload
194//!
195//! An IDL union discriminator may hold a value **no branch selects**
196//! (IDL 4.2 §7.4.1.4.4.1; DDS-XTypes 1.3 §7.4.1.1.5.2 writes only the
197//! selected branch, and there may be none). A `{ tag, payload }` shape
198//! cannot say that without inventing a tag the IDL does not have. So
199//! `_d` is a member in its own right and the branches sit beside it,
200//! at most one of them present. `_d` cannot collide with a branch: an
201//! IDL identifier begins with an ASCII letter (IDL 4.2 §7.2.3), so no
202//! IDL member is spelled `_d`.
203//!
204//! ## What is refused
205//!
206//! `any`, `ValueBase`, an object reference and an annotation type have
207//! no Lua data form and are named as
208//! [`MaterialiseError::NoLanguageForm`] where a member would carry one.
209//! Nothing is emitted for them at declaration level.
210//!
211//! **This matters more here than anywhere else.** Every other column has
212//! at least a compiler between the generated file and the program: Go
213//! would need a type for an `any` member, and there is none. Lua needs
214//! nothing — a table entry holding whatever an `any` turned out to be
215//! would load, run and report success, with no wire form behind it. That
216//! is the silent-substitution class this project is arranged against,
217//! and Lua is where it is easiest to commit.
218//!
219//! ## What the generated file checks at load, and what it does not
220//!
221//! Two things run while the chunk executes, and both are written into
222//! the file rather than left to a test:
223//!
224//! - `M._resolved` resolves every type name a descriptor mentions
225//!   through `assert`. Reading a missing key in Lua answers `nil` and
226//!   carries on, so a descriptor naming a type the file does not declare
227//!   would otherwise be found by whatever read it next.
228//! - a discarded local calls every constructor once. A Lua function body
229//!   is not checked against anything until it is called, so a load that
230//!   did not call them would say nothing about them.
231//!
232//! Everything else Lua does not check, and this column does not claim it
233//! does: no width, no bound, no member order and no member type is
234//! enforced at run time by anything in the generated file.
235
236use crate::adapter::constant;
237use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
238use crate::adapter::interface;
239use crate::apply::Applied;
240use crate::fixed_literal::zero_decimal;
241use crate::verbatim;
242use seesaw_tgg::ident::GhostId;
243use zerodds_ast::{
244    Ast, Bound, CharWidth, ConstValue, ModuleNode, PrimitiveKind, TypeKind, TypeNode,
245};
246use zerodds_ast_core::{Id, NodeId};
247use zerodds_lang_cst::common::{Doc, Ident, Literal};
248use zerodds_lang_cst::lua::{self, Entry, Expr, File, Function, Place, Stat, Table};
249
250/// The language this column names in a diagnostic.
251const LANGUAGE: &str = "Lua";
252
253/// The local the generated chunk builds and returns.
254///
255/// One letter, and the same in every generated file. A Lua module is
256/// whatever its chunk returns (Lua 5.4 reference §6.3, `require`), so the
257/// name is local to the file and never seen by a consumer — which is why
258/// it can be short without being cryptic.
259pub const MODULE: &str = "M";
260
261/// The key a union's discriminator takes.
262///
263/// `_d`, as in every other column. It cannot collide with a branch: an
264/// IDL identifier begins with an ASCII letter (IDL 4.2 §7.2.3), so no
265/// IDL member is spelled `_d`.
266pub const DISCRIMINATOR: &str = "_d";
267
268/// The key holding every descriptor, by IDL scoped name.
269const DESCRIPTORS: &str = "_descriptors";
270
271/// The key holding every declared scoped name, in declaration order.
272const NAMES: &str = "_names";
273
274/// The key holding the load-time resolution of every named reference.
275const RESOLVED: &str = "_resolved";
276
277/// How far up a module chain the walk follows before the model is taken
278/// to be malformed. IDL nests nowhere near this.
279const MAX_MODULE_DEPTH: usize = 64;
280
281/// How deep a type's descriptor is followed.
282///
283/// A `sequence<sequence<…>>` nests once per level; a chain longer than
284/// this is a cycle the resolver did not catch, and recursing into it
285/// would end the process rather than report anything.
286const MAX_TYPE_DEPTH: usize = 64;
287
288/// Build the Lua module a rule application produced.
289///
290/// The whole file and not a list of statements: the local the chunk
291/// builds, the registry every descriptor lands in, the load-time
292/// resolution and the `return` are all part of what this column decides,
293/// and a caller assembling them separately would be a second place where
294/// they are decided.
295///
296/// # Errors
297/// [`MaterialiseError`] when a value the output has to state does not
298/// evaluate, a type has no Lua form, or two declarations reach one Lua
299/// path — never a silently dropped declaration.
300pub fn lua_file(ast: &Ast, applied: &Applied) -> Result<File, MaterialiseError> {
301    let mut builder = Builder::new(ast);
302    builder.walk(applied)?;
303    Ok(File {
304        end_text: verbatim::file_slots(ast, verbatim::aliases::LUA).1,
305        doc: Doc::lines(&[
306            "Generated by ZeroDDS from an IDL source. Do not edit.",
307            "",
308            "Lua declares nothing. A struct, a sequence, an array, a map and a",
309            "union are all tables, and `math.type` is the only type question a",
310            "program can ask of a value. So every IDL fact is written into a",
311            "descriptor table instead: the member list in declaration order, and",
312            "for each member its IDL name, its Lua key, its kind, its width, its",
313            "signedness, its bound, whether it is optional, whether it is a key",
314            "and its member id.",
315            "",
316            "M._descriptors maps the IDL scoped name of every declared type to its",
317            "descriptor. M._names lists those names in declaration order.",
318            "",
319            "The ---@class and ---@field blocks are comments. lua-language-server",
320            "reads them; nothing in the Lua runtime does.",
321            "",
322            "A sequence, an array and a map are 1-based here and 0-based in IDL:",
323            "element i of a sequence is t[i + 1]. Every such descriptor says",
324            "base = 1 rather than leaving it to be assumed.",
325        ]),
326        body: {
327            let (begin_file, _) = verbatim::file_slots(ast, verbatim::aliases::LUA);
328            let mut body: Vec<Stat> = begin_file.into_iter().map(Stat::Verbatim).collect();
329            body.extend(builder.finish());
330            body
331        },
332        returns: Expr::place(Place::name(MODULE)),
333    })
334}
335
336/// The declarations only, without the chunk around them.
337///
338/// For a caller that emits its own preamble — a codec pass writing
339/// beside the types. The statements name `M`, so the caller has to bind
340/// it; [`lua_file`] is what a generator writes to disk. The module
341/// tables come first (`M.Robot = {}` before `M.Robot.Pose = …`): without
342/// them the first declaration inside a module indexes nil at load.
343///
344/// # Errors
345/// As [`lua_file`].
346pub fn lua_items(ast: &Ast, applied: &Applied) -> Result<Vec<Stat>, MaterialiseError> {
347    let mut builder = Builder::new(ast);
348    builder.walk(applied)?;
349    let mut out = builder.module_tables;
350    out.append(&mut builder.declarations);
351    Ok(verbatim::weave_file(ast, verbatim::aliases::LUA, out))
352}
353
354/// `@verbatim` in the Lua column. A declaration is a table constructor,
355/// which holds fields and not statements, so `BEGIN_DECLARATION` and
356/// `END_DECLARATION` land adjacent to it.
357impl verbatim::Carrier for Stat {
358    fn verbatim(text: String) -> Self {
359        Self::Verbatim(text)
360    }
361
362    fn take_into_body(&mut self, _: &str, _: &mut Vec<String>, _: &mut Vec<String>) -> bool {
363        false
364    }
365}
366
367/// The descriptor a value of `target` is described by.
368///
369/// The one answer, for every emitter: a member of a generated struct, a
370/// codec's parameter and a defaulter's destination all read the same
371/// table or they disagree about the wire.
372///
373/// # Errors
374/// [`MaterialiseError::NoLanguageForm`] for `any`, `ValueBase`, an
375/// object reference and an annotation type;
376/// [`MaterialiseError::Unevaluated`] for a bound or a dimension the
377/// model will not answer for.
378pub fn lua_type_descriptor(
379    ast: &Ast,
380    target: Id<TypeNode>,
381    at: &str,
382) -> Result<Table, MaterialiseError> {
383    let mut refs = Vec::new();
384    Ok(Table::of(type_entries(ast, target, at, &mut refs, 0)?))
385}
386
387// -- the walk -------------------------------------------------------------
388
389/// The statements of one generated chunk, as they are built.
390struct Builder<'a> {
391    ast: &'a Ast,
392    /// The module tables, emitted once each, parents first.
393    module_tables: Vec<Stat>,
394    /// The Lua paths already emitted for a module table.
395    module_paths: Vec<String>,
396    /// One declaration's statements, in model order.
397    declarations: Vec<Stat>,
398    /// Every Lua path a declaration took, with the IDL name behind it.
399    emitted: Vec<(String, String)>,
400    /// Every declared IDL scoped name, in declaration order.
401    names: Vec<String>,
402    /// Every scoped name a descriptor mentions, with the first mention.
403    refs: Vec<(String, String)>,
404    /// The constructors, so the chunk can run each once at load.
405    constructors: Vec<Place>,
406}
407
408impl<'a> Builder<'a> {
409    fn new(ast: &'a Ast) -> Self {
410        Self {
411            ast,
412            module_tables: Vec::new(),
413            module_paths: Vec::new(),
414            declarations: Vec::new(),
415            emitted: Vec::new(),
416            names: Vec::new(),
417            refs: Vec::new(),
418            constructors: Vec::new(),
419        }
420    }
421
422    /// Every declaration the rules created, in the model's own order.
423    ///
424    /// Nothing in a Lua chunk has to be declared before it is named: a
425    /// constructor's body runs when it is called, and a descriptor names
426    /// another type by its scoped name and not by its table. So the walk
427    /// is the model's declaration order and the output tracks the source,
428    /// as the Go column's does and unlike the C column's, which has to
429    /// order the type graph.
430    fn walk(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
431        let ast = self.ast;
432        for (id, _) in ast.types() {
433            let start = self.declarations.len();
434            self.declare_type(applied, id)?;
435            verbatim::weave(
436                verbatim::declaration_slots(ast, id, verbatim::aliases::LUA),
437                "",
438                start,
439                &mut self.declarations,
440            );
441        }
442        self.declare_constants(applied)?;
443        self.declare_exceptions(applied)?;
444        self.declare_interfaces(applied)?;
445        Ok(())
446    }
447
448    /// The chunk's statements, in written order.
449    fn finish(mut self) -> Vec<Stat> {
450        let mut out = vec![
451            Stat::Local {
452                doc: Doc::none(),
453                name: Ident::new(MODULE),
454                value: Some(Expr::Table(Table::default())),
455            },
456            Stat::Assign {
457                doc: Doc::lines(&[
458                    "The descriptor of every type this module declares, by its IDL",
459                    "scoped name. That name is the only one that survives every",
460                    "mapping in this file, so it is what a codec looks a type up by.",
461                ]),
462                target: Place::name(MODULE).field(DESCRIPTORS),
463                value: Expr::Table(Table::default()),
464            },
465        ];
466        out.append(&mut self.module_tables);
467        out.append(&mut self.declarations);
468        out.push(Stat::Assign {
469            doc: Doc::lines(&[
470                "Every declared type, in IDL declaration order.",
471                "",
472                "The array part of a table, because that is the only part of a Lua",
473                "table that has an order: `next` does not specify one for the rest",
474                "(Lua 5.4 reference 6.1).",
475            ]),
476            target: Place::name(MODULE).field(NAMES),
477            value: Expr::Table(Table::of(
478                self.names
479                    .iter()
480                    .map(|name| Entry::positional(Expr::string(name)))
481                    .collect(),
482            )),
483        });
484        out.push(Stat::Assign {
485            doc: Doc::lines(&[
486                "Every type a descriptor names, resolved while this chunk runs.",
487                "",
488                "Reading a missing key in Lua answers nil and carries on, so a",
489                "descriptor naming a type this module does not declare would be",
490                "found by whatever read it next rather than here. `assert` is what",
491                "notices, and it notices at load.",
492            ]),
493            target: Place::name(MODULE).field(RESOLVED),
494            value: Expr::Table(Table::of(self.reference_entries())),
495        });
496        out.push(Stat::Comment(Doc::lines(&[
497            "Every constructor, run once and discarded. A Lua function body is",
498            "not checked against anything until it is called, so a load that did",
499            "not call these would say nothing about them.",
500        ])));
501        out.push(Stat::Local {
502            doc: Doc::none(),
503            name: Ident::new("_"),
504            value: Some(Expr::Table(Table::of(
505                self.constructors
506                    .iter()
507                    .map(|target| {
508                        Entry::positional(Expr::Call {
509                            func: Box::new(target.clone()),
510                            args: Vec::new(),
511                        })
512                    })
513                    .collect(),
514            ))),
515        });
516        out
517    }
518
519    /// One `assert` per distinct scoped name a descriptor mentions.
520    fn reference_entries(&self) -> Vec<Entry> {
521        self.refs
522            .iter()
523            .map(|(name, at)| Entry {
524                doc: Doc::none(),
525                key: lua::Key::Expr(Expr::string(name)),
526                value: Expr::Call {
527                    func: Box::new(Place::name("assert")),
528                    args: vec![
529                        Expr::place(Place::Index {
530                            base: Box::new(Place::name(MODULE).field(DESCRIPTORS)),
531                            key: Box::new(Expr::string(name)),
532                        }),
533                        Expr::string(&format!(
534                            "{at} names {name}, which this module does not declare"
535                        )),
536                    ],
537                },
538            })
539            .collect()
540    }
541
542    /// The place a declaration in `module` named `name` is assigned to,
543    /// with every module table on the way emitted first.
544    ///
545    /// # Errors
546    /// [`MaterialiseError::NoModule`] when the model does not answer for
547    /// a module in the chain, or the chain runs past
548    /// [`MAX_MODULE_DEPTH`]. Placing the declaration at the root instead
549    /// would put it in another scope and say nothing, which is the same
550    /// class of substitution as writing the wrong octet.
551    fn place_of(&mut self, module: Id<ModuleNode>, name: &str) -> Result<Place, MaterialiseError> {
552        let ast = self.ast;
553        let chain = module_chain(ast, module)?;
554        let mut place = Place::name(MODULE);
555        let mut text = MODULE.to_string();
556        for segment in &chain {
557            place = place.field(segment);
558            text.push('.');
559            text.push_str(segment);
560            if !self.module_paths.contains(&text) {
561                self.module_paths.push(text.clone());
562                self.module_tables.push(Stat::Assign {
563                    doc: Doc::line(&format!(
564                        "The IDL module `{segment}`, as the table its declarations nest in."
565                    )),
566                    target: place.clone(),
567                    value: Expr::Table(Table::default()),
568                });
569            }
570        }
571        Ok(place.field(name))
572    }
573
574    /// The text of a Lua path, for the whole-file collision check.
575    fn path_text(module: &[String], name: &str) -> String {
576        let mut text = MODULE.to_string();
577        for segment in module {
578            text.push('.');
579            text.push_str(segment);
580        }
581        text.push('.');
582        text.push_str(name);
583        text
584    }
585
586    /// Record a declared path, refusing a second declaration reaching it.
587    ///
588    /// Lua binds a key; it does not declare one. `M.Pose = …` twice is
589    /// not an error, it is a rebinding, and the second silently wins — so
590    /// one of the two types would be gone from a file that loads. IDL
591    /// forbids two declarations of one scoped name, which is why this
592    /// should never fire; it is checked anyway, because "should never"
593    /// about a name collision is the assumption that makes one
594    /// declaration take another's.
595    fn remember(&mut self, path: &str, declared: &str) -> Result<(), MaterialiseError> {
596        if let Some((_, first)) = self.emitted.iter().find(|(other, _)| other == path) {
597            return Err(MaterialiseError::SpellingCollision {
598                first: first.clone(),
599                second: declared.to_string(),
600                spelled: path.to_string(),
601                language: LANGUAGE,
602                at: "the generated chunk".to_string(),
603            });
604        }
605        self.emitted.push((path.to_string(), declared.to_string()));
606        Ok(())
607    }
608
609    /// Emit one declaration: its LuaCATS block, its descriptor, its
610    /// registration and, where it has one, its constructor.
611    fn emit(&mut self, decl: Decl) -> Result<(), MaterialiseError> {
612        let ast = self.ast;
613        let chain = module_chain(ast, decl.module)?;
614        let path = Self::path_text(&chain, &decl.name);
615        self.remember(&path, &decl.scoped)?;
616        let target = self.place_of(decl.module, &decl.name)?;
617
618        if !decl.annotation.is_empty() {
619            self.declarations.push(Stat::Annotation(decl.annotation));
620        }
621        self.declarations.push(Stat::Assign {
622            doc: decl.doc,
623            target: target.clone(),
624            value: Expr::Table(decl.descriptor),
625        });
626        self.declarations.push(Stat::Assign {
627            doc: Doc::none(),
628            target: Place::Index {
629                base: Box::new(Place::name(MODULE).field(DESCRIPTORS)),
630                key: Box::new(Expr::string(&decl.scoped)),
631            },
632            value: Expr::place(target.clone()),
633        });
634        self.names.push(decl.scoped.clone());
635
636        if let Some(value) = decl.constructor {
637            let new = target.field("new");
638            let mut doc = Doc::line(&format!(
639                "A `{}` with every member at the default of its type.",
640                decl.scoped
641            ));
642            doc.push("");
643            doc.push("An @optional member is not given a key at all: in Lua the absence");
644            doc.push("of a value is the absence of the key (DDS-XTypes 1.3 7.2.2.4.4.4.7).");
645            doc.push(&format!("@return {}", decl.class));
646            self.declarations.push(Stat::Function(Function {
647                doc,
648                target: new.clone(),
649                params: Vec::new(),
650                body: vec![Stat::Return(Some(value))],
651            }));
652            self.constructors.push(new);
653        }
654        Ok(())
655    }
656}
657
658/// One declaration, as this column emits it.
659struct Decl {
660    /// The IDL module it was declared in.
661    module: Id<ModuleNode>,
662    /// The IDL simple name, which is also the Lua key.
663    name: String,
664    /// The IDL scoped name, `::` separated.
665    scoped: String,
666    /// The LuaCATS class name, dotted.
667    class: String,
668    /// The `---@class` block, or empty where there is no value shape.
669    annotation: Doc,
670    /// The `---` block above the descriptor.
671    doc: Doc,
672    /// The descriptor table.
673    descriptor: Table,
674    /// The constructor's body, where the type has a value shape.
675    constructor: Option<Expr>,
676}
677
678// -- types ----------------------------------------------------------------
679
680impl Builder<'_> {
681    #[allow(clippy::too_many_lines)]
682    fn declare_type(
683        &mut self,
684        applied: &Applied,
685        id: Id<TypeNode>,
686    ) -> Result<(), MaterialiseError> {
687        let ast = self.ast;
688        let Ok(node) = ast.type_(id) else {
689            return Ok(());
690        };
691        let scoped = scoped_name(ast, id);
692        let class = class_name(ast, id);
693
694        match &node.kind {
695            // A primitive, a string, a `fixed`, a sequence, an array and
696            // a map are all written where the member is: each is a
697            // descriptor entry and none of them declares anything. Lua
698            // needs no entry class for a map either, which the Go column
699            // does — a pair is a table with two keys and needs no name.
700            TypeKind::Primitive(_)
701            | TypeKind::String { .. }
702            | TypeKind::Fixed { .. }
703            | TypeKind::Sequence { .. }
704            | TypeKind::Map { .. }
705            | TypeKind::Array { .. } => {}
706
707            TypeKind::Alias { aliased } => {
708                // A typedef with no name is one the frontend made for an
709                // inline type; it is transparent and declares nothing.
710                if node.name.is_empty() {
711                    return Ok(());
712                }
713                if ghost_created_for(applied, node.node_id, "LuaAlias").is_none() {
714                    return Ok(());
715                }
716                let Some(target) = aliased.resolved() else {
717                    return Err(MaterialiseError::Unevaluated {
718                        construct: "typedef target".to_string(),
719                        at: node.name.clone(),
720                    });
721                };
722                let inner = Table::of(type_entries(ast, target, &scoped, &mut self.refs, 0)?);
723                self.emit(Decl {
724                    module: node.module,
725                    name: node.name.clone(),
726                    scoped: scoped.clone(),
727                    class: cats_type(ast, target, 0),
728                    annotation: Doc::none(),
729                    doc: Doc::lines(&[
730                        "A typedef. Lua has no alias to declare, so what is left of it",
731                        "is this entry: the name, and the descriptor of what it names.",
732                        "Every use of the alias takes the target's descriptor.",
733                    ]),
734                    descriptor: Table::of(vec![
735                        Entry::named("idl", Expr::string(&scoped)),
736                        Entry::named("name", Expr::string(&node.name)),
737                        Entry::named("kind", Expr::string("alias")),
738                        Entry::named("target", Expr::Table(inner)),
739                    ]),
740                    constructor: Some(default_expr(ast, target, &scoped, 0)?),
741                })?;
742            }
743
744            TypeKind::Enumeration { enumerators } => {
745                if ghost_created_for(applied, node.node_id, "LuaEnum").is_none() {
746                    return Ok(());
747                }
748                let mut ordered = Vec::new();
749                let mut values = Vec::new();
750                let mut names = Vec::new();
751                let mut first = None;
752                for enumerator in enumerators {
753                    let Ok(enum_node) = ast.enumerator(*enumerator) else {
754                        continue;
755                    };
756                    if ghost_created_for(applied, enum_node.node_id, "LuaEnumerator").is_none() {
757                        continue;
758                    }
759                    let value = self
760                        .ast
761                        .effective_enumerator_value(*enumerator)
762                        .ok_or_else(|| MaterialiseError::Unevaluated {
763                            construct: "enumerator value".to_string(),
764                            at: enum_node.name.clone(),
765                        })?;
766                    let literal = integer_literal(value, &enum_node.name)?;
767                    if first.is_none() {
768                        first = Some(literal.clone());
769                    }
770                    ordered.push(Entry::positional(Expr::Table(Table::of(vec![
771                        Entry::named("name", Expr::string(&enum_node.name)),
772                        Entry::named("value", literal.clone()),
773                    ]))));
774                    values.push(Entry::named(&enum_node.name, literal.clone()));
775                    names.push(Entry {
776                        doc: Doc::none(),
777                        key: lua::Key::Expr(literal),
778                        value: Expr::string(&enum_node.name),
779                    });
780                }
781                self.emit(Decl {
782                    module: node.module,
783                    name: node.name.clone(),
784                    scoped: scoped.clone(),
785                    class: class.clone(),
786                    annotation: Doc::none(),
787                    doc: Doc::lines(&[
788                        "An enumeration, as the two maps and the ordered list an",
789                        "enumerator carries. The values are written out because @value",
790                        "admits gaps, so a position is not a value.",
791                        "",
792                        "`names` is the inverse of `values`; a Lua table holds both",
793                        "directions at once because its keys may be integers as well as",
794                        "strings.",
795                    ]),
796                    descriptor: Table::of(vec![
797                        Entry::named("idl", Expr::string(&scoped)),
798                        Entry::named("name", Expr::string(&node.name)),
799                        Entry::named("kind", Expr::string("enum")),
800                        Entry::named(
801                            "bit_bound",
802                            Expr::int(i128::from(ast.effective_bit_bound(id))),
803                        ),
804                        Entry::named("enumerators", Expr::Table(Table::of(ordered))),
805                        Entry::named("values", Expr::Table(Table::of(values))),
806                        Entry::named("names", Expr::Table(Table::of(names))),
807                    ]),
808                    // The first enumerator's value, and not zero: `@value`
809                    // admits gaps, so an enumeration need not have a
810                    // literal with the value 0 at all, and a zero default
811                    // would be a value the type does not have.
812                    constructor: first,
813                })?;
814            }
815
816            TypeKind::Bitmask { bits } => {
817                if ghost_created_for(applied, node.node_id, "LuaBitmask").is_none() {
818                    return Ok(());
819                }
820                let bound = ast.effective_bit_bound(id);
821                let mut ordered = Vec::new();
822                let mut values = Vec::new();
823                for bit in bits {
824                    let Ok(bit_node) = ast.bit_value(*bit) else {
825                        continue;
826                    };
827                    if ghost_created_for(applied, bit_node.node_id, "LuaBitEntry").is_none() {
828                        continue;
829                    }
830                    let position = ast.effective_bit_position(*bit).ok_or_else(|| {
831                        MaterialiseError::Unevaluated {
832                            construct: "bit position".to_string(),
833                            at: bit_node.name.clone(),
834                        }
835                    })?;
836                    // `1 << position` and not the number it evaluates to:
837                    // the position is what the IDL declared and the value
838                    // is derived from it, so the file says which is which.
839                    let shift = Expr::Binary {
840                        op: "<<",
841                        lhs: Box::new(Expr::int(1)),
842                        rhs: Box::new(Expr::int(i128::from(position))),
843                    };
844                    ordered.push(Entry::positional(Expr::Table(Table::of(vec![
845                        Entry::named("name", Expr::string(&bit_node.name)),
846                        Entry::named("position", Expr::int(i128::from(position))),
847                        Entry::named("value", shift.clone()),
848                    ]))));
849                    values.push(Entry::named(&bit_node.name, shift));
850                }
851                self.emit(Decl {
852                    module: node.module,
853                    name: node.name.clone(),
854                    scoped: scoped.clone(),
855                    class: class.clone(),
856                    annotation: Doc::none(),
857                    doc: Doc::lines(&[
858                        "A bitmask: one value per named bit, and the holder width the",
859                        "@bit_bound gives (Table 33 of DDS-XTypes 1.3 7.4.1.1.3.2).",
860                        "",
861                        "A Lua integer is 64-bit two's complement, so a bit_bound of 64",
862                        "puts bit 63 in the sign; the width is here so a codec masks",
863                        "rather than assumes.",
864                    ]),
865                    descriptor: Table::of(vec![
866                        Entry::named("idl", Expr::string(&scoped)),
867                        Entry::named("name", Expr::string(&node.name)),
868                        Entry::named("kind", Expr::string("bitmask")),
869                        Entry::named("bit_bound", Expr::int(i128::from(bound))),
870                        Entry::named("bits", Expr::Table(Table::of(ordered))),
871                        Entry::named("values", Expr::Table(Table::of(values))),
872                    ]),
873                    // Zero: no bit set, which is a value of every bitmask.
874                    constructor: Some(Expr::int(0)),
875                })?;
876            }
877
878            TypeKind::Bitset { .. } => {
879                if ghost_created_for(applied, node.node_id, "LuaBitset").is_none() {
880                    return Ok(());
881                }
882                let mut members = Vec::new();
883                let mut defaults = Vec::new();
884                let mut fields = Vec::new();
885                let mut pairs = Vec::new();
886                // The base's bitfields are members of the derived bitset
887                // too: an inherited bitset is one flat run.
888                for field in &ast.all_bitfields_of(id) {
889                    let Ok(field_node) = ast.bitfield(*field) else {
890                        continue;
891                    };
892                    // An anonymous bitfield is padding: it moves the next
893                    // field along and carries no value of its own.
894                    let Some(name) = field_node.name.as_ref() else {
895                        continue;
896                    };
897                    pairs.push((name.clone(), name.clone()));
898                    members.push(Entry::positional(Expr::Table(Table::of(vec![
899                        Entry::named("name", Expr::string(name)),
900                        Entry::named("key", Expr::string(name)),
901                        Entry::named("lua", Expr::string("integer")),
902                        Entry::named("kind", Expr::string("int")),
903                        Entry::named("bits", Expr::int(i128::from(field_node.bits))),
904                        Entry::named("signed", Expr::bool(false)),
905                        Entry::named("wraps", Expr::bool(false)),
906                        Entry::named("optional", Expr::bool(false)),
907                        Entry::named("is_key", Expr::bool(false)),
908                        Entry::named("id", Expr::bool(false)),
909                    ]))));
910                    defaults.push(Entry::named(name, Expr::int(0)));
911                    fields.push(cats_field(name, "integer", false));
912                }
913                no_spelling_collision(as_pairs(&pairs), LANGUAGE, &node.name)?;
914                self.emit(Decl {
915                    module: node.module,
916                    name: node.name.clone(),
917                    scoped: scoped.clone(),
918                    class: class.clone(),
919                    annotation: cats_class(&class, &fields),
920                    doc: Doc::lines(&[
921                        "A bitset. Each field states its own bit width; Lua has no",
922                        "fixed-width integer, so nothing is rounded up to a holder the",
923                        "way the C and Go columns have to.",
924                        "",
925                        "The packing into the anchor is the codec's, because the order",
926                        "on the wire is the declaration's.",
927                    ]),
928                    descriptor: Table::of(vec![
929                        Entry::named("idl", Expr::string(&scoped)),
930                        Entry::named("name", Expr::string(&node.name)),
931                        Entry::named("kind", Expr::string("bitset")),
932                        Entry::named("members", Expr::Table(Table::of(members))),
933                    ]),
934                    constructor: Some(Expr::Table(Table::of(defaults))),
935                })?;
936            }
937
938            TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
939                let is_value = matches!(node.kind, TypeKind::Valuetype { .. });
940                let created = if is_value { "LuaValue" } else { "LuaStruct" };
941                if ghost_created_for(applied, node.node_id, created).is_none() {
942                    return Ok(());
943                }
944                // XTypes 1.3 §7.5.1.4.1.3: the base is the first member `parent`; the
945                // wire still carries the base's members first (§7.4.1.1.5.1).
946                let members = ast.members_of(id).to_vec();
947                let built =
948                    self.aggregate(crate::adapter::c::struct_base(ast, id), &members, &scoped)?;
949                self.emit(Decl {
950                    module: node.module,
951                    name: node.name.clone(),
952                    scoped: scoped.clone(),
953                    class: class.clone(),
954                    annotation: cats_class(&class, &built.fields),
955                    doc: Doc::lines(&[
956                        "The member list is the array part of `members`, so the index is",
957                        "the declaration order — which is the order the wire uses",
958                        "(DDS-XTypes 1.3 7.4.1.1.5.1). A Lua table has no order of its",
959                        "own: `next` does not specify one (Lua 5.4 reference 6.1).",
960                    ]),
961                    descriptor: Table::of(vec![
962                        Entry::named("idl", Expr::string(&scoped)),
963                        Entry::named("name", Expr::string(&node.name)),
964                        Entry::named(
965                            "kind",
966                            Expr::string(if is_value { "valuetype" } else { "struct" }),
967                        ),
968                        Entry::named(
969                            "extensibility",
970                            Expr::string(ast.effective_extensibility(id).as_str()),
971                        ),
972                        Entry::named("members", Expr::Table(Table::of(built.members))),
973                    ]),
974                    constructor: Some(Expr::Table(Table::of(built.defaults))),
975                })?;
976            }
977
978            TypeKind::Union { discriminator, .. } => {
979                if ghost_created_for(applied, node.node_id, "LuaUnion").is_none() {
980                    return Ok(());
981                }
982                let Some(disc) = discriminator.resolved() else {
983                    return Err(MaterialiseError::Unevaluated {
984                        construct: "union discriminator".to_string(),
985                        at: scoped,
986                    });
987                };
988                let cases = ast.cases_of(id).to_vec();
989                let mut branches = Vec::new();
990                let mut pairs = vec![(DISCRIMINATOR.to_string(), "<discriminator>".to_string())];
991                for case in &cases {
992                    let Ok(case_node) = ast.case(*case) else {
993                        continue;
994                    };
995                    if ghost_created_for(applied, case_node.node_id, "LuaCase").is_none() {
996                        continue;
997                    }
998                    let Ok(member) = ast.member(case_node.member) else {
999                        continue;
1000                    };
1001                    pairs.push((member.name.clone(), member.name.clone()));
1002                    branches.push(case_node.member);
1003                }
1004                no_spelling_collision(as_pairs(&pairs), LANGUAGE, &node.name)?;
1005
1006                let disc_entries = {
1007                    let mut entries = vec![
1008                        Entry::named("name", Expr::string(DISCRIMINATOR)),
1009                        Entry::named("key", Expr::string(DISCRIMINATOR)),
1010                    ];
1011                    entries.append(&mut type_entries(
1012                        ast,
1013                        disc,
1014                        &format!("{scoped}._d"),
1015                        &mut self.refs,
1016                        0,
1017                    )?);
1018                    entries
1019                };
1020                let built = self.aggregate(None, &branches, &scoped)?;
1021                let mut fields = vec![cats_field(DISCRIMINATOR, &cats_type(ast, disc, 0), false)];
1022                // Every branch is optional in the value: at most one of
1023                // them is present at a time, and the discriminator may
1024                // select none of them at all.
1025                for field in &built.fields {
1026                    fields.push(optional_field(field));
1027                }
1028                self.emit(Decl {
1029                    module: node.module,
1030                    name: node.name.clone(),
1031                    scoped: scoped.clone(),
1032                    class: class.clone(),
1033                    annotation: cats_class(&class, &fields),
1034                    doc: Doc::lines(&[
1035                        "A union: the discriminator under `_d`, and one key per branch.",
1036                        "",
1037                        "Not a tag and a payload. An IDL discriminator may hold a value",
1038                        "no branch selects (IDL 4.2 7.4.1.4.4.1), and only the selected",
1039                        "branch is written (DDS-XTypes 1.3 7.4.1.1.5.2) — so `_d` is a",
1040                        "member in its own right and the branches sit beside it, at most",
1041                        "one present.",
1042                    ]),
1043                    descriptor: Table::of(vec![
1044                        Entry::named("idl", Expr::string(&scoped)),
1045                        Entry::named("name", Expr::string(&node.name)),
1046                        Entry::named("kind", Expr::string("union")),
1047                        Entry::named(
1048                            "extensibility",
1049                            Expr::string(ast.effective_extensibility(id).as_str()),
1050                        ),
1051                        Entry::named("discriminator", Expr::Table(Table::of(disc_entries))),
1052                        Entry::named("members", Expr::Table(Table::of(built.members))),
1053                    ]),
1054                    // The discriminator and nothing else. Which branch its
1055                    // value selects is the codec's reading of the labels,
1056                    // and it may select none.
1057                    constructor: Some(Expr::Table(Table::of(vec![Entry::named(
1058                        DISCRIMINATOR,
1059                        default_expr(ast, disc, &scoped, 0)?,
1060                    )]))),
1061                })?;
1062            }
1063
1064            TypeKind::Native => {
1065                if ghost_created_for(applied, node.node_id, "LuaNative").is_none() {
1066                    return Ok(());
1067                }
1068                // Opaque, never a guess at what the name might mean: the
1069                // concrete mapping comes from the facet a `#pragma` or an
1070                // annotation set, and `false` where there is none.
1071                let mapping = self
1072                    .ast
1073                    .any_of_type(id)
1074                    .ok()
1075                    .and_then(|any| ast.facets(any))
1076                    .and_then(|facets| facets.corba.as_ref())
1077                    .and_then(|corba| corba.native_mapping.clone());
1078                self.emit(Decl {
1079                    module: node.module,
1080                    name: node.name.clone(),
1081                    scoped: scoped.clone(),
1082                    class: class.clone(),
1083                    annotation: Doc::none(),
1084                    doc: Doc::lines(&[
1085                        "A native type: opaque unless a facet named its mapping.",
1086                        "",
1087                        "`mapping = false` and not nil. A table entry set to nil is an",
1088                        "entry that does not exist, so nil could not be told from a key",
1089                        "this file never wrote.",
1090                    ]),
1091                    descriptor: Table::of(vec![
1092                        Entry::named("idl", Expr::string(&scoped)),
1093                        Entry::named("name", Expr::string(&node.name)),
1094                        Entry::named("kind", Expr::string("native")),
1095                        Entry::named(
1096                            "mapping",
1097                            mapping.as_deref().map_or(Expr::bool(false), Expr::string),
1098                        ),
1099                    ]),
1100                    constructor: None,
1101                })?;
1102            }
1103
1104            // A value box is transparent here: the descriptor of what it
1105            // boxes, with `boxed = true` on it. Lua has no reference type
1106            // to declare and no pointer to write.
1107            TypeKind::ValueBox { .. } => {}
1108
1109            // An object reference belongs to the RPC arc, an annotation
1110            // type has no runtime form at all, and `any` and `ValueBase`
1111            // are decided at run time — `zerodds_ast_layout` refuses to
1112            // plan any of them. Named here rather than passed over, so
1113            // the coverage table and this file say the same thing. A
1114            // member that reaches one is refused by name in
1115            // `type_entries`.
1116            TypeKind::ObjectRef { .. }
1117            | TypeKind::AnnotationType { .. }
1118            | TypeKind::Any
1119            | TypeKind::ValueBase => {}
1120        }
1121        Ok(())
1122    }
1123
1124    /// The member list, the constructor's defaults and the `---@field`
1125    /// lines of one aggregate.
1126    fn aggregate(
1127        &mut self,
1128        base: Option<Id<TypeNode>>,
1129        members: &[Id<zerodds_ast::MemberNode>],
1130        at: &str,
1131    ) -> Result<Aggregate, MaterialiseError> {
1132        let ast = self.ast;
1133        let mut pairs = Vec::new();
1134        if base.is_some() {
1135            pairs.push(("parent".to_string(), "parent".to_string()));
1136        }
1137        for member in members {
1138            if let Ok(node) = ast.member(*member) {
1139                pairs.push((node.name.clone(), node.name.clone()));
1140            }
1141        }
1142        no_spelling_collision(as_pairs(&pairs), LANGUAGE, at)?;
1143
1144        let mut built = Aggregate::default();
1145        // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
1146        if let Some(base) = base {
1147            let site = format!("{at}.parent");
1148            let mut entries = vec![
1149                Entry::named("name", Expr::string("parent")),
1150                Entry::named("key", Expr::string("parent")),
1151            ];
1152            entries.append(&mut type_entries(ast, base, &site, &mut self.refs, 0)?);
1153            entries.push(Entry::named("optional", Expr::bool(false)));
1154            entries.push(Entry::named("is_key", Expr::bool(false)));
1155            entries.push(Entry::named("id", Expr::bool(false)));
1156            built
1157                .members
1158                .push(Entry::positional(Expr::Table(Table::of(entries))));
1159            built
1160                .fields
1161                .push(cats_field("parent", &cats_type(ast, base, 0), false));
1162            built.defaults.push(Entry {
1163                doc: Doc::none(),
1164                key: lua::table_key("parent"),
1165                value: default_expr(ast, base, &site, 0)?,
1166            });
1167        }
1168        for member in members {
1169            let Ok(node) = ast.member(*member) else {
1170                continue;
1171            };
1172            // The aggregate and the member together, because this is
1173            // what the `assert` in `M._resolved` names when a descriptor
1174            // mentions a type the file does not declare — and a bare
1175            // member name would leave the reader to find which aggregate.
1176            let site = format!("{at}.{}", node.name);
1177            let Some(target) = node.type_.resolved() else {
1178                return Err(MaterialiseError::Unevaluated {
1179                    construct: "member type".to_string(),
1180                    at: site,
1181                });
1182            };
1183            let optional = node.facets.is_optional();
1184            let mut entries = vec![
1185                Entry::named("name", Expr::string(&node.name)),
1186                Entry::named("key", Expr::string(&node.name)),
1187            ];
1188            entries.append(&mut type_entries(ast, target, &site, &mut self.refs, 0)?);
1189            entries.push(Entry::named("optional", Expr::bool(optional)));
1190            entries.push(Entry::named("is_key", Expr::bool(node.facets.is_key())));
1191            // `false` and not nil where the model has assigned no id: a
1192            // table entry set to nil does not exist, so nil could not be
1193            // told from a key this file never wrote — which is the whole
1194            // reason this descriptor is here.
1195            entries.push(Entry::named(
1196                "id",
1197                node.facets
1198                    .effective_member_id()
1199                    .map_or(Expr::bool(false), |id| Expr::int(i128::from(id))),
1200            ));
1201            built
1202                .members
1203                .push(Entry::positional(Expr::Table(Table::of(entries))));
1204            built
1205                .fields
1206                .push(cats_field(&node.name, &cats_type(ast, target, 0), optional));
1207            // An @optional member gets no key at all. In Lua the absence
1208            // of a value is the absence of the key, so writing one with a
1209            // default would make an absent member indistinguishable from
1210            // one that arrived with that value.
1211            if !optional {
1212                built.defaults.push(Entry {
1213                    doc: Doc::none(),
1214                    key: lua::table_key(&node.name),
1215                    value: default_expr(ast, target, &site, 0)?,
1216                });
1217            }
1218        }
1219        Ok(built)
1220    }
1221}
1222
1223/// What one aggregate contributes to its descriptor, its constructor and
1224/// its LuaCATS block.
1225#[derive(Default)]
1226struct Aggregate {
1227    /// The member descriptors, in declaration order.
1228    members: Vec<Entry>,
1229    /// The constructor's entries, the optional members left out.
1230    defaults: Vec<Entry>,
1231    /// The `---@field` lines.
1232    fields: Vec<String>,
1233}
1234
1235// -- constants, exceptions and interfaces ---------------------------------
1236
1237impl Builder<'_> {
1238    fn declare_constants(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
1239        let ast = self.ast;
1240        for (_, node) in ast.constants() {
1241            if ghost_created_for(applied, node.node_id, "LuaConst").is_none() {
1242                continue;
1243            }
1244            // `eval_const` and not `eval_integral`: `const string NAME` is
1245            // a constant that evaluates perfectly well and is not a
1246            // number, and the integral question is the wrong one for four
1247            // of the eight forms IDL 4.2 admits.
1248            let value = ast
1249                .eval_const(node.expr)
1250                .map_err(|_| MaterialiseError::Unevaluated {
1251                    construct: "constant's value".to_string(),
1252                    at: node.name.clone(),
1253                })?;
1254            let declared = node.type_.resolved();
1255            let spelling =
1256                declared.map_or_else(|| "long".to_string(), |target| idl_spelling(ast, target, 0));
1257            let chain = module_chain(ast, node.module)?;
1258            let path = Self::path_text(&chain, &node.name);
1259            let scoped = declared_scoped(&chain, &node.name);
1260            self.remember(&path, &scoped)?;
1261            let target = self.place_of(node.module, &node.name)?;
1262            let expr = constant_expr(
1263                &value,
1264                declared.is_some_and(|id| is_decimal_holder(ast, id)),
1265                &node.name,
1266            )?;
1267            self.declarations.push(Stat::Assign {
1268                doc: Doc::line(&format!("`const {spelling} {}` — IDL {scoped}.", node.name)),
1269                target,
1270                value: expr,
1271            });
1272        }
1273        Ok(())
1274    }
1275
1276    fn declare_exceptions(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
1277        let ast = self.ast;
1278        for (_, node) in ast.exceptions() {
1279            if ghost_created_for(applied, node.node_id, "LuaError").is_none() {
1280                continue;
1281            }
1282            let chain = module_chain(ast, node.module)?;
1283            let scoped = declared_scoped(&chain, &node.name);
1284            let class = dotted(&chain, &node.name);
1285            let built = self.aggregate(None, &node.members, &scoped)?;
1286            self.emit(Decl {
1287                module: node.module,
1288                name: node.name.clone(),
1289                scoped: scoped.clone(),
1290                class: class.clone(),
1291                annotation: cats_class(&class, &built.fields),
1292                doc: Doc::lines(&[
1293                    "An exception, as the structure its members make. Raising is the",
1294                    "RPC arc's decision; the shape is the IDL's.",
1295                ]),
1296                descriptor: Table::of(vec![
1297                    Entry::named("idl", Expr::string(&scoped)),
1298                    Entry::named("name", Expr::string(&node.name)),
1299                    Entry::named("kind", Expr::string("exception")),
1300                    Entry::named("members", Expr::Table(Table::of(built.members))),
1301                ]),
1302                constructor: Some(Expr::Table(Table::of(built.defaults))),
1303            })?;
1304        }
1305        Ok(())
1306    }
1307
1308    fn declare_interfaces(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
1309        let ast = self.ast;
1310        for (id, node) in ast.interfaces() {
1311            if ghost_created_for(applied, node.node_id, "LuaInterface").is_none() {
1312                continue;
1313            }
1314            let chain = module_chain(ast, node.module)?;
1315            let scoped = declared_scoped(&chain, &node.name);
1316            let class = dotted(&chain, &node.name);
1317            let mut operations = Vec::new();
1318            let mut signatures = Vec::new();
1319            // The whole answer and not the declared one. The descriptor
1320            // table has no link to a base's table, so a reader holding
1321            // this one sees exactly the operations listed here and OMG
1322            // IDL 4.2 §7.4.3's inherited ones have to be among them.
1323            // `interface::all_operations_flattened` walks the bases the
1324            // same `Ast::all_operations_of` did and gives the declared
1325            // operations only (`Origin::Declared`) — attributes get
1326            // their own descriptor array below, a shape of their own
1327            // and not a pair of synthesised operations, for the same
1328            // reason this column's own doc gives params/results a
1329            // table each: "an operation's parameters and result are a
1330            // shape, and a shape is exactly what this column carries".
1331            let all = interface::all_operations_flattened(ast, id)
1332                .map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
1333            for op in all
1334                .iter()
1335                .filter(|op| op.origin == interface::Origin::Declared)
1336            {
1337                signatures.push((op.name.clone(), op.name.clone()));
1338                let mut params = Vec::new();
1339                for param in &op.params {
1340                    let mut entries = vec![
1341                        Entry::named("name", Expr::string(&param.name)),
1342                        Entry::named("key", Expr::string(&param.name)),
1343                    ];
1344                    entries.append(&mut type_entries(
1345                        ast,
1346                        param.type_,
1347                        &format!("{scoped}.{}({})", op.name, param.name),
1348                        &mut self.refs,
1349                        0,
1350                    )?);
1351                    params.push(Entry::positional(Expr::Table(Table::of(entries))));
1352                }
1353                // An array of nought or one, and not a key that is
1354                // sometimes absent: a `void` operation and one whose
1355                // result key was never written would be the same table.
1356                let mut results = Vec::new();
1357                if let Some(returns) = op.returns {
1358                    results.push(Entry::positional(Expr::Table(Table::of(type_entries(
1359                        ast,
1360                        returns,
1361                        &format!("{scoped}.{}", op.name),
1362                        &mut self.refs,
1363                        0,
1364                    )?))));
1365                }
1366                operations.push(Entry::positional(Expr::Table(Table::of(vec![
1367                    Entry::named("name", Expr::string(&op.name)),
1368                    Entry::named("key", Expr::string(&op.name)),
1369                    Entry::named("params", Expr::Table(Table::of(params))),
1370                    Entry::named("results", Expr::Table(Table::of(results))),
1371                    Entry::named("raises", raise_names(&op.raises)),
1372                ]))));
1373            }
1374            let mut attributes = Vec::new();
1375            let mut attribute_signatures = Vec::new();
1376            let all_attrs = interface::all_attributes_flattened(ast, id)
1377                .map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
1378            for attribute in &all_attrs {
1379                attribute_signatures.push((attribute.name.clone(), attribute.name.clone()));
1380                let type_entries_for_attr = type_entries(
1381                    ast,
1382                    attribute.type_,
1383                    &format!("{scoped}.{}", attribute.name),
1384                    &mut self.refs,
1385                    0,
1386                )?;
1387                attributes.push(Entry::positional(Expr::Table(Table::of(vec![
1388                    Entry::named("name", Expr::string(&attribute.name)),
1389                    Entry::named("key", Expr::string(&attribute.name)),
1390                    Entry::named("type", Expr::Table(Table::of(type_entries_for_attr))),
1391                    Entry::named("readonly", Expr::bool(attribute.readonly)),
1392                    Entry::named("get_raises", raise_names(&attribute.get_raises)),
1393                    Entry::named("set_raises", raise_names(&attribute.set_raises)),
1394                ]))));
1395            }
1396            no_spelling_collision(as_pairs(&signatures), LANGUAGE, &node.name)?;
1397            no_spelling_collision(as_pairs(&attribute_signatures), LANGUAGE, &node.name)?;
1398            self.emit(Decl {
1399                module: node.module,
1400                name: node.name.clone(),
1401                scoped: scoped.clone(),
1402                class,
1403                annotation: Doc::none(),
1404                doc: Doc::lines(&[
1405                    "An interface, as the operations and attributes it has — its",
1406                    "bases' before its own, because the table has no link to a base's.",
1407                    "Emitted rather than refused: an operation's parameters and result",
1408                    "are a shape, and a shape is exactly what this column carries — an",
1409                    "attribute's type and `readonly` flag are the same kind of shape.",
1410                    "What none of it is is a call — the transport is the RPC arc's",
1411                    "business.",
1412                    "",
1413                    "`results` is an array of nought or one. A `void` operation and one",
1414                    "whose result key was never written would otherwise be the same",
1415                    "table, because a Lua key set to nil does not exist. `raises`/",
1416                    "`get_raises`/`set_raises` are arrays of exception names, empty",
1417                    "where the IDL declares none.",
1418                ]),
1419                descriptor: Table::of(vec![
1420                    Entry::named("idl", Expr::string(&scoped)),
1421                    Entry::named("name", Expr::string(&node.name)),
1422                    Entry::named("kind", Expr::string("interface")),
1423                    Entry::named("operations", Expr::Table(Table::of(operations))),
1424                    Entry::named("attributes", Expr::Table(Table::of(attributes))),
1425                ]),
1426                constructor: None,
1427            })?;
1428        }
1429        Ok(())
1430    }
1431}
1432
1433// -- describing a type ----------------------------------------------------
1434
1435/// The entries describing a value of `id`.
1436///
1437/// Every IDL fact Lua cannot enforce lands here: the width, the
1438/// signedness, the bound, the digit count, the dimensions and the name of
1439/// A `raises`/`getraises`/`setraises` clause, as the array of exception
1440/// names its descriptor table carries — empty where the IDL declared
1441/// none, which is a table entry a reader indexes rather than a doc
1442/// comment it has to parse.
1443fn raise_names(raises: &[interface::Raise]) -> Expr {
1444    Expr::Table(Table::of(
1445        raises
1446            .iter()
1447            .map(|raise| Entry::positional(Expr::string(&raise.name)))
1448            .collect(),
1449    ))
1450}
1451
1452/// whatever named type is on the other end.
1453///
1454/// zerodds-lint: recursion-depth 64 (type nesting; bounded by `MAX_TYPE_DEPTH`)
1455#[allow(clippy::too_many_lines)]
1456fn type_entries(
1457    ast: &Ast,
1458    id: Id<TypeNode>,
1459    at: &str,
1460    refs: &mut Vec<(String, String)>,
1461    depth: usize,
1462) -> Result<Vec<Entry>, MaterialiseError> {
1463    if depth > MAX_TYPE_DEPTH {
1464        return Err(MaterialiseError::Unevaluated {
1465            construct: format!("type, whose descriptor nests past {MAX_TYPE_DEPTH}"),
1466            at: at.to_string(),
1467        });
1468    }
1469    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1470        construct: "type".to_string(),
1471        at: at.to_string(),
1472    })?;
1473    let refused = match &node.kind {
1474        TypeKind::Any => Some("any"),
1475        TypeKind::ValueBase => Some("ValueBase"),
1476        TypeKind::ObjectRef { .. } => Some("Object"),
1477        TypeKind::AnnotationType { .. } => Some("annotation type"),
1478        _ => None,
1479    };
1480    if let Some(construct) = refused {
1481        return Err(MaterialiseError::NoLanguageForm {
1482            construct: construct.to_string(),
1483            language: LANGUAGE,
1484            at: at.to_string(),
1485        });
1486    }
1487    Ok(match &node.kind {
1488        TypeKind::Primitive(kind) => primitive_entries(*kind),
1489        TypeKind::String { width, bound } => vec![
1490            Entry::named("lua", Expr::string("string")),
1491            Entry::named("kind", Expr::string("string")),
1492            Entry::named("bits", Expr::int(char_bits(*width))),
1493            Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
1494        ],
1495        TypeKind::Fixed { digits, scale } => vec![
1496            Entry::named("lua", Expr::string("string")),
1497            Entry::named("kind", Expr::string("decimal")),
1498            Entry::named("digits", Expr::int(i128::from(*digits))),
1499            Entry::named("scale", Expr::int(i128::from(*scale))),
1500        ],
1501        TypeKind::Sequence { element, bound } => {
1502            let Some(element) = element.resolved() else {
1503                return Err(MaterialiseError::Unevaluated {
1504                    construct: "sequence element".to_string(),
1505                    at: at.to_string(),
1506                });
1507            };
1508            vec![
1509                Entry::named("lua", Expr::string("table")),
1510                Entry::named("kind", Expr::string("sequence")),
1511                // A Lua array starts at 1 and an IDL index at 0. Written
1512                // out rather than assumed: element i is t[i + 1].
1513                Entry::named("base", Expr::int(1)),
1514                Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
1515                Entry::named(
1516                    "element",
1517                    Expr::Table(Table::of(type_entries(ast, element, at, refs, depth + 1)?)),
1518                ),
1519            ]
1520        }
1521        TypeKind::Array { element, dims } => {
1522            let Some(element) = element.resolved() else {
1523                return Err(MaterialiseError::Unevaluated {
1524                    construct: "array element".to_string(),
1525                    at: at.to_string(),
1526                });
1527            };
1528            let mut lengths = Vec::new();
1529            for dim in dims {
1530                let length =
1531                    ast.eval_integral(*dim)
1532                        .map_err(|_| MaterialiseError::Unevaluated {
1533                            construct: "array length".to_string(),
1534                            at: at.to_string(),
1535                        })?;
1536                lengths.push(Entry::positional(Expr::int(length)));
1537            }
1538            vec![
1539                Entry::named("lua", Expr::string("table")),
1540                Entry::named("kind", Expr::string("array")),
1541                Entry::named("base", Expr::int(1)),
1542                // Outermost dimension first, as the model holds them:
1543                // `long grid[2][3]` is two rows of three, and reversing
1544                // them silently transposes the data.
1545                Entry::named("dims", Expr::Table(Table::of(lengths))),
1546                Entry::named(
1547                    "element",
1548                    Expr::Table(Table::of(type_entries(ast, element, at, refs, depth + 1)?)),
1549                ),
1550            ]
1551        }
1552        TypeKind::Map { key, value, bound } => {
1553            let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
1554                return Err(MaterialiseError::Unevaluated {
1555                    construct: "map key or value".to_string(),
1556                    at: at.to_string(),
1557                });
1558            };
1559            vec![
1560                Entry::named("lua", Expr::string("table")),
1561                Entry::named("kind", Expr::string("map")),
1562                Entry::named("base", Expr::int(1)),
1563                Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
1564                Entry::named(
1565                    "key_type",
1566                    Expr::Table(Table::of(type_entries(ast, key, at, refs, depth + 1)?)),
1567                ),
1568                Entry::named(
1569                    "value_type",
1570                    Expr::Table(Table::of(type_entries(ast, value, at, refs, depth + 1)?)),
1571                ),
1572            ]
1573        }
1574        // A typedef is transparent: the use takes the target's
1575        // descriptor, with the alias's own name kept beside it so the
1576        // declaration is not lost.
1577        TypeKind::Alias { aliased } => {
1578            let Some(target) = aliased.resolved() else {
1579                return Err(MaterialiseError::Unevaluated {
1580                    construct: "typedef target".to_string(),
1581                    at: at.to_string(),
1582                });
1583            };
1584            let mut entries = type_entries(ast, target, at, refs, depth + 1)?;
1585            if !node.name.is_empty() {
1586                entries.push(Entry::named("alias", Expr::string(&scoped_name(ast, id))));
1587            }
1588            entries
1589        }
1590        // A boxed value is the value, or its absence. Lua has no
1591        // reference type to name, so the descriptor is the target's with
1592        // `boxed = true` on it.
1593        TypeKind::ValueBox { boxed } => {
1594            let Some(target) = boxed.resolved() else {
1595                return Err(MaterialiseError::Unevaluated {
1596                    construct: "value box target".to_string(),
1597                    at: at.to_string(),
1598                });
1599            };
1600            let mut entries = type_entries(ast, target, at, refs, depth + 1)?;
1601            entries.push(Entry::named("boxed", Expr::bool(true)));
1602            entries
1603        }
1604        TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => {
1605            let scoped = scoped_name(ast, id);
1606            note(refs, &scoped, at);
1607            vec![
1608                Entry::named("lua", Expr::string("integer")),
1609                Entry::named(
1610                    "kind",
1611                    Expr::string(if matches!(node.kind, TypeKind::Bitmask { .. }) {
1612                        "bitmask"
1613                    } else {
1614                        "enum"
1615                    }),
1616                ),
1617                Entry::named("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
1618                Entry::named("ref", Expr::string(&scoped)),
1619            ]
1620        }
1621        TypeKind::Structure { .. }
1622        | TypeKind::Union { .. }
1623        | TypeKind::Bitset { .. }
1624        | TypeKind::Valuetype { .. } => {
1625            let scoped = scoped_name(ast, id);
1626            note(refs, &scoped, at);
1627            vec![
1628                Entry::named("lua", Expr::string("table")),
1629                Entry::named("kind", Expr::string(aggregate_kind(&node.kind))),
1630                Entry::named("ref", Expr::string(&scoped)),
1631            ]
1632        }
1633        TypeKind::Native => {
1634            let scoped = scoped_name(ast, id);
1635            note(refs, &scoped, at);
1636            vec![
1637                Entry::named("lua", Expr::string("any")),
1638                Entry::named("kind", Expr::string("native")),
1639                Entry::named("ref", Expr::string(&scoped)),
1640            ]
1641        }
1642        // Refused above; the arm keeps the match total, so a kind added
1643        // to the model without a Lua answer is a compile error and not a
1644        // member that silently takes another's descriptor.
1645        TypeKind::Any
1646        | TypeKind::ValueBase
1647        | TypeKind::ObjectRef { .. }
1648        | TypeKind::AnnotationType { .. } => {
1649            return Err(MaterialiseError::NoLanguageForm {
1650                construct: node.kind.as_str().to_string(),
1651                language: LANGUAGE,
1652                at: at.to_string(),
1653            });
1654        }
1655    })
1656}
1657
1658/// Record a scoped name a descriptor mentioned, once.
1659fn note(refs: &mut Vec<(String, String)>, name: &str, at: &str) {
1660    if refs.iter().any(|(seen, _)| seen == name) {
1661        return;
1662    }
1663    refs.push((name.to_string(), at.to_string()));
1664}
1665
1666/// Which of the four aggregate kinds a type is.
1667const fn aggregate_kind(kind: &TypeKind) -> &'static str {
1668    match kind {
1669        TypeKind::Union { .. } => "union",
1670        TypeKind::Bitset { .. } => "bitset",
1671        TypeKind::Valuetype { .. } => "valuetype",
1672        _ => "struct",
1673    }
1674}
1675
1676/// The entries describing one IDL primitive.
1677///
1678/// Exhaustive, so a primitive added to the model without a Lua answer is
1679/// a compile error and not a member that silently takes another's.
1680///
1681/// `char` and `wchar` are integers and not one-character strings: a
1682/// `wchar` is not one octet, so a string holding one would have to choose
1683/// an encoding, and choosing one here would put the DDS-XTypes 1.3
1684/// §7.4.1.1.2 question in two places. `bits` is what tells the two apart.
1685///
1686/// `long double` is a string holding the decimal; the module
1687/// documentation carries why.
1688fn primitive_entries(kind: PrimitiveKind) -> Vec<Entry> {
1689    let integer = |bits: i128, signed: bool, wraps: bool| {
1690        vec![
1691            Entry::named("lua", Expr::string("integer")),
1692            Entry::named("kind", Expr::string("int")),
1693            Entry::named("bits", Expr::int(bits)),
1694            Entry::named("signed", Expr::bool(signed)),
1695            Entry::named("wraps", Expr::bool(wraps)),
1696        ]
1697    };
1698    let character = |bits: i128| {
1699        vec![
1700            Entry::named("lua", Expr::string("integer")),
1701            Entry::named("kind", Expr::string("char")),
1702            Entry::named("bits", Expr::int(bits)),
1703        ]
1704    };
1705    match kind {
1706        PrimitiveKind::Boolean => vec![
1707            Entry::named("lua", Expr::string("boolean")),
1708            Entry::named("kind", Expr::string("boolean")),
1709            Entry::named("bits", Expr::int(8)),
1710        ],
1711        PrimitiveKind::Char => character(8),
1712        PrimitiveKind::WChar => character(16),
1713        PrimitiveKind::Octet | PrimitiveKind::UInt8 => integer(8, false, false),
1714        PrimitiveKind::Int8 => integer(8, true, false),
1715        PrimitiveKind::Int16 => integer(16, true, false),
1716        PrimitiveKind::UInt16 => integer(16, false, false),
1717        PrimitiveKind::Int32 => integer(32, true, false),
1718        PrimitiveKind::UInt32 => integer(32, false, false),
1719        PrimitiveKind::Int64 => integer(64, true, false),
1720        // The one integer that does not fit. A Lua integer is 64-bit
1721        // two's complement, so a value at or above 2^63 reads back
1722        // negative — `wraps` is where that is written down, and a range
1723        // check is the codec's to make.
1724        PrimitiveKind::UInt64 => integer(64, false, true),
1725        PrimitiveKind::Float32 => vec![
1726            Entry::named("lua", Expr::string("number")),
1727            Entry::named("kind", Expr::string("float")),
1728            Entry::named("bits", Expr::int(32)),
1729        ],
1730        PrimitiveKind::Float64 => vec![
1731            Entry::named("lua", Expr::string("number")),
1732            Entry::named("kind", Expr::string("float")),
1733            Entry::named("bits", Expr::int(64)),
1734        ],
1735        // A Lua float is a C double: 53 bits of significand against
1736        // binary128's 113. The member holds the decimal text instead.
1737        PrimitiveKind::Float128 => vec![
1738            Entry::named("lua", Expr::string("string")),
1739            Entry::named("kind", Expr::string("float")),
1740            Entry::named("bits", Expr::int(128)),
1741        ],
1742    }
1743}
1744
1745/// How wide one character of `width` is on the wire.
1746const fn char_bits(width: CharWidth) -> i128 {
1747    match width {
1748        CharWidth::Char => 8,
1749        CharWidth::WChar => 16,
1750    }
1751}
1752
1753/// A bound as a number, with `0` meaning unbounded.
1754///
1755/// # Errors
1756/// [`MaterialiseError::Unevaluated`] when the bounding expression does
1757/// not evaluate. Zero is what unbounded is written as, so a bound that
1758/// failed to evaluate must not fall back to it: the two would be
1759/// indistinguishable in the output.
1760fn bound_of(ast: &Ast, bound: Bound, at: &str) -> Result<i128, MaterialiseError> {
1761    match bound.expr() {
1762        None => Ok(0),
1763        Some(expr) => ast
1764            .eval_integral(expr)
1765            .map_err(|_| MaterialiseError::Unevaluated {
1766                construct: "bound".to_string(),
1767                at: at.to_string(),
1768            }),
1769    }
1770}
1771
1772// -- default values -------------------------------------------------------
1773
1774/// The value a member of type `id` takes in a constructor.
1775///
1776/// zerodds-lint: recursion-depth 64 (type nesting; bounded by `MAX_TYPE_DEPTH`)
1777#[allow(clippy::too_many_lines)]
1778fn default_expr(
1779    ast: &Ast,
1780    id: Id<TypeNode>,
1781    at: &str,
1782    depth: usize,
1783) -> Result<Expr, MaterialiseError> {
1784    if depth > MAX_TYPE_DEPTH {
1785        return Err(MaterialiseError::Unevaluated {
1786            construct: format!("type, whose default nests past {MAX_TYPE_DEPTH}"),
1787            at: at.to_string(),
1788        });
1789    }
1790    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1791        construct: "type".to_string(),
1792        at: at.to_string(),
1793    })?;
1794    Ok(match &node.kind {
1795        TypeKind::Primitive(PrimitiveKind::Boolean) => Expr::bool(false),
1796        TypeKind::Primitive(PrimitiveKind::Float32 | PrimitiveKind::Float64) => {
1797            Expr::Literal(Literal::Float(lua::float_text("0")))
1798        }
1799        // The decimal text, because the holder is a string.
1800        TypeKind::Primitive(PrimitiveKind::Float128) => Expr::string("0"),
1801        TypeKind::Primitive(_) => Expr::int(0),
1802        TypeKind::String { .. } => Expr::string(""),
1803        TypeKind::Fixed { scale, .. } => Expr::string(&zero_decimal(*scale)),
1804        // An empty table: a sequence and a map both start with nothing in
1805        // them, and a Lua table used as an array starts at index 1.
1806        TypeKind::Sequence { .. } | TypeKind::Map { .. } => Expr::Table(Table::default()),
1807        TypeKind::Array { element, dims } => {
1808            let Some(element) = element.resolved() else {
1809                return Err(MaterialiseError::Unevaluated {
1810                    construct: "array element".to_string(),
1811                    at: at.to_string(),
1812                });
1813            };
1814            let mut lengths = Vec::new();
1815            for dim in dims {
1816                lengths.push(ast.eval_integral(*dim).map_err(|_| {
1817                    MaterialiseError::Unevaluated {
1818                        construct: "array length".to_string(),
1819                        at: at.to_string(),
1820                    }
1821                })?);
1822            }
1823            // Built from the innermost dimension out, so `long grid[2][3]`
1824            // is two tables of three and not three of two. The whole
1825            // array is written out: an IDL array has its length in its
1826            // type, and a constructor returning an empty table would
1827            // return a value of a different shape from the one declared.
1828            let mut value = default_expr(ast, element, at, depth + 1)?;
1829            for length in lengths.iter().rev() {
1830                let count =
1831                    usize::try_from(*length).map_err(|_| MaterialiseError::NoLanguageForm {
1832                        construct: format!("an array of {length} elements"),
1833                        language: LANGUAGE,
1834                        at: at.to_string(),
1835                    })?;
1836                value = Expr::Table(Table::of(
1837                    vec![value; count]
1838                        .into_iter()
1839                        .map(Entry::positional)
1840                        .collect(),
1841                ));
1842            }
1843            value
1844        }
1845        TypeKind::Alias { aliased } => {
1846            let Some(target) = aliased.resolved() else {
1847                return Err(MaterialiseError::Unevaluated {
1848                    construct: "typedef target".to_string(),
1849                    at: at.to_string(),
1850                });
1851            };
1852            default_expr(ast, target, at, depth + 1)?
1853        }
1854        TypeKind::ValueBox { boxed } => {
1855            let Some(target) = boxed.resolved() else {
1856                return Err(MaterialiseError::Unevaluated {
1857                    construct: "value box target".to_string(),
1858                    at: at.to_string(),
1859                });
1860            };
1861            default_expr(ast, target, at, depth + 1)?
1862        }
1863        // The first enumerator's value. `@value` admits gaps, so an
1864        // enumeration need not have a literal with the value 0 at all,
1865        // and a zero default would be a value the type does not have.
1866        TypeKind::Enumeration { enumerators } => {
1867            let first = enumerators
1868                .first()
1869                .and_then(|first| ast.effective_enumerator_value(*first));
1870            match first {
1871                Some(value) => integer_literal(value, at)?,
1872                None => Expr::int(0),
1873            }
1874        }
1875        // No bit set, which is a value of every bitmask.
1876        TypeKind::Bitmask { .. } => Expr::int(0),
1877        TypeKind::Structure { .. }
1878        | TypeKind::Union { .. }
1879        | TypeKind::Bitset { .. }
1880        | TypeKind::Valuetype { .. } => Expr::Call {
1881            func: Box::new(constructor_place(ast, id)?),
1882            args: Vec::new(),
1883        },
1884        // A native type is opaque and this column cannot build a value of
1885        // it. `false` and not nil: a table entry set to nil is an entry
1886        // that does not exist, so the member would vanish from the value
1887        // while the descriptor still listed it.
1888        TypeKind::Native => Expr::bool(false),
1889        TypeKind::Any
1890        | TypeKind::ValueBase
1891        | TypeKind::ObjectRef { .. }
1892        | TypeKind::AnnotationType { .. } => {
1893            return Err(MaterialiseError::NoLanguageForm {
1894                construct: node.kind.as_str().to_string(),
1895                language: LANGUAGE,
1896                at: at.to_string(),
1897            });
1898        }
1899    })
1900}
1901
1902/// `M.a.b.T.new`, the constructor of a declared aggregate.
1903///
1904/// # Errors
1905/// [`MaterialiseError::NoModule`] when the model does not answer for a
1906/// module the type nests in.
1907fn constructor_place(ast: &Ast, id: Id<TypeNode>) -> Result<Place, MaterialiseError> {
1908    Ok(declared_place(ast, id)?.field("new"))
1909}
1910
1911/// `M.a.b.T` — the place a declared type's descriptor is bound to.
1912///
1913/// Public because the codec backend and the Table 9 defaulters reach a
1914/// declared enumeration's `values` table and a bitmask's through it:
1915/// one derivation of the path, so a codec cannot part from the
1916/// materialiser over a spelling.
1917///
1918/// # Errors
1919/// [`MaterialiseError::NoModule`] when the model does not answer for a
1920/// module the type nests in.
1921pub fn declared_place(ast: &Ast, id: Id<TypeNode>) -> Result<Place, MaterialiseError> {
1922    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1923        construct: "type".to_string(),
1924        at: "a member's declared type".to_string(),
1925    })?;
1926    let mut place = Place::name(MODULE);
1927    for segment in module_chain(ast, node.module)? {
1928        place = place.field(&segment);
1929    }
1930    Ok(place.field(&node.name))
1931}
1932
1933// -- constants ------------------------------------------------------------
1934
1935/// Whether a constant's declared type is held as decimal text.
1936///
1937/// A `long double` and a `fixed` both are, and neither has a Lua literal
1938/// that carries the value: see [`constant_expr`].
1939fn is_decimal_holder(ast: &Ast, id: Id<TypeNode>) -> bool {
1940    // A counted loop and not a call to itself on the alias target: a
1941    // `typedef` cycle is a finding the model's invariant pass reports
1942    // rather than a shape it rejects, and the recursive form read such
1943    // a chain with nothing counting it.
1944    let mut current = id;
1945    for _ in 0..MAX_TYPE_DEPTH {
1946        match ast.type_(current).map(|node| node.kind.clone()) {
1947            Ok(TypeKind::Primitive(PrimitiveKind::Float128) | TypeKind::Fixed { .. }) => {
1948                return true;
1949            }
1950            Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
1951                Some(target) => current = target,
1952                None => return false,
1953            },
1954            _ => return false,
1955        }
1956    }
1957    false
1958}
1959
1960/// The Lua expression a constant's value takes.
1961///
1962/// `long double` is refused by name. Its Lua holder is a string carrying
1963/// the decimal, and a Lua float is a C `double`: writing the literal
1964/// would put a different number in the file than the IDL wrote.
1965fn constant_expr(
1966    value: &ConstValue,
1967    decimal_holder: bool,
1968    at: &str,
1969) -> Result<Expr, MaterialiseError> {
1970    let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
1971        construct: construct.to_string(),
1972        language: LANGUAGE,
1973        at: at.to_string(),
1974    };
1975    Ok(match value {
1976        ConstValue::Boolean(flag) => Expr::bool(*flag),
1977        ConstValue::Integer(number) => integer_literal(*number, at)?,
1978        // The code point as a number: a `char` is one octet here and a
1979        // `wchar` two, and both are held as integers.
1980        ConstValue::Char(character) => Expr::int(i128::from(u32::from(*character))),
1981        ConstValue::WChar(point) => Expr::int(i128::from(*point)),
1982        ConstValue::Float { mantissa, exponent } => {
1983            if decimal_holder {
1984                return Err(refuse("a `long double` constant, which has no Lua literal"));
1985            }
1986            // Written so that `math.type` answers `float`: Lua 5.4
1987            // reference §3.1 makes a numeral without a radix point or an
1988            // exponent an integer.
1989            Expr::Literal(Literal::Float(lua::float_text(&constant::float_text(
1990                mantissa,
1991                exponent.as_deref(),
1992            ))))
1993        }
1994        ConstValue::Fixed { digits, scale } => {
1995            Expr::string(&constant::fixed_decimal(digits, *scale))
1996        }
1997        ConstValue::String(text) => Expr::string(text),
1998        ConstValue::WString(points) => Expr::string(
1999            &constant::wstring_utf8(points)
2000                .ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
2001        ),
2002        // `ConstValue` is `#[non_exhaustive]`, so a form added to the
2003        // model reaches here. Refused by name rather than given a
2004        // plausible default: a constant emitted as the wrong literal is
2005        // a file that disagrees with the IDL and loads.
2006        _ => return Err(refuse("a constant form this binding has no literal for")),
2007    })
2008}
2009
2010/// The literal a whole number takes.
2011///
2012/// Decimal where it fits in a Lua integer, and **hexadecimal** where it
2013/// does not. Lua 5.4 reference §3.1: a decimal numeral whose value does
2014/// not fit in an integer denotes a *float*, and a hexadecimal one "wraps
2015/// around" to fit. Measured with the interpreter on the path:
2016/// `math.type(18446744073709551615)` is `float`, and
2017/// `math.type(0xFFFFFFFFFFFFFFFF)` is `integer`, with the value `-1`.
2018///
2019/// So `const unsigned long long BIG = 18446744073709551615;` is written
2020/// `0xFFFFFFFFFFFFFFFF`, which is the exact eight octets CDR writes.
2021///
2022/// # Errors
2023/// [`MaterialiseError::NoLanguageForm`] for a value outside 64 bits in
2024/// either direction. A Lua integer cannot hold it and a float would put
2025/// a different number in the file.
2026fn integer_literal(value: i128, at: &str) -> Result<Expr, MaterialiseError> {
2027    // −2^63 itself is hexadecimal too: `-9223372036854775808` is unary
2028    // minus over a decimal numeral, and that numeral is already a
2029    // *float* by the time the minus applies (reference §3.1). Measured:
2030    // `math.type(-9223372036854775808)` prints `float`, and
2031    // `0x8000000000000000` is the integer.
2032    if value == i128::from(i64::MIN) {
2033        return Ok(Expr::Literal(Literal::Int(
2034            "0x8000000000000000".to_string(),
2035        )));
2036    }
2037    if i64::try_from(value).is_ok() {
2038        return Ok(Expr::int(value));
2039    }
2040    if let Ok(unsigned) = u64::try_from(value) {
2041        return Ok(Expr::Literal(Literal::Int(format!("0x{unsigned:016X}"))));
2042    }
2043    Err(MaterialiseError::NoLanguageForm {
2044        construct: format!("the integer {value}, which is outside a 64-bit Lua integer"),
2045        language: LANGUAGE,
2046        at: at.to_string(),
2047    })
2048}
2049
2050// -- names ----------------------------------------------------------------
2051
2052/// A declared type's IDL scoped name: `::common::util::Location`.
2053///
2054/// The one name that survives every mapping in this file, which is why
2055/// it is the key of `M._descriptors` and the value of every `ref`.
2056#[must_use]
2057pub fn scoped_name(ast: &Ast, id: Id<TypeNode>) -> String {
2058    let Ok(any) = ast.any_of_type(id) else {
2059        return "::<unnamed>".to_string();
2060    };
2061    match ast.scoped_name_of(any) {
2062        Some(scoped) => format!("{scoped}"),
2063        None => "::<unnamed>".to_string(),
2064    }
2065}
2066
2067/// The scoped name of a declaration outside the type arena.
2068fn declared_scoped(chain: &[String], name: &str) -> String {
2069    let mut out = String::from("::");
2070    for segment in chain {
2071        out.push_str(segment);
2072        out.push_str("::");
2073    }
2074    out.push_str(name);
2075    out
2076}
2077
2078/// A dotted name, for a LuaCATS class.
2079fn dotted(chain: &[String], name: &str) -> String {
2080    let mut segments: Vec<&str> = chain.iter().map(String::as_str).collect();
2081    segments.push(name);
2082    segments.join(".")
2083}
2084
2085/// The LuaCATS class name of a declared type.
2086fn class_name(ast: &Ast, id: Id<TypeNode>) -> String {
2087    let Ok(any) = ast.any_of_type(id) else {
2088        return "unknown".to_string();
2089    };
2090    match ast.scoped_name_of(any) {
2091        Some(scoped) => scoped.segments().join("."),
2092        None => "unknown".to_string(),
2093    }
2094}
2095
2096/// The IDL spelling of a type, for the comment above a constant.
2097///
2098/// zerodds-lint: recursion-depth 64 (type nesting; bounded by `MAX_TYPE_DEPTH`)
2099fn idl_spelling(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
2100    if depth > MAX_TYPE_DEPTH {
2101        return "…".to_string();
2102    }
2103    match ast
2104        .type_(id)
2105        .map(|node| (node.name.clone(), node.kind.clone()))
2106    {
2107        Ok((_, TypeKind::Primitive(kind))) => kind.as_str().to_string(),
2108        Ok((
2109            _,
2110            TypeKind::String {
2111                width: CharWidth::Char,
2112                ..
2113            },
2114        )) => "string".to_string(),
2115        Ok((
2116            _,
2117            TypeKind::String {
2118                width: CharWidth::WChar,
2119                ..
2120            },
2121        )) => "wstring".to_string(),
2122        Ok((_, TypeKind::Fixed { digits, scale })) => format!("fixed<{digits},{scale}>"),
2123        Ok((_, TypeKind::Sequence { element, .. })) => match element.resolved() {
2124            Some(element) => format!("sequence<{}>", idl_spelling(ast, element, depth + 1)),
2125            None => "sequence<?>".to_string(),
2126        },
2127        Ok((name, _)) if !name.is_empty() => scoped_name(ast, id),
2128        _ => "?".to_string(),
2129    }
2130}
2131
2132/// The modules a declaration nests in, outermost first.
2133///
2134/// The root module is the anonymous global scope and is the chunk
2135/// itself, so it produces no table.
2136///
2137/// # Errors
2138/// [`MaterialiseError::NoModule`] when the model does not answer for a
2139/// module, or the parent chain runs past [`MAX_MODULE_DEPTH`].
2140fn module_chain(ast: &Ast, module: Id<ModuleNode>) -> Result<Vec<String>, MaterialiseError> {
2141    let mut out = Vec::new();
2142    let mut current = Some(module);
2143    let mut depth = 0;
2144    while let Some(id) = current {
2145        if depth > MAX_MODULE_DEPTH {
2146            return Err(MaterialiseError::NoModule {
2147                at: format!(
2148                    "#{}@{} — the parent chain runs past {MAX_MODULE_DEPTH}",
2149                    module.slot(),
2150                    module.generation()
2151                ),
2152            });
2153        }
2154        let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
2155            at: format!("#{}@{}", id.slot(), id.generation()),
2156        })?;
2157        // The root has no name and no parent, and produces no table.
2158        if node.parent.is_none() {
2159            break;
2160        }
2161        out.push(node.name.clone());
2162        current = node.parent;
2163        depth += 1;
2164    }
2165    out.reverse();
2166    Ok(out)
2167}
2168
2169// -- the LuaCATS block ----------------------------------------------------
2170
2171/// One `---@field` line, or a prose line where the name cannot be one.
2172///
2173/// LuaCATS writes a field as `---@field name type`, and a name that is
2174/// not a Lua identifier — a keyword, in practice — has no place in that
2175/// grammar. It is written as prose instead, naming the key and the type,
2176/// rather than guessed at: the key is unchanged either way, and this
2177/// block is read by an editor and by nothing else.
2178fn cats_field(name: &str, type_: &str, optional: bool) -> String {
2179    if lua::is_identifier(name) {
2180        let mark = if optional { "?" } else { "" };
2181        return format!("@field {name}{mark} {type_}");
2182    }
2183    let absence = if optional {
2184        ", absent when not set"
2185    } else {
2186        ""
2187    };
2188    format!("the member `{name}` is at [\"{name}\"] and holds {type_}{absence}")
2189}
2190
2191/// The `---@class` block of one type.
2192fn cats_class(class: &str, fields: &[String]) -> Doc {
2193    let mut doc = Doc::line(&format!("@class {class}"));
2194    for field in fields {
2195        doc.push(field);
2196    }
2197    doc
2198}
2199
2200/// The same `---@field` line, with the member marked optional.
2201fn optional_field(field: &str) -> String {
2202    match field.strip_prefix("@field ") {
2203        Some(rest) => match rest.split_once(' ') {
2204            Some((name, type_)) if !name.ends_with('?') => {
2205                format!("@field {name}? {type_}")
2206            }
2207            _ => field.to_string(),
2208        },
2209        None => field.to_string(),
2210    }
2211}
2212
2213/// The LuaCATS type a value of `id` is annotated with.
2214///
2215/// A comment and nothing else: `lua-language-server` reads it and the
2216/// Lua runtime does not.
2217///
2218/// zerodds-lint: recursion-depth 64 (type nesting; bounded by `MAX_TYPE_DEPTH`)
2219fn cats_type(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
2220    if depth > MAX_TYPE_DEPTH {
2221        return "any".to_string();
2222    }
2223    let Ok(node) = ast.type_(id) else {
2224        return "any".to_string();
2225    };
2226    match &node.kind {
2227        TypeKind::Primitive(PrimitiveKind::Boolean) => "boolean".to_string(),
2228        TypeKind::Primitive(PrimitiveKind::Float32 | PrimitiveKind::Float64) => {
2229            "number".to_string()
2230        }
2231        TypeKind::Primitive(PrimitiveKind::Float128) | TypeKind::String { .. } => {
2232            "string".to_string()
2233        }
2234        TypeKind::Primitive(_) => "integer".to_string(),
2235        TypeKind::Fixed { .. } => "string".to_string(),
2236        TypeKind::Sequence { element, .. } => match element.resolved() {
2237            Some(element) => format!("{}[]", cats_type(ast, element, depth + 1)),
2238            None => "any[]".to_string(),
2239        },
2240        TypeKind::Array { element, dims } => {
2241            let mut out = match element.resolved() {
2242                Some(element) => cats_type(ast, element, depth + 1),
2243                None => "any".to_string(),
2244            };
2245            for _ in dims {
2246                out.push_str("[]");
2247            }
2248            out
2249        }
2250        // A map is an array of pair tables. LuaCATS has no shape for
2251        // that which is worth asserting here, so `table[]` says what it
2252        // is and the descriptor says what is in it.
2253        TypeKind::Map { .. } => "table[]".to_string(),
2254        TypeKind::Alias { aliased } => match aliased.resolved() {
2255            Some(target) => cats_type(ast, target, depth + 1),
2256            None => "any".to_string(),
2257        },
2258        TypeKind::ValueBox { boxed } => match boxed.resolved() {
2259            Some(target) => format!("{}?", cats_type(ast, target, depth + 1)),
2260            None => "any?".to_string(),
2261        },
2262        TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => "integer".to_string(),
2263        TypeKind::Structure { .. }
2264        | TypeKind::Union { .. }
2265        | TypeKind::Bitset { .. }
2266        | TypeKind::Valuetype { .. } => class_name(ast, id),
2267        TypeKind::Native
2268        | TypeKind::Any
2269        | TypeKind::ValueBase
2270        | TypeKind::ObjectRef { .. }
2271        | TypeKind::AnnotationType { .. } => "any".to_string(),
2272    }
2273}
2274
2275// -- the graph ------------------------------------------------------------
2276
2277/// Borrow a list of owned name pairs for [`no_spelling_collision`].
2278fn as_pairs(pairs: &[(String, String)]) -> Vec<(&str, &str)> {
2279    pairs
2280        .iter()
2281        .map(|(spelled, declared)| (spelled.as_str(), declared.as_str()))
2282        .collect()
2283}
2284
2285/// Whether the rules created a node of `type_name` for `node`.
2286///
2287/// The same two-hop walk every sibling materialiser does: a
2288/// correspondence is an ordinary node with a link to the anchor and a
2289/// link to what was created.
2290fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
2291    let anchor = applied
2292        .origin
2293        .iter()
2294        .find(|(_, model)| **model == node)
2295        .map(|(ghost, _)| *ghost)?;
2296    let typ = applied.graph.types.lookup(type_name)?;
2297    applied
2298        .graph
2299        .nodes_of_type(typ)
2300        .map(|created| created.id)
2301        .find(|created| {
2302            applied.graph.connected(&anchor, created)
2303                || applied
2304                    .graph
2305                    .parts(&anchor)
2306                    .any(|part| applied.graph.connected(&part.other, created))
2307        })
2308}