weaveffi_model/ty.rs
1//! The **resolved type** every backend consumes, the single taxonomy that
2//! classifies it for the C ABI and the value-buffer wire format, and the
3//! [`TypeIndex`] that maps each user type name to its declaration.
4//!
5//! The IDL's [`TypeRef`](crate::ir::TypeRef) is the type *as written*: a
6//! user-defined type is a bare `Named` string because the parser can't know
7//! whether it names a record, an enum, an interface, or a callback
8//! interface. [`Ty`] is the type *as resolved* by validation: every user
9//! reference carries its kind, and there is no "unresolved" variant for a
10//! backend to trip over. Type names are global (validation rejects two
11//! declarations with one name), so a [`Ty`] carries the bare name and the
12//! [`TypeIndex`] answers where it's declared.
13//!
14//! Three questions about a type are answered once here:
15//!
16//! * [`Ty::family`]: how the type crosses a **call boundary** (by value, as a
17//! pinned string, as a `(ptr, len)` byte pair, as a serialized value
18//! buffer, as an object pointer, or as a callback vtable pair). The ABI
19//! lowering, the marshalling plan, and every backend's argument and return
20//! handling dispatch on it.
21//! * [`Ty::wire`]: how the type is encoded **inside a value buffer**. Every
22//! backend's codec emitter dispatches on it.
23//! * [`Ty::contains_user_type`] and [`Ty::contains_object`]: whether encoding
24//! the type needs a user-defined codec function or object-token support.
25
26use std::collections::BTreeMap;
27use std::fmt;
28
29/// A built-in primitive type: the fixed vocabulary the IDL's
30/// [`TypeRef`](crate::ir::TypeRef) and the resolved [`Ty`] share.
31///
32/// Adding a primitive means adding a variant here; every classification
33/// ([`Ty::family`], [`Ty::wire`], the C ABI lowering) dispatches on it.
34/// Inside a value buffer each primitive is one fixed-width or
35/// length-prefixed run (see the variant docs), and every backend spells the
36/// read/write routine for one the same way modulo casing (`read_i32`,
37/// `readI32`, `ReadI32`), so [`snake`](Self::snake) and
38/// [`pascal`](Self::pascal) let a codec emitter dispatch on the whole set
39/// with one arm.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
41pub enum Prim {
42 /// Boolean (`bool`): one byte, `0` or `1`.
43 Bool,
44 /// Signed 8-bit integer (`i8`): one signed byte.
45 I8,
46 /// Signed 16-bit integer (`i16`): two bytes, little-endian.
47 I16,
48 /// Signed 32-bit integer (`i32`): four bytes, little-endian.
49 I32,
50 /// Signed 64-bit integer (`i64`): eight bytes, little-endian.
51 I64,
52 /// Unsigned 8-bit integer (`u8`): one unsigned byte.
53 U8,
54 /// Unsigned 16-bit integer (`u16`): two bytes, little-endian.
55 U16,
56 /// Unsigned 32-bit integer (`u32`): four bytes, little-endian.
57 U32,
58 /// Unsigned 64-bit integer (`u64`): eight bytes, little-endian.
59 U64,
60 /// 32-bit IEEE 754 floating-point number (`f32`): four bytes.
61 F32,
62 /// 64-bit IEEE 754 floating-point number (`f64`): eight bytes.
63 F64,
64 /// UTF-8 string (`string`): a `u32` byte length followed by UTF-8 bytes,
65 /// no NUL terminator. Borrowed as a parameter, owned as a return.
66 String,
67 /// Byte buffer (`bytes`): a `u32` length followed by raw bytes. Borrowed
68 /// as a parameter, owned as a return.
69 Bytes,
70}
71
72impl Prim {
73 /// Every primitive, for exhaustive tables in runtime preambles.
74 pub const ALL: [Prim; 13] = [
75 Prim::Bool,
76 Prim::I8,
77 Prim::I16,
78 Prim::I32,
79 Prim::I64,
80 Prim::U8,
81 Prim::U16,
82 Prim::U32,
83 Prim::U64,
84 Prim::F32,
85 Prim::F64,
86 Prim::String,
87 Prim::Bytes,
88 ];
89
90 /// The primitive an IDL spelling names (`i32`, `string`, `bytes`), or
91 /// `None` when `name` isn't a primitive.
92 #[must_use]
93 pub fn from_name(name: &str) -> Option<Prim> {
94 Prim::ALL.into_iter().find(|p| p.snake() == name)
95 }
96
97 /// The lower-case IDL spelling (`bool`, `i32`, `string`, `bytes`), also
98 /// the stem of snake-case routine names such as `read_i32`.
99 #[must_use]
100 pub fn snake(self) -> &'static str {
101 match self {
102 Prim::Bool => "bool",
103 Prim::I8 => "i8",
104 Prim::I16 => "i16",
105 Prim::I32 => "i32",
106 Prim::I64 => "i64",
107 Prim::U8 => "u8",
108 Prim::U16 => "u16",
109 Prim::U32 => "u32",
110 Prim::U64 => "u64",
111 Prim::F32 => "f32",
112 Prim::F64 => "f64",
113 Prim::String => "string",
114 Prim::Bytes => "bytes",
115 }
116 }
117
118 /// The capitalized spelling (`Bool`, `I32`, `String`, `Bytes`), the stem
119 /// of camel-case routine names such as `readI32`.
120 #[must_use]
121 pub fn pascal(self) -> &'static str {
122 match self {
123 Prim::Bool => "Bool",
124 Prim::I8 => "I8",
125 Prim::I16 => "I16",
126 Prim::I32 => "I32",
127 Prim::I64 => "I64",
128 Prim::U8 => "U8",
129 Prim::U16 => "U16",
130 Prim::U32 => "U32",
131 Prim::U64 => "U64",
132 Prim::F32 => "F32",
133 Prim::F64 => "F64",
134 Prim::String => "String",
135 Prim::Bytes => "Bytes",
136 }
137 }
138
139 /// `true` for the integer and float primitives (everything but `bool`,
140 /// `string`, and `bytes`).
141 #[must_use]
142 pub fn is_numeric(self) -> bool {
143 !matches!(self, Prim::Bool | Prim::String | Prim::Bytes)
144 }
145
146 /// `true` for the integer primitives.
147 #[must_use]
148 pub fn is_integer(self) -> bool {
149 self.is_numeric() && !matches!(self, Prim::F32 | Prim::F64)
150 }
151}
152
153impl fmt::Display for Prim {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str(self.snake())
156 }
157}
158
159/// A fully resolved type: the shape generators render and the ABI lowers.
160///
161/// User types carry their bare declared name; [`TypeIndex`] (on the
162/// [`Model`](crate::model::Model)) maps the name to its declaration.
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
164pub enum Ty {
165 /// A built-in primitive: a scalar, `bool`, `string`, or `bytes`.
166 Prim(Prim),
167 /// A user record (struct): a plain value type. Crosses the C ABI by value
168 /// as a serialized buffer (`ptr` + `len`), borrowed for a call as a
169 /// parameter and owned (freed with `{prefix}_free_bytes`) as a return.
170 Record(String),
171 /// An algebraic (rich) enum: a sum type with at least one payload-carrying
172 /// variant. A value type that crosses the C ABI as a serialized buffer
173 /// (an `i32` tag followed by the active variant's fields), exactly like a
174 /// [`Record`](Self::Record).
175 RichEnum(String),
176 /// A C-style integer enum (no variant payloads). Lowers by value.
177 Enum(String),
178 /// A user interface: a reference-counted object. As a parameter the
179 /// object is borrowed for the call; as a return (or inside a buffer) the
180 /// receiver adopts one strong reference it must eventually release.
181 Interface(String),
182 /// A callback interface: a method set the consumer implements. Only valid
183 /// as a top-level parameter (bare, or optional as `Cb?`), where it lowers
184 /// to a context pointer plus a vtable pointer.
185 CallbackInterface(String),
186 /// Optional value (`T?`): either the inner type or nothing.
187 Optional(Box<Ty>),
188 /// Homogeneous list (`[T]`) of the inner element type.
189 List(Box<Ty>),
190 /// Map (`{K:V}`) from a key type to a value type.
191 Map(Box<Ty>, Box<Ty>),
192 /// Lazy sequence (`iter<T>`) of the inner type, lowered to a next/destroy
193 /// iterator object rather than a materialized collection.
194 Iterator(Box<Ty>),
195}
196
197/// How a value of some [`Ty`] crosses a **call boundary**: the one
198/// classification the ABI lowering, the marshalling plan, and every backend's
199/// argument and return handling agree on.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum Family {
202 /// One C slot passed by value: scalars, bools, and C-style enums.
203 Direct,
204 /// A `(ptr, len)` pair of UTF-8 bytes, never NUL-terminated. Borrowed for
205 /// a call as a parameter; producer-owned and released with
206 /// `{prefix}_free_bytes` as a return.
207 String,
208 /// A `(ptr, len)` raw byte pair. Borrowed as a parameter; producer-owned
209 /// and released with `{prefix}_free_bytes` as a return.
210 Bytes,
211 /// A `(ptr, len)` serialized value buffer (record, rich enum, optional,
212 /// list, or map). Borrowed as a parameter; producer-owned and released
213 /// with `{prefix}_free_bytes` after decoding as a return.
214 Buffer,
215 /// An object pointer to an interface. Borrowed as a parameter; one
216 /// strong reference (released with its `_destroy` symbol) as a return.
217 Object {
218 /// `true` for `Interface?`: null is a legal "none" value.
219 nullable: bool,
220 },
221 /// A callback interface: a `void* ctx` plus `const {tag}_vtable*` pair.
222 /// Only ever a top-level parameter.
223 Callback {
224 /// `true` for `Cb?`: a null vtable pointer is a legal "none" value.
225 nullable: bool,
226 },
227 /// An `iter<T>` return: an opaque iterator handle with its own
228 /// `next`/`destroy` protocol. Never a parameter.
229 Iterator,
230}
231
232/// The closed set of shapes a value inside a value buffer can take.
233///
234/// This is the dispatch alphabet for every backend's buffer codec: one
235/// variant per encode/decode primitive of the wire format. The borrowed
236/// references point back into the classified [`Ty`], so classification
237/// allocates nothing.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum WireType<'a> {
240 /// A fixed-width primitive, a string, or a byte blob.
241 Prim(Prim),
242 /// An object token: eight bytes, little-endian, unsigned, holding the
243 /// object pointer and carrying one strong reference. Carries the
244 /// interface name so backends can wrap the adopted pointer in the right
245 /// class and, when writing, call the right `_clone` symbol.
246 Object(&'a str),
247 /// A C-style enum: an `i32` discriminant. Carries the enum name so
248 /// backends can wrap the integer in their typed enum.
249 Enum(&'a str),
250 /// A record or rich enum: the named type's own codec applies (fields in
251 /// declaration order for a record; an `i32` tag plus the active
252 /// variant's fields for a rich enum). Backends emit one codec function
253 /// per user type and delegate here by name.
254 User(&'a str),
255 /// A one-byte presence flag (`0` absent, `1` present) followed by the
256 /// inner value when present.
257 Optional(&'a Ty),
258 /// A `u32` element count followed by each element.
259 List(&'a Ty),
260 /// A `u32` entry count followed by alternating key and value.
261 Map(&'a Ty, &'a Ty),
262}
263
264impl Ty {
265 /// How this type crosses a call boundary. Total over every `Ty`.
266 pub fn family(&self) -> Family {
267 match self {
268 Ty::Prim(Prim::String) => Family::String,
269 Ty::Prim(Prim::Bytes) => Family::Bytes,
270 Ty::Prim(_) | Ty::Enum(_) => Family::Direct,
271 Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => Family::Buffer,
272 Ty::Interface(_) => Family::Object { nullable: false },
273 // The one optional that is not buffered: `Interface?` stays a
274 // nullable pointer so the common "maybe an object" shape needs no
275 // encoding step.
276 Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => {
277 Family::Object { nullable: true }
278 }
279 // Likewise `Cb?` stays a `(ctx, vtable)` pair with a null vtable
280 // meaning none; callback interfaces never enter a value buffer.
281 Ty::Optional(inner) if matches!(inner.as_ref(), Ty::CallbackInterface(_)) => {
282 Family::Callback { nullable: true }
283 }
284 Ty::Optional(_) => Family::Buffer,
285 Ty::CallbackInterface(_) => Family::Callback { nullable: false },
286 Ty::Iterator(_) => Family::Iterator,
287 }
288 }
289
290 /// `true` when this type crosses the C ABI as a serialized value buffer
291 /// (`const uint8_t*` + `size_t`) rather than as dedicated C slots.
292 pub fn is_buffered(&self) -> bool {
293 self.family() == Family::Buffer
294 }
295
296 /// The referenced user-type name for a record, rich enum, enum,
297 /// interface, or callback interface, or `None` for every other type.
298 pub fn user_name(&self) -> Option<&str> {
299 match self {
300 Ty::Record(n)
301 | Ty::RichEnum(n)
302 | Ty::Enum(n)
303 | Ty::Interface(n)
304 | Ty::CallbackInterface(n) => Some(n),
305 _ => None,
306 }
307 }
308
309 /// The interface name inside a bare or optional interface type, or
310 /// `None` when the type is not an object reference.
311 pub fn interface_name(&self) -> Option<&str> {
312 match self {
313 Ty::Interface(n) => Some(n),
314 Ty::Optional(inner) => inner.interface_name(),
315 _ => None,
316 }
317 }
318
319 /// The callback interface name inside a bare or optional callback
320 /// interface type, or `None` for every other type.
321 pub fn callback_interface_name(&self) -> Option<&str> {
322 match self {
323 Ty::CallbackInterface(n) => Some(n),
324 Ty::Optional(inner) => inner.callback_interface_name(),
325 _ => None,
326 }
327 }
328
329 /// Classify this type's encoding inside a value buffer.
330 ///
331 /// Total over every type validation admits inside a buffered position.
332 /// Callback interfaces and iterators never appear inside value buffers
333 /// (validation rejects them there), so those inputs are bugs in the
334 /// caller's pipeline, not user errors.
335 ///
336 /// # Panics
337 ///
338 /// Panics when `self` is a callback interface or an iterator, neither of
339 /// which can legally appear inside a value buffer.
340 pub fn wire(&self) -> WireType<'_> {
341 match self {
342 Ty::Prim(p) => WireType::Prim(*p),
343 Ty::Interface(n) => WireType::Object(n),
344 Ty::Enum(name) => WireType::Enum(name),
345 Ty::Record(name) | Ty::RichEnum(name) => WireType::User(name),
346 Ty::Optional(inner) => WireType::Optional(inner),
347 Ty::List(inner) => WireType::List(inner),
348 Ty::Map(k, v) => WireType::Map(k, v),
349 Ty::CallbackInterface(_) | Ty::Iterator(_) => {
350 panic!("{self} cannot appear inside value buffers")
351 }
352 }
353 }
354
355 /// `true` when a value of this type needs a user-defined codec function
356 /// somewhere in its encoding: it is (or transitively contains) a record
357 /// or rich enum.
358 pub fn contains_user_type(&self) -> bool {
359 self.any(&|t| matches!(t, Ty::Record(_) | Ty::RichEnum(_)))
360 }
361
362 /// `true` when this type is (or transitively contains) an interface, so
363 /// encoding it needs object-token support in the codec.
364 pub fn contains_object(&self) -> bool {
365 self.any(&|t| matches!(t, Ty::Interface(_)))
366 }
367
368 /// `true` when `pred` holds for this type or any type nested inside it
369 /// (optional payloads, list and iterator elements, map keys and values).
370 pub fn any(&self, pred: &dyn Fn(&Ty) -> bool) -> bool {
371 if pred(self) {
372 return true;
373 }
374 match self {
375 Ty::Optional(inner) | Ty::List(inner) | Ty::Iterator(inner) => inner.any(pred),
376 Ty::Map(k, v) => k.any(pred) || v.any(pred),
377 _ => false,
378 }
379 }
380
381 /// The element type of an `iter<T>`, or `None` for any other type.
382 pub fn iterator_elem(&self) -> Option<&Ty> {
383 match self {
384 Ty::Iterator(inner) => Some(inner),
385 _ => None,
386 }
387 }
388}
389
390impl fmt::Display for Ty {
391 /// Renders the IDL spelling (`i32`, `[string]`, `{string:i32}`,
392 /// `Contact?`, `iter<Contact>`), which is what diagnostics and generated
393 /// doc comments quote.
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395 match self {
396 Ty::Prim(p) => f.write_str(p.snake()),
397 Ty::Record(n)
398 | Ty::RichEnum(n)
399 | Ty::Enum(n)
400 | Ty::Interface(n)
401 | Ty::CallbackInterface(n) => f.write_str(n),
402 Ty::Optional(inner) => write!(f, "{inner}?"),
403 Ty::List(inner) => write!(f, "[{inner}]"),
404 Ty::Map(k, v) => write!(f, "{{{k}:{v}}}"),
405 Ty::Iterator(inner) => write!(f, "iter<{inner}>"),
406 }
407 }
408}
409
410/// What kind of declaration a user type name refers to.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub enum TypeKind {
413 /// A `structs:` entry.
414 Record,
415 /// An `enums:` entry with no payload-carrying variant.
416 Enum,
417 /// An `enums:` entry with at least one payload-carrying variant.
418 RichEnum,
419 /// An `interfaces:` entry.
420 Interface,
421 /// A `callback_interfaces:` entry.
422 CallbackInterface,
423}
424
425/// Where a user type is declared.
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub struct TypeDecl {
428 /// The declaration kind.
429 pub kind: TypeKind,
430 /// Position of the declaring module in
431 /// [`Model::modules`](crate::model::Model::modules).
432 pub module: usize,
433 /// Position of the declaration in its module's list for its kind
434 /// (`structs`, `enums`, `interfaces`, or `callback_interfaces`).
435 pub index: usize,
436}
437
438/// Every user type name in an API, mapped to its declaration.
439///
440/// Type names are unique across the whole API, so a bare name identifies
441/// one declaration. The [`Model`](crate::model::Model) owns the index and
442/// exposes typed lookups (`interface`, `record`, `owner`, and so on) on top
443/// of it.
444#[derive(Debug, Clone, Default, PartialEq, Eq)]
445pub struct TypeIndex {
446 decls: BTreeMap<String, TypeDecl>,
447 /// Underscore-joined C path of every module, by position.
448 module_paths: Vec<String>,
449}
450
451impl TypeIndex {
452 /// Register a module (in [`Model::modules`](crate::model::Model::modules)
453 /// order) by its underscore-joined C path, returning its position.
454 pub(crate) fn push_module(&mut self, path: String) -> usize {
455 self.module_paths.push(path);
456 self.module_paths.len() - 1
457 }
458
459 /// Record a declaration. The first declaration of a name wins;
460 /// validation reports any later one.
461 pub(crate) fn declare(&mut self, name: &str, decl: TypeDecl) {
462 self.decls.entry(name.to_string()).or_insert(decl);
463 }
464
465 /// The declaration `name` refers to, or `None` for an undeclared name.
466 pub fn get(&self, name: &str) -> Option<&TypeDecl> {
467 self.decls.get(name)
468 }
469
470 /// The kind of the declaration `name` refers to.
471 pub fn kind(&self, name: &str) -> Option<TypeKind> {
472 self.get(name).map(|d| d.kind)
473 }
474
475 /// The underscore-joined C path of the module declaring `name` (the
476 /// `outer_inner` in `{prefix}_outer_inner_Name`).
477 pub fn module_path(&self, name: &str) -> Option<&str> {
478 self.get(name).map(|d| self.module_paths[d.module].as_str())
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 const I32: Ty = Ty::Prim(Prim::I32);
487 const STRING: Ty = Ty::Prim(Prim::String);
488
489 #[test]
490 fn families_are_total_and_agree_with_the_abi_contract() {
491 assert_eq!(I32.family(), Family::Direct);
492 assert_eq!(Ty::Prim(Prim::Bool).family(), Family::Direct);
493 assert_eq!(Ty::Enum("Color".into()).family(), Family::Direct);
494 assert_eq!(STRING.family(), Family::String);
495 assert_eq!(Ty::Prim(Prim::Bytes).family(), Family::Bytes);
496 for ty in [
497 Ty::Record("C".into()),
498 Ty::RichEnum("S".into()),
499 Ty::List(Box::new(I32)),
500 Ty::List(Box::new(Ty::Interface("Store".into()))),
501 Ty::Map(Box::new(STRING), Box::new(I32)),
502 Ty::Optional(Box::new(I32)),
503 Ty::Optional(Box::new(STRING)),
504 Ty::Optional(Box::new(Ty::Record("C".into()))),
505 ] {
506 assert_eq!(ty.family(), Family::Buffer, "{ty}");
507 assert!(ty.is_buffered());
508 }
509 assert_eq!(
510 Ty::Interface("Store".into()).family(),
511 Family::Object { nullable: false }
512 );
513 assert_eq!(
514 Ty::Optional(Box::new(Ty::Interface("Store".into()))).family(),
515 Family::Object { nullable: true }
516 );
517 assert_eq!(
518 Ty::CallbackInterface("Listener".into()).family(),
519 Family::Callback { nullable: false }
520 );
521 assert_eq!(
522 Ty::Optional(Box::new(Ty::CallbackInterface("Listener".into()))).family(),
523 Family::Callback { nullable: true }
524 );
525 assert_eq!(Ty::Iterator(Box::new(I32)).family(), Family::Iterator);
526 }
527
528 #[test]
529 fn wire_shapes() {
530 for p in Prim::ALL {
531 assert_eq!(Ty::Prim(p).wire(), WireType::Prim(p));
532 }
533 assert_eq!(
534 Ty::Interface("Store".into()).wire(),
535 WireType::Object("Store")
536 );
537 assert_eq!(Ty::RichEnum("Shape".into()).wire(), WireType::User("Shape"));
538 assert_eq!(Ty::Enum("Color".into()).wire(), WireType::Enum("Color"));
539 let list = Ty::List(Box::new(I32));
540 assert_eq!(list.wire(), WireType::List(&I32));
541 }
542
543 #[test]
544 #[should_panic(expected = "cannot appear inside value buffers")]
545 fn callback_interfaces_have_no_wire_shape() {
546 Ty::CallbackInterface("Listener".into()).wire();
547 }
548
549 #[test]
550 fn containment_recurses() {
551 assert!(Ty::Record("C".into()).contains_user_type());
552 assert!(Ty::Map(
553 Box::new(STRING),
554 Box::new(Ty::Optional(Box::new(Ty::RichEnum("S".into()))))
555 )
556 .contains_user_type());
557 assert!(!Ty::List(Box::new(Ty::Enum("Color".into()))).contains_user_type());
558 assert!(Ty::List(Box::new(Ty::Interface("Store".into()))).contains_object());
559 assert!(!Ty::List(Box::new(Ty::Record("C".into()))).contains_object());
560 }
561
562 #[test]
563 fn display_is_the_idl_spelling() {
564 let ty = Ty::List(Box::new(Ty::Map(
565 Box::new(STRING),
566 Box::new(Ty::Optional(Box::new(Ty::Record("Contact".into())))),
567 )));
568 assert_eq!(ty.to_string(), "[{string:Contact?}]");
569 assert_eq!(
570 Ty::Iterator(Box::new(Ty::Interface("Store".into()))).to_string(),
571 "iter<Store>"
572 );
573 assert_eq!(Prim::String.snake(), "string");
574 assert_eq!(Prim::I32.pascal(), "I32");
575 }
576
577 #[test]
578 fn primitive_names_round_trip() {
579 for p in Prim::ALL {
580 assert_eq!(Prim::from_name(p.snake()), Some(p));
581 }
582 assert_eq!(Prim::from_name("usize"), None);
583 assert!(Prim::U8.is_integer() && !Prim::F64.is_integer() && !Prim::Bool.is_numeric());
584 }
585}