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