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:
- 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. - A descriptor table per type —
M.Poseitself — 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
| IDL | Lua value | where the rest is written |
|---|---|---|
boolean | false / true | kind = "boolean" |
octet, uint8, int8 … long long | integer | kind, bits, signed |
unsigned long long | integer, two’s complement | signed = false, wraps = true |
char, wchar | integer | kind = "char", bits = 8 or 16 |
float, double | float | kind = "float", bits |
long double | string, the decimal | kind = "float", bits = 128 |
string, wstring | string | kind = "string", bits, bound |
fixed<P,S> | string, the decimal | kind = "decimal", digits, scale |
sequence<T,N> | table, 1-based | kind = "sequence", base = 1, bound, element |
T[N][M] | nested tables, 1-based | kind = "array", dims = { N, M } |
map<K,V> | array of { key = …, value = … } | kind = "map", key_type, value_type |
enum | integer | values, names, enumerators, bit_bound |
bitmask | integer | values, bits, bit_bound |
union | table with _d and one key per branch | discriminator, members |
@optional T m | the key, or no key | optional = true |
typedef | nothing | kind = "alias", target |
native | opaque | kind = "native", mapping |
const | M.LIMIT = 8 | the --- line above it |
interface | table of operation descriptors | operations |
§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._resolvedresolves every type name a descriptor mentions throughassert. Reading a missing key in Lua answersniland 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
targetis described by. - scoped_
name - A declared type’s IDL scoped name:
::common::util::Location.