use crate::adapter::constant;
use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
use crate::adapter::interface;
use crate::apply::Applied;
use crate::fixed_literal::zero_decimal;
use crate::verbatim;
use seesaw_tgg::ident::GhostId;
use zerodds_ast::{
Ast, Bound, CharWidth, ConstValue, ModuleNode, PrimitiveKind, TypeKind, TypeNode,
};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal};
use zerodds_lang_cst::lua::{self, Entry, Expr, File, Function, Place, Stat, Table};
const LANGUAGE: &str = "Lua";
pub const MODULE: &str = "M";
pub const DISCRIMINATOR: &str = "_d";
const DESCRIPTORS: &str = "_descriptors";
const NAMES: &str = "_names";
const RESOLVED: &str = "_resolved";
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
pub fn lua_file(ast: &Ast, applied: &Applied) -> Result<File, MaterialiseError> {
let mut builder = Builder::new(ast);
builder.walk(applied)?;
Ok(File {
end_text: verbatim::file_slots(ast, verbatim::aliases::LUA).1,
doc: Doc::lines(&[
"Generated by ZeroDDS from an IDL source. Do not edit.",
"",
"Lua declares nothing. A struct, a sequence, an array, a map and a",
"union are all tables, and `math.type` is the only type question a",
"program can ask of a value. So every IDL fact is written into a",
"descriptor table instead: the member list in declaration order, and",
"for each member 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.",
"",
"M._descriptors maps the IDL scoped name of every declared type to its",
"descriptor. M._names lists those names in declaration order.",
"",
"The ---@class and ---@field blocks are comments. lua-language-server",
"reads them; nothing in the Lua runtime does.",
"",
"A sequence, an array and a map are 1-based here and 0-based in IDL:",
"element i of a sequence is t[i + 1]. Every such descriptor says",
"base = 1 rather than leaving it to be assumed.",
]),
body: {
let (begin_file, _) = verbatim::file_slots(ast, verbatim::aliases::LUA);
let mut body: Vec<Stat> = begin_file.into_iter().map(Stat::Verbatim).collect();
body.extend(builder.finish());
body
},
returns: Expr::place(Place::name(MODULE)),
})
}
pub fn lua_items(ast: &Ast, applied: &Applied) -> Result<Vec<Stat>, MaterialiseError> {
let mut builder = Builder::new(ast);
builder.walk(applied)?;
let mut out = builder.module_tables;
out.append(&mut builder.declarations);
Ok(verbatim::weave_file(ast, verbatim::aliases::LUA, out))
}
impl verbatim::Carrier for Stat {
fn verbatim(text: String) -> Self {
Self::Verbatim(text)
}
fn take_into_body(&mut self, _: &str, _: &mut Vec<String>, _: &mut Vec<String>) -> bool {
false
}
}
pub fn lua_type_descriptor(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<Table, MaterialiseError> {
let mut refs = Vec::new();
Ok(Table::of(type_entries(ast, target, at, &mut refs, 0)?))
}
struct Builder<'a> {
ast: &'a Ast,
module_tables: Vec<Stat>,
module_paths: Vec<String>,
declarations: Vec<Stat>,
emitted: Vec<(String, String)>,
names: Vec<String>,
refs: Vec<(String, String)>,
constructors: Vec<Place>,
}
impl<'a> Builder<'a> {
fn new(ast: &'a Ast) -> Self {
Self {
ast,
module_tables: Vec::new(),
module_paths: Vec::new(),
declarations: Vec::new(),
emitted: Vec::new(),
names: Vec::new(),
refs: Vec::new(),
constructors: Vec::new(),
}
}
fn walk(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
let ast = self.ast;
for (id, _) in ast.types() {
let start = self.declarations.len();
self.declare_type(applied, id)?;
verbatim::weave(
verbatim::declaration_slots(ast, id, verbatim::aliases::LUA),
"",
start,
&mut self.declarations,
);
}
self.declare_constants(applied)?;
self.declare_exceptions(applied)?;
self.declare_interfaces(applied)?;
Ok(())
}
fn finish(mut self) -> Vec<Stat> {
let mut out = vec![
Stat::Local {
doc: Doc::none(),
name: Ident::new(MODULE),
value: Some(Expr::Table(Table::default())),
},
Stat::Assign {
doc: Doc::lines(&[
"The descriptor of every type this module declares, by its IDL",
"scoped name. That name is the only one that survives every",
"mapping in this file, so it is what a codec looks a type up by.",
]),
target: Place::name(MODULE).field(DESCRIPTORS),
value: Expr::Table(Table::default()),
},
];
out.append(&mut self.module_tables);
out.append(&mut self.declarations);
out.push(Stat::Assign {
doc: Doc::lines(&[
"Every declared type, in IDL declaration order.",
"",
"The array part of a table, because that is the only part of a Lua",
"table that has an order: `next` does not specify one for the rest",
"(Lua 5.4 reference 6.1).",
]),
target: Place::name(MODULE).field(NAMES),
value: Expr::Table(Table::of(
self.names
.iter()
.map(|name| Entry::positional(Expr::string(name)))
.collect(),
)),
});
out.push(Stat::Assign {
doc: Doc::lines(&[
"Every type a descriptor names, resolved while this chunk runs.",
"",
"Reading a missing key in Lua answers nil and carries on, so a",
"descriptor naming a type this module does not declare would be",
"found by whatever read it next rather than here. `assert` is what",
"notices, and it notices at load.",
]),
target: Place::name(MODULE).field(RESOLVED),
value: Expr::Table(Table::of(self.reference_entries())),
});
out.push(Stat::Comment(Doc::lines(&[
"Every constructor, run once and discarded. A Lua function body is",
"not checked against anything until it is called, so a load that did",
"not call these would say nothing about them.",
])));
out.push(Stat::Local {
doc: Doc::none(),
name: Ident::new("_"),
value: Some(Expr::Table(Table::of(
self.constructors
.iter()
.map(|target| {
Entry::positional(Expr::Call {
func: Box::new(target.clone()),
args: Vec::new(),
})
})
.collect(),
))),
});
out
}
fn reference_entries(&self) -> Vec<Entry> {
self.refs
.iter()
.map(|(name, at)| Entry {
doc: Doc::none(),
key: lua::Key::Expr(Expr::string(name)),
value: Expr::Call {
func: Box::new(Place::name("assert")),
args: vec![
Expr::place(Place::Index {
base: Box::new(Place::name(MODULE).field(DESCRIPTORS)),
key: Box::new(Expr::string(name)),
}),
Expr::string(&format!(
"{at} names {name}, which this module does not declare"
)),
],
},
})
.collect()
}
fn place_of(&mut self, module: Id<ModuleNode>, name: &str) -> Result<Place, MaterialiseError> {
let ast = self.ast;
let chain = module_chain(ast, module)?;
let mut place = Place::name(MODULE);
let mut text = MODULE.to_string();
for segment in &chain {
place = place.field(segment);
text.push('.');
text.push_str(segment);
if !self.module_paths.contains(&text) {
self.module_paths.push(text.clone());
self.module_tables.push(Stat::Assign {
doc: Doc::line(&format!(
"The IDL module `{segment}`, as the table its declarations nest in."
)),
target: place.clone(),
value: Expr::Table(Table::default()),
});
}
}
Ok(place.field(name))
}
fn path_text(module: &[String], name: &str) -> String {
let mut text = MODULE.to_string();
for segment in module {
text.push('.');
text.push_str(segment);
}
text.push('.');
text.push_str(name);
text
}
fn remember(&mut self, path: &str, declared: &str) -> Result<(), MaterialiseError> {
if let Some((_, first)) = self.emitted.iter().find(|(other, _)| other == path) {
return Err(MaterialiseError::SpellingCollision {
first: first.clone(),
second: declared.to_string(),
spelled: path.to_string(),
language: LANGUAGE,
at: "the generated chunk".to_string(),
});
}
self.emitted.push((path.to_string(), declared.to_string()));
Ok(())
}
fn emit(&mut self, decl: Decl) -> Result<(), MaterialiseError> {
let ast = self.ast;
let chain = module_chain(ast, decl.module)?;
let path = Self::path_text(&chain, &decl.name);
self.remember(&path, &decl.scoped)?;
let target = self.place_of(decl.module, &decl.name)?;
if !decl.annotation.is_empty() {
self.declarations.push(Stat::Annotation(decl.annotation));
}
self.declarations.push(Stat::Assign {
doc: decl.doc,
target: target.clone(),
value: Expr::Table(decl.descriptor),
});
self.declarations.push(Stat::Assign {
doc: Doc::none(),
target: Place::Index {
base: Box::new(Place::name(MODULE).field(DESCRIPTORS)),
key: Box::new(Expr::string(&decl.scoped)),
},
value: Expr::place(target.clone()),
});
self.names.push(decl.scoped.clone());
if let Some(value) = decl.constructor {
let new = target.field("new");
let mut doc = Doc::line(&format!(
"A `{}` with every member at the default of its type.",
decl.scoped
));
doc.push("");
doc.push("An @optional member is not given a key at all: in Lua the absence");
doc.push("of a value is the absence of the key (DDS-XTypes 1.3 7.2.2.4.4.4.7).");
doc.push(&format!("@return {}", decl.class));
self.declarations.push(Stat::Function(Function {
doc,
target: new.clone(),
params: Vec::new(),
body: vec![Stat::Return(Some(value))],
}));
self.constructors.push(new);
}
Ok(())
}
}
struct Decl {
module: Id<ModuleNode>,
name: String,
scoped: String,
class: String,
annotation: Doc,
doc: Doc,
descriptor: Table,
constructor: Option<Expr>,
}
impl Builder<'_> {
#[allow(clippy::too_many_lines)]
fn declare_type(
&mut self,
applied: &Applied,
id: Id<TypeNode>,
) -> Result<(), MaterialiseError> {
let ast = self.ast;
let Ok(node) = ast.type_(id) else {
return Ok(());
};
let scoped = scoped_name(ast, id);
let class = class_name(ast, id);
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Map { .. }
| TypeKind::Array { .. } => {}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "LuaAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let inner = Table::of(type_entries(ast, target, &scoped, &mut self.refs, 0)?);
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: cats_type(ast, target, 0),
annotation: Doc::none(),
doc: Doc::lines(&[
"A typedef. Lua has no alias to declare, so what is left of it",
"is this entry: the name, and the descriptor of what it names.",
"Every use of the alias takes the target's descriptor.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("alias")),
Entry::named("target", Expr::Table(inner)),
]),
constructor: Some(default_expr(ast, target, &scoped, 0)?),
})?;
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "LuaEnum").is_none() {
return Ok(());
}
let mut ordered = Vec::new();
let mut values = Vec::new();
let mut names = Vec::new();
let mut first = None;
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
continue;
};
if ghost_created_for(applied, enum_node.node_id, "LuaEnumerator").is_none() {
continue;
}
let value = self
.ast
.effective_enumerator_value(*enumerator)
.ok_or_else(|| MaterialiseError::Unevaluated {
construct: "enumerator value".to_string(),
at: enum_node.name.clone(),
})?;
let literal = integer_literal(value, &enum_node.name)?;
if first.is_none() {
first = Some(literal.clone());
}
ordered.push(Entry::positional(Expr::Table(Table::of(vec![
Entry::named("name", Expr::string(&enum_node.name)),
Entry::named("value", literal.clone()),
]))));
values.push(Entry::named(&enum_node.name, literal.clone()));
names.push(Entry {
doc: Doc::none(),
key: lua::Key::Expr(literal),
value: Expr::string(&enum_node.name),
});
}
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: Doc::none(),
doc: Doc::lines(&[
"An enumeration, as the two maps and the ordered list an",
"enumerator carries. The values are written out because @value",
"admits gaps, so a position is not a value.",
"",
"`names` is the inverse of `values`; a Lua table holds both",
"directions at once because its keys may be integers as well as",
"strings.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("enum")),
Entry::named(
"bit_bound",
Expr::int(i128::from(ast.effective_bit_bound(id))),
),
Entry::named("enumerators", Expr::Table(Table::of(ordered))),
Entry::named("values", Expr::Table(Table::of(values))),
Entry::named("names", Expr::Table(Table::of(names))),
]),
constructor: first,
})?;
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "LuaBitmask").is_none() {
return Ok(());
}
let bound = ast.effective_bit_bound(id);
let mut ordered = Vec::new();
let mut values = Vec::new();
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
continue;
};
if ghost_created_for(applied, bit_node.node_id, "LuaBitEntry").is_none() {
continue;
}
let position = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
let shift = Expr::Binary {
op: "<<",
lhs: Box::new(Expr::int(1)),
rhs: Box::new(Expr::int(i128::from(position))),
};
ordered.push(Entry::positional(Expr::Table(Table::of(vec![
Entry::named("name", Expr::string(&bit_node.name)),
Entry::named("position", Expr::int(i128::from(position))),
Entry::named("value", shift.clone()),
]))));
values.push(Entry::named(&bit_node.name, shift));
}
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: Doc::none(),
doc: Doc::lines(&[
"A bitmask: one value per named bit, and the holder width the",
"@bit_bound gives (Table 33 of DDS-XTypes 1.3 7.4.1.1.3.2).",
"",
"A Lua integer is 64-bit two's complement, so a bit_bound of 64",
"puts bit 63 in the sign; the width is here so a codec masks",
"rather than assumes.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("bitmask")),
Entry::named("bit_bound", Expr::int(i128::from(bound))),
Entry::named("bits", Expr::Table(Table::of(ordered))),
Entry::named("values", Expr::Table(Table::of(values))),
]),
constructor: Some(Expr::int(0)),
})?;
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "LuaBitset").is_none() {
return Ok(());
}
let mut members = Vec::new();
let mut defaults = Vec::new();
let mut fields = Vec::new();
let mut pairs = Vec::new();
for field in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*field) else {
continue;
};
let Some(name) = field_node.name.as_ref() else {
continue;
};
pairs.push((name.clone(), name.clone()));
members.push(Entry::positional(Expr::Table(Table::of(vec![
Entry::named("name", Expr::string(name)),
Entry::named("key", Expr::string(name)),
Entry::named("lua", Expr::string("integer")),
Entry::named("kind", Expr::string("int")),
Entry::named("bits", Expr::int(i128::from(field_node.bits))),
Entry::named("signed", Expr::bool(false)),
Entry::named("wraps", Expr::bool(false)),
Entry::named("optional", Expr::bool(false)),
Entry::named("is_key", Expr::bool(false)),
Entry::named("id", Expr::bool(false)),
]))));
defaults.push(Entry::named(name, Expr::int(0)));
fields.push(cats_field(name, "integer", false));
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &node.name)?;
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: cats_class(&class, &fields),
doc: Doc::lines(&[
"A bitset. Each field states its own bit width; Lua has no",
"fixed-width integer, so nothing is rounded up to a holder the",
"way the C and Go columns have to.",
"",
"The packing into the anchor is the codec's, because the order",
"on the wire is the declaration's.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("bitset")),
Entry::named("members", Expr::Table(Table::of(members))),
]),
constructor: Some(Expr::Table(Table::of(defaults))),
})?;
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let is_value = matches!(node.kind, TypeKind::Valuetype { .. });
let created = if is_value { "LuaValue" } else { "LuaStruct" };
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let members = ast.members_of(id).to_vec();
let built =
self.aggregate(crate::adapter::c::struct_base(ast, id), &members, &scoped)?;
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: cats_class(&class, &built.fields),
doc: Doc::lines(&[
"The member list is the array part of `members`, so the index is",
"the declaration order — which is the order the wire uses",
"(DDS-XTypes 1.3 7.4.1.1.5.1). A Lua table has no order of its",
"own: `next` does not specify one (Lua 5.4 reference 6.1).",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named(
"kind",
Expr::string(if is_value { "valuetype" } else { "struct" }),
),
Entry::named(
"extensibility",
Expr::string(ast.effective_extensibility(id).as_str()),
),
Entry::named("members", Expr::Table(Table::of(built.members))),
]),
constructor: Some(Expr::Table(Table::of(built.defaults))),
})?;
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "LuaUnion").is_none() {
return Ok(());
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: scoped,
});
};
let cases = ast.cases_of(id).to_vec();
let mut branches = Vec::new();
let mut pairs = vec![(DISCRIMINATOR.to_string(), "<discriminator>".to_string())];
for case in &cases {
let Ok(case_node) = ast.case(*case) else {
continue;
};
if ghost_created_for(applied, case_node.node_id, "LuaCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
continue;
};
pairs.push((member.name.clone(), member.name.clone()));
branches.push(case_node.member);
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, &node.name)?;
let disc_entries = {
let mut entries = vec![
Entry::named("name", Expr::string(DISCRIMINATOR)),
Entry::named("key", Expr::string(DISCRIMINATOR)),
];
entries.append(&mut type_entries(
ast,
disc,
&format!("{scoped}._d"),
&mut self.refs,
0,
)?);
entries
};
let built = self.aggregate(None, &branches, &scoped)?;
let mut fields = vec![cats_field(DISCRIMINATOR, &cats_type(ast, disc, 0), false)];
for field in &built.fields {
fields.push(optional_field(field));
}
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: cats_class(&class, &fields),
doc: Doc::lines(&[
"A union: the discriminator under `_d`, and one key per branch.",
"",
"Not a tag and a payload. An IDL discriminator may hold a value",
"no branch selects (IDL 4.2 7.4.1.4.4.1), and only the selected",
"branch is written (DDS-XTypes 1.3 7.4.1.1.5.2) — so `_d` is a",
"member in its own right and the branches sit beside it, at most",
"one present.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("union")),
Entry::named(
"extensibility",
Expr::string(ast.effective_extensibility(id).as_str()),
),
Entry::named("discriminator", Expr::Table(Table::of(disc_entries))),
Entry::named("members", Expr::Table(Table::of(built.members))),
]),
constructor: Some(Expr::Table(Table::of(vec![Entry::named(
DISCRIMINATOR,
default_expr(ast, disc, &scoped, 0)?,
)]))),
})?;
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "LuaNative").is_none() {
return Ok(());
}
let mapping = self
.ast
.any_of_type(id)
.ok()
.and_then(|any| ast.facets(any))
.and_then(|facets| facets.corba.as_ref())
.and_then(|corba| corba.native_mapping.clone());
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: Doc::none(),
doc: Doc::lines(&[
"A native type: opaque unless a facet named its mapping.",
"",
"`mapping = false` and not nil. A table entry set to nil is an",
"entry that does not exist, so nil could not be told from a key",
"this file never wrote.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("native")),
Entry::named(
"mapping",
mapping.as_deref().map_or(Expr::bool(false), Expr::string),
),
]),
constructor: None,
})?;
}
TypeKind::ValueBox { .. } => {}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
fn aggregate(
&mut self,
base: Option<Id<TypeNode>>,
members: &[Id<zerodds_ast::MemberNode>],
at: &str,
) -> Result<Aggregate, MaterialiseError> {
let ast = self.ast;
let mut pairs = Vec::new();
if base.is_some() {
pairs.push(("parent".to_string(), "parent".to_string()));
}
for member in members {
if let Ok(node) = ast.member(*member) {
pairs.push((node.name.clone(), node.name.clone()));
}
}
no_spelling_collision(as_pairs(&pairs), LANGUAGE, at)?;
let mut built = Aggregate::default();
if let Some(base) = base {
let site = format!("{at}.parent");
let mut entries = vec![
Entry::named("name", Expr::string("parent")),
Entry::named("key", Expr::string("parent")),
];
entries.append(&mut type_entries(ast, base, &site, &mut self.refs, 0)?);
entries.push(Entry::named("optional", Expr::bool(false)));
entries.push(Entry::named("is_key", Expr::bool(false)));
entries.push(Entry::named("id", Expr::bool(false)));
built
.members
.push(Entry::positional(Expr::Table(Table::of(entries))));
built
.fields
.push(cats_field("parent", &cats_type(ast, base, 0), false));
built.defaults.push(Entry {
doc: Doc::none(),
key: lua::table_key("parent"),
value: default_expr(ast, base, &site, 0)?,
});
}
for member in members {
let Ok(node) = ast.member(*member) else {
continue;
};
let site = format!("{at}.{}", node.name);
let Some(target) = node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: site,
});
};
let optional = node.facets.is_optional();
let mut entries = vec![
Entry::named("name", Expr::string(&node.name)),
Entry::named("key", Expr::string(&node.name)),
];
entries.append(&mut type_entries(ast, target, &site, &mut self.refs, 0)?);
entries.push(Entry::named("optional", Expr::bool(optional)));
entries.push(Entry::named("is_key", Expr::bool(node.facets.is_key())));
entries.push(Entry::named(
"id",
node.facets
.effective_member_id()
.map_or(Expr::bool(false), |id| Expr::int(i128::from(id))),
));
built
.members
.push(Entry::positional(Expr::Table(Table::of(entries))));
built
.fields
.push(cats_field(&node.name, &cats_type(ast, target, 0), optional));
if !optional {
built.defaults.push(Entry {
doc: Doc::none(),
key: lua::table_key(&node.name),
value: default_expr(ast, target, &site, 0)?,
});
}
}
Ok(built)
}
}
#[derive(Default)]
struct Aggregate {
members: Vec<Entry>,
defaults: Vec<Entry>,
fields: Vec<String>,
}
impl Builder<'_> {
fn declare_constants(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
let ast = self.ast;
for (_, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "LuaConst").is_none() {
continue;
}
let value = ast
.eval_const(node.expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "constant's value".to_string(),
at: node.name.clone(),
})?;
let declared = node.type_.resolved();
let spelling =
declared.map_or_else(|| "long".to_string(), |target| idl_spelling(ast, target, 0));
let chain = module_chain(ast, node.module)?;
let path = Self::path_text(&chain, &node.name);
let scoped = declared_scoped(&chain, &node.name);
self.remember(&path, &scoped)?;
let target = self.place_of(node.module, &node.name)?;
let expr = constant_expr(
&value,
declared.is_some_and(|id| is_decimal_holder(ast, id)),
&node.name,
)?;
self.declarations.push(Stat::Assign {
doc: Doc::line(&format!("`const {spelling} {}` — IDL {scoped}.", node.name)),
target,
value: expr,
});
}
Ok(())
}
fn declare_exceptions(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
let ast = self.ast;
for (_, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "LuaError").is_none() {
continue;
}
let chain = module_chain(ast, node.module)?;
let scoped = declared_scoped(&chain, &node.name);
let class = dotted(&chain, &node.name);
let built = self.aggregate(None, &node.members, &scoped)?;
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class: class.clone(),
annotation: cats_class(&class, &built.fields),
doc: Doc::lines(&[
"An exception, as the structure its members make. Raising is the",
"RPC arc's decision; the shape is the IDL's.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("exception")),
Entry::named("members", Expr::Table(Table::of(built.members))),
]),
constructor: Some(Expr::Table(Table::of(built.defaults))),
})?;
}
Ok(())
}
fn declare_interfaces(&mut self, applied: &Applied) -> Result<(), MaterialiseError> {
let ast = self.ast;
for (id, node) in ast.interfaces() {
if ghost_created_for(applied, node.node_id, "LuaInterface").is_none() {
continue;
}
let chain = module_chain(ast, node.module)?;
let scoped = declared_scoped(&chain, &node.name);
let class = dotted(&chain, &node.name);
let mut operations = Vec::new();
let mut signatures = Vec::new();
let all = interface::all_operations_flattened(ast, id)
.map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
for op in all
.iter()
.filter(|op| op.origin == interface::Origin::Declared)
{
signatures.push((op.name.clone(), op.name.clone()));
let mut params = Vec::new();
for param in &op.params {
let mut entries = vec![
Entry::named("name", Expr::string(¶m.name)),
Entry::named("key", Expr::string(¶m.name)),
];
entries.append(&mut type_entries(
ast,
param.type_,
&format!("{scoped}.{}({})", op.name, param.name),
&mut self.refs,
0,
)?);
params.push(Entry::positional(Expr::Table(Table::of(entries))));
}
let mut results = Vec::new();
if let Some(returns) = op.returns {
results.push(Entry::positional(Expr::Table(Table::of(type_entries(
ast,
returns,
&format!("{scoped}.{}", op.name),
&mut self.refs,
0,
)?))));
}
operations.push(Entry::positional(Expr::Table(Table::of(vec![
Entry::named("name", Expr::string(&op.name)),
Entry::named("key", Expr::string(&op.name)),
Entry::named("params", Expr::Table(Table::of(params))),
Entry::named("results", Expr::Table(Table::of(results))),
Entry::named("raises", raise_names(&op.raises)),
]))));
}
let mut attributes = Vec::new();
let mut attribute_signatures = Vec::new();
let all_attrs = interface::all_attributes_flattened(ast, id)
.map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
for attribute in &all_attrs {
attribute_signatures.push((attribute.name.clone(), attribute.name.clone()));
let type_entries_for_attr = type_entries(
ast,
attribute.type_,
&format!("{scoped}.{}", attribute.name),
&mut self.refs,
0,
)?;
attributes.push(Entry::positional(Expr::Table(Table::of(vec![
Entry::named("name", Expr::string(&attribute.name)),
Entry::named("key", Expr::string(&attribute.name)),
Entry::named("type", Expr::Table(Table::of(type_entries_for_attr))),
Entry::named("readonly", Expr::bool(attribute.readonly)),
Entry::named("get_raises", raise_names(&attribute.get_raises)),
Entry::named("set_raises", raise_names(&attribute.set_raises)),
]))));
}
no_spelling_collision(as_pairs(&signatures), LANGUAGE, &node.name)?;
no_spelling_collision(as_pairs(&attribute_signatures), LANGUAGE, &node.name)?;
self.emit(Decl {
module: node.module,
name: node.name.clone(),
scoped: scoped.clone(),
class,
annotation: Doc::none(),
doc: Doc::lines(&[
"An interface, as the operations and attributes it has — its",
"bases' before its own, because the table has no link to a base's.",
"Emitted rather than refused: an operation's parameters and result",
"are a shape, and a shape is exactly what this column carries — an",
"attribute's type and `readonly` flag are the same kind of shape.",
"What none of it is is a call — the transport is the RPC arc's",
"business.",
"",
"`results` is an array of nought or one. A `void` operation and one",
"whose result key was never written would otherwise be the same",
"table, because a Lua key set to nil does not exist. `raises`/",
"`get_raises`/`set_raises` are arrays of exception names, empty",
"where the IDL declares none.",
]),
descriptor: Table::of(vec![
Entry::named("idl", Expr::string(&scoped)),
Entry::named("name", Expr::string(&node.name)),
Entry::named("kind", Expr::string("interface")),
Entry::named("operations", Expr::Table(Table::of(operations))),
Entry::named("attributes", Expr::Table(Table::of(attributes))),
]),
constructor: None,
})?;
}
Ok(())
}
}
fn raise_names(raises: &[interface::Raise]) -> Expr {
Expr::Table(Table::of(
raises
.iter()
.map(|raise| Entry::positional(Expr::string(&raise.name)))
.collect(),
))
}
#[allow(clippy::too_many_lines)]
fn type_entries(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
refs: &mut Vec<(String, String)>,
depth: usize,
) -> Result<Vec<Entry>, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose descriptor nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
let refused = match &node.kind {
TypeKind::Any => Some("any"),
TypeKind::ValueBase => Some("ValueBase"),
TypeKind::ObjectRef { .. } => Some("Object"),
TypeKind::AnnotationType { .. } => Some("annotation type"),
_ => None,
};
if let Some(construct) = refused {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
Ok(match &node.kind {
TypeKind::Primitive(kind) => primitive_entries(*kind),
TypeKind::String { width, bound } => vec![
Entry::named("lua", Expr::string("string")),
Entry::named("kind", Expr::string("string")),
Entry::named("bits", Expr::int(char_bits(*width))),
Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
],
TypeKind::Fixed { digits, scale } => vec![
Entry::named("lua", Expr::string("string")),
Entry::named("kind", Expr::string("decimal")),
Entry::named("digits", Expr::int(i128::from(*digits))),
Entry::named("scale", Expr::int(i128::from(*scale))),
],
TypeKind::Sequence { element, bound } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
vec![
Entry::named("lua", Expr::string("table")),
Entry::named("kind", Expr::string("sequence")),
Entry::named("base", Expr::int(1)),
Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
Entry::named(
"element",
Expr::Table(Table::of(type_entries(ast, element, at, refs, depth + 1)?)),
),
]
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut lengths = Vec::new();
for dim in dims {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
})?;
lengths.push(Entry::positional(Expr::int(length)));
}
vec![
Entry::named("lua", Expr::string("table")),
Entry::named("kind", Expr::string("array")),
Entry::named("base", Expr::int(1)),
Entry::named("dims", Expr::Table(Table::of(lengths))),
Entry::named(
"element",
Expr::Table(Table::of(type_entries(ast, element, at, refs, depth + 1)?)),
),
]
}
TypeKind::Map { key, value, bound } => {
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: at.to_string(),
});
};
vec![
Entry::named("lua", Expr::string("table")),
Entry::named("kind", Expr::string("map")),
Entry::named("base", Expr::int(1)),
Entry::named("bound", Expr::int(bound_of(ast, *bound, at)?)),
Entry::named(
"key_type",
Expr::Table(Table::of(type_entries(ast, key, at, refs, depth + 1)?)),
),
Entry::named(
"value_type",
Expr::Table(Table::of(type_entries(ast, value, at, refs, depth + 1)?)),
),
]
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
let mut entries = type_entries(ast, target, at, refs, depth + 1)?;
if !node.name.is_empty() {
entries.push(Entry::named("alias", Expr::string(&scoped_name(ast, id))));
}
entries
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
let mut entries = type_entries(ast, target, at, refs, depth + 1)?;
entries.push(Entry::named("boxed", Expr::bool(true)));
entries
}
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => {
let scoped = scoped_name(ast, id);
note(refs, &scoped, at);
vec![
Entry::named("lua", Expr::string("integer")),
Entry::named(
"kind",
Expr::string(if matches!(node.kind, TypeKind::Bitmask { .. }) {
"bitmask"
} else {
"enum"
}),
),
Entry::named("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
Entry::named("ref", Expr::string(&scoped)),
]
}
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => {
let scoped = scoped_name(ast, id);
note(refs, &scoped, at);
vec![
Entry::named("lua", Expr::string("table")),
Entry::named("kind", Expr::string(aggregate_kind(&node.kind))),
Entry::named("ref", Expr::string(&scoped)),
]
}
TypeKind::Native => {
let scoped = scoped_name(ast, id);
note(refs, &scoped, at);
vec![
Entry::named("lua", Expr::string("any")),
Entry::named("kind", Expr::string("native")),
Entry::named("ref", Expr::string(&scoped)),
]
}
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
})
}
fn note(refs: &mut Vec<(String, String)>, name: &str, at: &str) {
if refs.iter().any(|(seen, _)| seen == name) {
return;
}
refs.push((name.to_string(), at.to_string()));
}
const fn aggregate_kind(kind: &TypeKind) -> &'static str {
match kind {
TypeKind::Union { .. } => "union",
TypeKind::Bitset { .. } => "bitset",
TypeKind::Valuetype { .. } => "valuetype",
_ => "struct",
}
}
fn primitive_entries(kind: PrimitiveKind) -> Vec<Entry> {
let integer = |bits: i128, signed: bool, wraps: bool| {
vec![
Entry::named("lua", Expr::string("integer")),
Entry::named("kind", Expr::string("int")),
Entry::named("bits", Expr::int(bits)),
Entry::named("signed", Expr::bool(signed)),
Entry::named("wraps", Expr::bool(wraps)),
]
};
let character = |bits: i128| {
vec![
Entry::named("lua", Expr::string("integer")),
Entry::named("kind", Expr::string("char")),
Entry::named("bits", Expr::int(bits)),
]
};
match kind {
PrimitiveKind::Boolean => vec![
Entry::named("lua", Expr::string("boolean")),
Entry::named("kind", Expr::string("boolean")),
Entry::named("bits", Expr::int(8)),
],
PrimitiveKind::Char => character(8),
PrimitiveKind::WChar => character(16),
PrimitiveKind::Octet | PrimitiveKind::UInt8 => integer(8, false, false),
PrimitiveKind::Int8 => integer(8, true, false),
PrimitiveKind::Int16 => integer(16, true, false),
PrimitiveKind::UInt16 => integer(16, false, false),
PrimitiveKind::Int32 => integer(32, true, false),
PrimitiveKind::UInt32 => integer(32, false, false),
PrimitiveKind::Int64 => integer(64, true, false),
PrimitiveKind::UInt64 => integer(64, false, true),
PrimitiveKind::Float32 => vec![
Entry::named("lua", Expr::string("number")),
Entry::named("kind", Expr::string("float")),
Entry::named("bits", Expr::int(32)),
],
PrimitiveKind::Float64 => vec![
Entry::named("lua", Expr::string("number")),
Entry::named("kind", Expr::string("float")),
Entry::named("bits", Expr::int(64)),
],
PrimitiveKind::Float128 => vec![
Entry::named("lua", Expr::string("string")),
Entry::named("kind", Expr::string("float")),
Entry::named("bits", Expr::int(128)),
],
}
}
const fn char_bits(width: CharWidth) -> i128 {
match width {
CharWidth::Char => 8,
CharWidth::WChar => 16,
}
}
fn bound_of(ast: &Ast, bound: Bound, at: &str) -> Result<i128, MaterialiseError> {
match bound.expr() {
None => Ok(0),
Some(expr) => ast
.eval_integral(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "bound".to_string(),
at: at.to_string(),
}),
}
}
#[allow(clippy::too_many_lines)]
fn default_expr(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<Expr, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose default nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
Ok(match &node.kind {
TypeKind::Primitive(PrimitiveKind::Boolean) => Expr::bool(false),
TypeKind::Primitive(PrimitiveKind::Float32 | PrimitiveKind::Float64) => {
Expr::Literal(Literal::Float(lua::float_text("0")))
}
TypeKind::Primitive(PrimitiveKind::Float128) => Expr::string("0"),
TypeKind::Primitive(_) => Expr::int(0),
TypeKind::String { .. } => Expr::string(""),
TypeKind::Fixed { scale, .. } => Expr::string(&zero_decimal(*scale)),
TypeKind::Sequence { .. } | TypeKind::Map { .. } => Expr::Table(Table::default()),
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut lengths = Vec::new();
for dim in dims {
lengths.push(ast.eval_integral(*dim).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
}
})?);
}
let mut value = default_expr(ast, element, at, depth + 1)?;
for length in lengths.iter().rev() {
let count =
usize::try_from(*length).map_err(|_| MaterialiseError::NoLanguageForm {
construct: format!("an array of {length} elements"),
language: LANGUAGE,
at: at.to_string(),
})?;
value = Expr::Table(Table::of(
vec![value; count]
.into_iter()
.map(Entry::positional)
.collect(),
));
}
value
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
default_expr(ast, target, at, depth + 1)?
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
default_expr(ast, target, at, depth + 1)?
}
TypeKind::Enumeration { enumerators } => {
let first = enumerators
.first()
.and_then(|first| ast.effective_enumerator_value(*first));
match first {
Some(value) => integer_literal(value, at)?,
None => Expr::int(0),
}
}
TypeKind::Bitmask { .. } => Expr::int(0),
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => Expr::Call {
func: Box::new(constructor_place(ast, id)?),
args: Vec::new(),
},
TypeKind::Native => Expr::bool(false),
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: LANGUAGE,
at: at.to_string(),
});
}
})
}
fn constructor_place(ast: &Ast, id: Id<TypeNode>) -> Result<Place, MaterialiseError> {
Ok(declared_place(ast, id)?.field("new"))
}
pub fn declared_place(ast: &Ast, id: Id<TypeNode>) -> Result<Place, MaterialiseError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "a member's declared type".to_string(),
})?;
let mut place = Place::name(MODULE);
for segment in module_chain(ast, node.module)? {
place = place.field(&segment);
}
Ok(place.field(&node.name))
}
fn is_decimal_holder(ast: &Ast, id: Id<TypeNode>) -> bool {
let mut current = id;
for _ in 0..MAX_TYPE_DEPTH {
match ast.type_(current).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(PrimitiveKind::Float128) | TypeKind::Fixed { .. }) => {
return true;
}
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(target) => current = target,
None => return false,
},
_ => return false,
}
}
false
}
fn constant_expr(
value: &ConstValue,
decimal_holder: bool,
at: &str,
) -> Result<Expr, MaterialiseError> {
let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: LANGUAGE,
at: at.to_string(),
};
Ok(match value {
ConstValue::Boolean(flag) => Expr::bool(*flag),
ConstValue::Integer(number) => integer_literal(*number, at)?,
ConstValue::Char(character) => Expr::int(i128::from(u32::from(*character))),
ConstValue::WChar(point) => Expr::int(i128::from(*point)),
ConstValue::Float { mantissa, exponent } => {
if decimal_holder {
return Err(refuse("a `long double` constant, which has no Lua literal"));
}
Expr::Literal(Literal::Float(lua::float_text(&constant::float_text(
mantissa,
exponent.as_deref(),
))))
}
ConstValue::Fixed { digits, scale } => {
Expr::string(&constant::fixed_decimal(digits, *scale))
}
ConstValue::String(text) => Expr::string(text),
ConstValue::WString(points) => Expr::string(
&constant::wstring_utf8(points)
.ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
),
_ => return Err(refuse("a constant form this binding has no literal for")),
})
}
fn integer_literal(value: i128, at: &str) -> Result<Expr, MaterialiseError> {
if value == i128::from(i64::MIN) {
return Ok(Expr::Literal(Literal::Int(
"0x8000000000000000".to_string(),
)));
}
if i64::try_from(value).is_ok() {
return Ok(Expr::int(value));
}
if let Ok(unsigned) = u64::try_from(value) {
return Ok(Expr::Literal(Literal::Int(format!("0x{unsigned:016X}"))));
}
Err(MaterialiseError::NoLanguageForm {
construct: format!("the integer {value}, which is outside a 64-bit Lua integer"),
language: LANGUAGE,
at: at.to_string(),
})
}
#[must_use]
pub fn scoped_name(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return "::<unnamed>".to_string();
};
match ast.scoped_name_of(any) {
Some(scoped) => format!("{scoped}"),
None => "::<unnamed>".to_string(),
}
}
fn declared_scoped(chain: &[String], name: &str) -> String {
let mut out = String::from("::");
for segment in chain {
out.push_str(segment);
out.push_str("::");
}
out.push_str(name);
out
}
fn dotted(chain: &[String], name: &str) -> String {
let mut segments: Vec<&str> = chain.iter().map(String::as_str).collect();
segments.push(name);
segments.join(".")
}
fn class_name(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return "unknown".to_string();
};
match ast.scoped_name_of(any) {
Some(scoped) => scoped.segments().join("."),
None => "unknown".to_string(),
}
}
fn idl_spelling(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_TYPE_DEPTH {
return "…".to_string();
}
match ast
.type_(id)
.map(|node| (node.name.clone(), node.kind.clone()))
{
Ok((_, TypeKind::Primitive(kind))) => kind.as_str().to_string(),
Ok((
_,
TypeKind::String {
width: CharWidth::Char,
..
},
)) => "string".to_string(),
Ok((
_,
TypeKind::String {
width: CharWidth::WChar,
..
},
)) => "wstring".to_string(),
Ok((_, TypeKind::Fixed { digits, scale })) => format!("fixed<{digits},{scale}>"),
Ok((_, TypeKind::Sequence { element, .. })) => match element.resolved() {
Some(element) => format!("sequence<{}>", idl_spelling(ast, element, depth + 1)),
None => "sequence<?>".to_string(),
},
Ok((name, _)) if !name.is_empty() => scoped_name(ast, id),
_ => "?".to_string(),
}
}
fn module_chain(ast: &Ast, module: Id<ModuleNode>) -> Result<Vec<String>, MaterialiseError> {
let mut out = Vec::new();
let mut current = Some(module);
let mut depth = 0;
while let Some(id) = current {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!(
"#{}@{} — the parent chain runs past {MAX_MODULE_DEPTH}",
module.slot(),
module.generation()
),
});
}
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: format!("#{}@{}", id.slot(), id.generation()),
})?;
if node.parent.is_none() {
break;
}
out.push(node.name.clone());
current = node.parent;
depth += 1;
}
out.reverse();
Ok(out)
}
fn cats_field(name: &str, type_: &str, optional: bool) -> String {
if lua::is_identifier(name) {
let mark = if optional { "?" } else { "" };
return format!("@field {name}{mark} {type_}");
}
let absence = if optional {
", absent when not set"
} else {
""
};
format!("the member `{name}` is at [\"{name}\"] and holds {type_}{absence}")
}
fn cats_class(class: &str, fields: &[String]) -> Doc {
let mut doc = Doc::line(&format!("@class {class}"));
for field in fields {
doc.push(field);
}
doc
}
fn optional_field(field: &str) -> String {
match field.strip_prefix("@field ") {
Some(rest) => match rest.split_once(' ') {
Some((name, type_)) if !name.ends_with('?') => {
format!("@field {name}? {type_}")
}
_ => field.to_string(),
},
None => field.to_string(),
}
}
fn cats_type(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_TYPE_DEPTH {
return "any".to_string();
}
let Ok(node) = ast.type_(id) else {
return "any".to_string();
};
match &node.kind {
TypeKind::Primitive(PrimitiveKind::Boolean) => "boolean".to_string(),
TypeKind::Primitive(PrimitiveKind::Float32 | PrimitiveKind::Float64) => {
"number".to_string()
}
TypeKind::Primitive(PrimitiveKind::Float128) | TypeKind::String { .. } => {
"string".to_string()
}
TypeKind::Primitive(_) => "integer".to_string(),
TypeKind::Fixed { .. } => "string".to_string(),
TypeKind::Sequence { element, .. } => match element.resolved() {
Some(element) => format!("{}[]", cats_type(ast, element, depth + 1)),
None => "any[]".to_string(),
},
TypeKind::Array { element, dims } => {
let mut out = match element.resolved() {
Some(element) => cats_type(ast, element, depth + 1),
None => "any".to_string(),
};
for _ in dims {
out.push_str("[]");
}
out
}
TypeKind::Map { .. } => "table[]".to_string(),
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => cats_type(ast, target, depth + 1),
None => "any".to_string(),
},
TypeKind::ValueBox { boxed } => match boxed.resolved() {
Some(target) => format!("{}?", cats_type(ast, target, depth + 1)),
None => "any?".to_string(),
},
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => "integer".to_string(),
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => class_name(ast, id),
TypeKind::Native
| TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => "any".to_string(),
}
}
fn as_pairs(pairs: &[(String, String)]) -> Vec<(&str, &str)> {
pairs
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str()))
.collect()
}
fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
let anchor = applied
.origin
.iter()
.find(|(_, model)| **model == node)
.map(|(ghost, _)| *ghost)?;
let typ = applied.graph.types.lookup(type_name)?;
applied
.graph
.nodes_of_type(typ)
.map(|created| created.id)
.find(|created| {
applied.graph.connected(&anchor, created)
|| applied
.graph
.parts(&anchor)
.any(|part| applied.graph.connected(&part.other, created))
})
}