Skip to main content

Module lua

Module lua 

Source
Expand description

The Lua CST as the other end of a rule. Spec §3.2.

The counterpart of crate::adapter::python, which asks the same question, and of crate::adapter::go, which does not have to.

Values are resolved from the model, never from the overlay — the same rule the sibling materialisers follow and for the same reason (spec §2.3).

§Lua is the hardest of the ten, and it is worth saying why plainly

Python at least has a place to write an annotation. Lua has none. There is no type syntax, no declaration, no struct, no constant and no annotation the language itself reads. A struct is a table. So is a sequence, a map, an array and a union. math.type distinguishes an integer from a float and that is the entire type information a Lua program can obtain about a value it was handed.

A column that emitted only constructors would therefore carry none of the IDL: not a width, not a signedness, not a bound, not a member id — and not the member order, which matters here in a way it does not in Python. A Python class body has an order and __annotations__ keeps it. A Lua table has none: next, and therefore pairs, “does not specify any order for the traversal” (Lua 5.4 reference §6.1). DDS-XTypes writes an aggregate’s members in declaration order (DDS-XTypes 1.3 §7.4.1.1.5.1), so an order that is not written down is an order that is gone.

So this column emits two things and both are data:

  1. A constructor per type — M.Pose.new() — returning a table with every member at the default of its type, so a program can make a value without reading anything.
  2. A descriptor table per type — M.Pose itself — holding the member list in the array part, where the index is the order, each member with its IDL name, its Lua key, its kind, its width, its signedness, its bound, whether it is optional, whether it is a key and its member id. That table is what a codec reads. Without it this column carries nothing.

Every descriptor is also registered in M._descriptors, keyed by the IDL scoped name (::common::util::Location), which is the one name that survives every mapping in this file.

§---@class and ---@field are comments

Above each type the file carries a LuaCATS block — ---@class Pose, ---@field x integer — which lua-language-server reads and an editor shows. Nothing in the Lua runtime reads it. It cannot refuse a program, it cannot refuse a value, and it is discarded by luac. It is written because an editor checking what Lua cannot is worth having, and it is named as a comment here so that nobody mistakes it for a check.

§Modules nest, they do not flatten

::common::util::Location is M.common.util.Location. A Lua table key is a string and any string will do, so the IDL scope survives whole. The Go column flattens to Common_Util_Location because a Go package is a directory and cannot nest inside one file; that join is not invertible, since an IDL identifier may itself contain _ (IDL 4.2 §7.2.3). Nesting is, so this column takes it.

§No identifier is ever renamed

This is the one thing this column has that the other nine do not. Lua restricts the syntax t.name to identifiers that are not keywords, and not the key: t.end does not parse and t["end"] does, and both name the key "end" (Lua 5.4 reference §3.4.9). So a member called end keeps the name end — only the form it is written in changes, which zerodds_lang_cst::lua::table_key and zerodds_lang_cst::lua::Place::field decide. Every descriptor carries name and key anyway, and in this column they are always the same string; a codec reads key, so if that ever stopped being true the codec would not change.

§The mapping

IDLLua valuewhere the rest is written
booleanfalse / truekind = "boolean"
octet, uint8, int8 … long longintegerkind, bits, signed
unsigned long longinteger, two’s complementsigned = false, wraps = true
char, wcharintegerkind = "char", bits = 8 or 16
float, doublefloatkind = "float", bits
long doublestring, the decimalkind = "float", bits = 128
string, wstringstringkind = "string", bits, bound
fixed<P,S>string, the decimalkind = "decimal", digits, scale
sequence<T,N>table, 1-basedkind = "sequence", base = 1, bound, element
T[N][M]nested tables, 1-basedkind = "array", dims = { N, M }
map<K,V>array of { key = …, value = … }kind = "map", key_type, value_type
enumintegervalues, names, enumerators, bit_bound
bitmaskintegervalues, bits, bit_bound
uniontable with _d and one key per branchdiscriminator, members
@optional T mthe key, or no keyoptional = true
typedefnothingkind = "alias", target
nativeopaquekind = "native", mapping
constM.LIMIT = 8the --- line above it
interfacetable of operation descriptorsoperations

§Which Lua this was measured against

The target is Lua 5.4 and every clause cited here is from its reference manual. The interpreter the measurements below were taken with is the one on the path, which reports Lua 5.5.0: integers and floats are still two subtypes of number, an integer is still 64-bit two’s complement, a hexadecimal numeral still wraps, pairs still has no defined order and an array still starts at 1. Where a measurement is quoted it is that interpreter’s output, and it is quoted rather than described so that a later version disagreeing with it is visible.

§unsigned long long is an integer holding the bit pattern

Lua 5.4 integers are 64-bit two’s complement and wrap: measured with the interpreter on the path, lua -e 'print(math.maxinteger + 1 == math.mininteger)' prints true. So a unsigned long long above 2^63 − 1 has no Lua integer that reads as the number it is — 18446744073709551615 reads back as -1.

A decimal string was considered, as long double and fixed get. It is not taken, for two reasons. The eight octets CDR writes are exactly the eight octets a Lua integer holds, so the integer is lossless on the wire and only the reading of it as a number is lost; and a string would make one of the ten integers a different Lua type from the other nine, so a codec would branch on the width it is already reading from the descriptor. What the descriptor says instead is signed = false, wraps = true, which is the fact, and a range check is the codec’s to make.

A constant is the one place this bites, because it has to be written as a literal. Lua 5.4 reference §3.1: a decimal numeral whose value does not fit in an integer “denotes a float”, and a hexadecimal one “wraps around” to fit. Measured: lua -e 'print(math.type(18446744073709551615))' prints float, and lua -e 'print(math.type(0xFFFFFFFFFFFFFFFF), 0xFFFFFFFFFFFFFFFF)' prints integer and -1. So a constant above 2^63 − 1 is written in hexadecimal, which is the exact bit pattern; one that does not fit in 64 bits at all is refused by name.

§long double is a string and never a Lua float

A Lua float is a C double — 53 bits of significand — and an IDL long double is a binary128 with 113. Writing one into a float loses sixty bits at the assignment, before any codec runs. The member holds the decimal text instead, as it does in the Go and Python columns.

§A sequence is 1-based and an IDL index is not

A Lua table used as an array starts at 1: the length operator and ipairs both assume it (Lua 5.4 reference §3.4.7). An IDL sequence index starts at 0. So element i of a sequence<T> is t[i + 1], and this is the single most likely defect in any program written against this column. The descriptor says base = 1 on every sequence, array and map, so a codec reads the convention rather than assuming it.

§map<K,V> is an array of pairs and never a Lua table keyed by K

Three reasons, any one of which decides it.

pairs has no defined order — next “does not specify any order for the traversal” (Lua 5.4 reference §6.1) — and a map<K,V> goes on the wire as an ordered sequence of key/value pairs (DDS-XTypes 1.3 §7.4.1.1.4). A Lua table would give a different octet stream for one value depending on nothing the program controls.

A Lua table cannot hold a nil value at all: assigning nil removes the key. A map whose value type is @optional, or whose value is simply absent, has nowhere to live.

And a Lua table cannot hold a nil key either, nor two keys that compare equal by value but not by identity — map<Point, long> is ordinary IDL, and two tables holding the same numbers are two different keys.

§@optional is the absence of the key, and that is why the

descriptor exists

DDS-XTypes 1.3 §7.2.2.4.4.4.7 makes absence the value. In Lua the absence of a value is the absence of the key: t.m = nil removes it, #t does not see it and pairs does not visit it. So a program holding a value cannot tell “the member is absent” from “this table was never a Pose” — the value carries no member list at all.

That is exactly why the descriptor carries the member list in order. M.Pose.members is where the members are; the value is where the ones that are present are.

§A union has a _d and one key per branch, not a tag and a payload

An IDL union discriminator may hold a value no branch selects (IDL 4.2 §7.4.1.4.4.1; DDS-XTypes 1.3 §7.4.1.1.5.2 writes only the selected branch, and there may be none). A { tag, payload } shape cannot say that without inventing a tag the IDL does not have. So _d is a member in its own right and the branches sit beside it, at most one of them present. _d cannot collide with a branch: an IDL identifier begins with an ASCII letter (IDL 4.2 §7.2.3), so no IDL member is spelled _d.

§What is refused

any, ValueBase, an object reference and an annotation type have no Lua data form and are named as MaterialiseError::NoLanguageForm where a member would carry one. Nothing is emitted for them at declaration level.

This matters more here than anywhere else. Every other column has at least a compiler between the generated file and the program: Go would need a type for an any member, and there is none. Lua needs nothing — a table entry holding whatever an any turned out to be would load, run and report success, with no wire form behind it. That is the silent-substitution class this project is arranged against, and Lua is where it is easiest to commit.

§What the generated file checks at load, and what it does not

Two things run while the chunk executes, and both are written into the file rather than left to a test:

  • M._resolved resolves every type name a descriptor mentions through assert. Reading a missing key in Lua answers nil and carries on, so a descriptor naming a type the file does not declare would otherwise be found by whatever read it next.
  • a discarded local calls every constructor once. A Lua function body is not checked against anything until it is called, so a load that did not call them would say nothing about them.

Everything else Lua does not check, and this column does not claim it does: no width, no bound, no member order and no member type is enforced at run time by anything in the generated file.

Constants§

DISCRIMINATOR
The key a union’s discriminator takes.
MODULE
The local the generated chunk builds and returns.

Functions§

declared_place
M.a.b.T — the place a declared type’s descriptor is bound to.
lua_file
Build the Lua module a rule application produced.
lua_items
The declarations only, without the chunk around them.
lua_type_descriptor
The descriptor a value of target is described by.
scoped_name
A declared type’s IDL scoped name: ::common::util::Location.