Skip to main content

polydat_grammar/
port_type.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The port type vocabulary: every type a wire, a port, a cast, or a
5//! declaration can name, with its keyword. What a type means to a
6//! compiled buffer (its slot color, width, and scratch element) is
7//! the runtime's, defined on this type by `polydat-core`.
8
9use std::fmt;
10
11/// Compile-time type tag for a port on a Polydat node.
12///
13/// **Narrow types and runtime storage:**
14///
15/// `PortType` includes narrow integer and float variants (U8/U16/U32,
16/// I8/I16/I32, F16/F32) that have no `Value` of their own. At
17/// runtime, narrow values are stored in the wide `Value` of their
18/// kind, with the assumption that the bits fit:
19///
20/// - unsigned (`u8`, `u16`, `u32`) → zero-extended in `Value::U64`
21/// - signed (`i8`, `i16`, `i32`) → sign-extended in `Value::I64`
22/// - `f32` → losslessly widened in `Value::F64` (`f16` rides
23///   `Value::U64` as its bit pattern; see [`PortType::F16`])
24///
25/// The narrow `PortType` variants exist for compile-time type
26/// checking and auto-adapter insertion (`U32ToU64`, `F32ToF64`).
27/// P2/P3 compiled kernels use flat u64 buffers where this packing
28/// is natural. The `Value` enum stays small — no combinatorial
29/// explosion of narrow variant types.
30///
31/// Every input and output port declares its `PortType`. The assembler
32/// uses these to validate wiring and auto-insert type adapters (e.g.,
33/// `u64 → f64` widening). At runtime, the corresponding `Value`
34/// variant is used.
35///
36/// **Widening rules** (auto-inserted by the assembler):
37/// - `U32 → U64`, `I32 → I64`, `F32 → F64` (lossless widening)
38/// - `U64 → F64` (lossless for values < 2^53)
39/// - `Bool → U64` (true=1, false=0)
40/// - Any type → `Str` (via display conversion)
41///
42/// **Narrowing** is never implicit — use explicit cast functions.
43///
44/// **Exhaustive on purpose.** The language grows a type now and then,
45/// and a `match` over every variant is then a compile error until it
46/// decides what the new type means. Polydat's own code relies on that,
47/// and so should a host's code that behaves differently per type. A host
48/// that only names or classifies types should not match at all: use
49/// [`Self::to_keyword`] or `Display` for a label, [`Self::from_keyword`]
50/// to parse one, and [`Self::numeric_domain`] or polydat's `SlotShape`
51/// queries to classify, none of which breaks when a type is added.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum PortType {
54    /// 64-bit unsigned integer. The primary numeric type.
55    U64,
56    /// 64-bit IEEE 754 float. Used for math, distributions, noise.
57    F64,
58    /// 32-bit unsigned integer. Widens to U64 automatically.
59    U32,
60    /// 32-bit signed integer. Widens to I64 automatically.
61    I32,
62    /// 64-bit signed integer.
63    I64,
64    /// 32-bit IEEE 754 float. Widens to F64 automatically.
65    F32,
66    /// 8-bit unsigned integer (cranelift I8 lane, unsigned
67    /// interpretation). Zero-extended in `Value::U64`; widens to
68    /// U64 automatically.
69    U8,
70    /// 8-bit signed integer (cranelift I8 lane, signed
71    /// interpretation). Sign-extended in `Value::I64`; widens to
72    /// I64 automatically.
73    I8,
74    /// 16-bit unsigned integer (cranelift I16 lane, unsigned
75    /// interpretation). Zero-extended in `Value::U64`; widens to
76    /// U64 automatically.
77    U16,
78    /// 16-bit signed integer (cranelift I16 lane, signed
79    /// interpretation). Sign-extended in `Value::I64`; widens to
80    /// I64 automatically.
81    I16,
82    /// 16-bit IEEE 754-2008 binary16 float (cranelift F16).
83    /// Carried as its bit pattern in `Value::U64` (low 16 bits),
84    /// the same stuffing convention as `F32`; widens to F32/F64
85    /// automatically (every f16 is exactly representable in both).
86    F16,
87    /// 128-bit unsigned integer (cranelift I128, unsigned
88    /// interpretation). Real `Value::U128` two-limb carrier — a
89    /// 128-bit value cannot ride a 64-bit slot. Rides two
90    /// consecutive u64 slots (a limb pair) on the compiled engines.
91    U128,
92    /// 128-bit signed integer (cranelift I128, signed
93    /// interpretation). Same carrier story as `U128`.
94    I128,
95    /// 128-bit SIMD register word, raw view — the full word as
96    /// algorithm-defined buffer state (heterogeneous lane
97    /// roles). Free bitcast to/from every lane-typed view.
98    Reg128,
99    /// Register word viewed as 16 × i8 lanes.
100    RegI8x16,
101    /// Register word viewed as 8 × i16 lanes.
102    RegI16x8,
103    /// Register word viewed as 4 × i32 lanes.
104    RegI32x4,
105    /// Register word viewed as 2 × i64 lanes.
106    RegI64x2,
107    /// Register word viewed as 8 × f16 lanes.
108    RegF16x8,
109    /// Register word viewed as 4 × f32 lanes.
110    RegF32x4,
111    /// Register word viewed as 2 × f64 lanes.
112    RegF64x2,
113    /// Boolean (true/false). Widens to U64 (1/0).
114    Bool,
115    /// Heap-allocated string. Any type auto-converts to Str.
116    Str,
117    /// Raw byte buffer.
118    Bytes,
119    /// Structured JSON value.
120    Json,
121    /// Adapter-contributed reflected type (e.g., CQL UUID).
122    Ext,
123    /// Type-erased Arc handle to a resolved resource (dataset,
124    /// prepared statement, ...). The producer node populates an
125    /// `Arc<dyn Any + Send + Sync>`; the consumer node downcasts to
126    /// the concrete type via `Value::as_handle::<T>()`.
127    Handle,
128    /// Typed `f32` vector slice (`Arc<[f32]>`). Bound natively by
129    /// adapters that understand `[f32]` (CQL `vector<float, N>`).
130    VecF32,
131    /// Typed `i32` vector slice (`Arc<[i32]>`).
132    VecI32,
133    /// Typed `f64` vector slice (`Arc<[f64]>`). Bound natively
134    /// for CQL `vector<double, N>`.
135    VecF64,
136    /// Typed `i64` vector slice (`Arc<[i64]>`). Bound natively
137    /// for CQL `vector<bigint, N>`.
138    VecI64,
139    /// Typed half-precision float vector (`Arc<[half::f16]>`).
140    /// Bound natively for CQL `vector<half_float, N>`-style
141    /// columns; stays at f16 on the wire so embeddings stored
142    /// as 16-bit floats don't widen to f32 at the boundary.
143    VecF16,
144    /// Typed `i16` vector slice (`Arc<[i16]>`). Bound natively
145    /// for CQL `vector<smallint, N>`.
146    VecI16,
147    /// Typed `i8` vector slice (`Arc<[i8]>`). Completes the
148    /// cranelift lane family; CQL `vector<tinyint, N>`.
149    VecI8,
150    /// Any value, as written: the slot of an input whose type may vary
151    /// over a kernel's lifetime (input_variance.md). Only a converter
152    /// node reads it, turning the value into the type its consumers
153    /// read; no other port has this type, and no value is typed `Dyn`.
154    Dyn,
155}
156
157impl fmt::Display for PortType {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            PortType::U64 => write!(f, "u64"),
161            PortType::F64 => write!(f, "f64"),
162            PortType::U32 => write!(f, "u32"),
163            PortType::I32 => write!(f, "i32"),
164            PortType::I64 => write!(f, "i64"),
165            PortType::F32 => write!(f, "f32"),
166            PortType::U8 => write!(f, "u8"),
167            PortType::I8 => write!(f, "i8"),
168            PortType::U16 => write!(f, "u16"),
169            PortType::I16 => write!(f, "i16"),
170            PortType::F16 => write!(f, "f16"),
171            PortType::U128 => write!(f, "u128"),
172            PortType::I128 => write!(f, "i128"),
173            PortType::Reg128 => write!(f, "reg128"),
174            PortType::RegI8x16 => write!(f, "reg_i8x16"),
175            PortType::RegI16x8 => write!(f, "reg_i16x8"),
176            PortType::RegI32x4 => write!(f, "reg_i32x4"),
177            PortType::RegI64x2 => write!(f, "reg_i64x2"),
178            PortType::RegF16x8 => write!(f, "reg_f16x8"),
179            PortType::RegF32x4 => write!(f, "reg_f32x4"),
180            PortType::RegF64x2 => write!(f, "reg_f64x2"),
181            PortType::Bool => write!(f, "bool"),
182            PortType::Str => write!(f, "String"),
183            PortType::Bytes => write!(f, "bytes"),
184            PortType::Json => write!(f, "json"),
185            PortType::Ext => write!(f, "ext"),
186            PortType::Handle => write!(f, "handle"),
187            PortType::VecF32 => write!(f, "vec_f32"),
188            PortType::VecI32 => write!(f, "vec_i32"),
189            PortType::VecF64 => write!(f, "vec_f64"),
190            PortType::VecI64 => write!(f, "vec_i64"),
191            PortType::VecF16 => write!(f, "vec_f16"),
192            PortType::VecI16 => write!(f, "vec_i16"),
193            PortType::VecI8 => write!(f, "vec_i8"),
194            PortType::Dyn => write!(f, "dyn"),
195        }
196    }
197}
198
199/// The set of numbers a scalar port type can carry, described by the
200/// properties that decide whether one of them holds every value of
201/// another: how many bits the representation has, and how it spends
202/// them.
203///
204/// `Bool` is the one-bit unsigned domain, which is what the type
205/// system already says of it — it widens to `U64` as 1 and 0.
206/// Non-scalar types (`Str`, `Bytes`, `Json`, the vectors, the
207/// register views, `Ext`, `Handle`) have no domain.
208#[derive(Clone, Copy, PartialEq, Eq, Debug)]
209pub enum NumericDomain {
210    /// Non-negative integers in `0 ..= 2^bits - 1`.
211    Unsigned {
212        /// Width of the representation.
213        bits: u32,
214    },
215    /// Two's-complement integers, one of whose bits is the sign.
216    Signed {
217        /// Width of the representation, sign bit included.
218        bits: u32,
219    },
220    /// An IEEE 754 binary float.
221    Float {
222        /// Significand bits, the implicit leading one included: the
223        /// largest integer represented exactly is `2^mantissa`.
224        mantissa: u32,
225        /// Exponent bits, which fix the magnitude range.
226        exponent: u32,
227    },
228}
229
230impl NumericDomain {
231    /// Whether every value of this domain is a value of `other` —
232    /// that is, whether a conversion into `other` is lossless.
233    ///
234    /// Integers fit by counting the bits each spends on magnitude: an
235    /// unsigned domain needs a signed one strictly wider, a signed
236    /// domain never fits an unsigned one. An integer fits a float
237    /// when its magnitude bits fit the float's significand, which is
238    /// why `U64 → F64` does not: 64 magnitude bits do not fit 53, and
239    /// the values above `2^53` round. A float fits a wider float when
240    /// both its significand and its exponent do.
241    pub fn fits_in(self, other: Self) -> bool {
242        use NumericDomain::{Float, Signed, Unsigned};
243        match (self, other) {
244            (Unsigned { bits: a }, Unsigned { bits: b }) => a <= b,
245            (Unsigned { bits: a }, Signed { bits: b }) => a < b,
246            (Signed { bits: a }, Signed { bits: b }) => a <= b,
247            (Signed { .. }, Unsigned { .. }) => false,
248            (Unsigned { bits }, Float { mantissa, .. }) => bits <= mantissa,
249            (Signed { bits }, Float { mantissa, .. }) => bits - 1 <= mantissa,
250            (Float { .. }, Unsigned { .. } | Signed { .. }) => false,
251            (
252                Float {
253                    mantissa: m1,
254                    exponent: e1,
255                },
256                Float {
257                    mantissa: m2,
258                    exponent: e2,
259                },
260            ) => m1 <= m2 && e1 <= e2,
261        }
262    }
263}
264
265impl PortType {
266    /// Every port type, once.
267    ///
268    /// A test that must hold for *all* types can walk this rather than
269    /// name the ones its author thought of. That is the difference
270    /// between a mapping that is complete and one that is complete so
271    /// far: the value↔slot writers were extended three times by finding
272    /// a type they had missed at run time, each found by a program that
273    /// happened to use it (2026-09-22).
274    ///
275    /// [`Self::every_variant_is_listed`] keeps this honest — it is an
276    /// exhaustive `match`, so adding a variant without adding it here
277    /// fails to compile rather than quietly shrinking every sweep that
278    /// walks this list.
279    pub const ALL: &'static [PortType] = &[
280        PortType::U64,
281        PortType::F64,
282        PortType::U32,
283        PortType::I32,
284        PortType::I64,
285        PortType::F32,
286        PortType::U8,
287        PortType::I8,
288        PortType::U16,
289        PortType::I16,
290        PortType::F16,
291        PortType::U128,
292        PortType::I128,
293        PortType::Reg128,
294        PortType::RegI8x16,
295        PortType::RegI16x8,
296        PortType::RegI32x4,
297        PortType::RegI64x2,
298        PortType::RegF16x8,
299        PortType::RegF32x4,
300        PortType::RegF64x2,
301        PortType::Bool,
302        PortType::Str,
303        PortType::Bytes,
304        PortType::Json,
305        PortType::Ext,
306        PortType::Handle,
307        PortType::VecF32,
308        PortType::VecI32,
309        PortType::VecF64,
310        PortType::VecI64,
311        PortType::VecF16,
312        PortType::VecI16,
313        PortType::VecI8,
314        PortType::Dyn,
315    ];
316
317    /// `true` for every variant, by an exhaustive match: the compiler
318    /// refuses this function when a variant is added, and the test
319    /// beside it checks [`Self::ALL`] carries the one that was added.
320    #[doc(hidden)]
321    pub fn every_variant_is_listed(self) -> bool {
322        match self {
323            PortType::U64
324            | PortType::F64
325            | PortType::U32
326            | PortType::I32
327            | PortType::I64
328            | PortType::F32
329            | PortType::U8
330            | PortType::I8
331            | PortType::U16
332            | PortType::I16
333            | PortType::F16
334            | PortType::U128
335            | PortType::I128
336            | PortType::Reg128
337            | PortType::RegI8x16
338            | PortType::RegI16x8
339            | PortType::RegI32x4
340            | PortType::RegI64x2
341            | PortType::RegF16x8
342            | PortType::RegF32x4
343            | PortType::RegF64x2
344            | PortType::Bool
345            | PortType::Str
346            | PortType::Bytes
347            | PortType::Json
348            | PortType::Ext
349            | PortType::Handle
350            | PortType::VecF32
351            | PortType::VecI32
352            | PortType::VecF64
353            | PortType::VecI64
354            | PortType::VecF16
355            | PortType::VecI16
356            | PortType::VecI8
357            | PortType::Dyn => Self::ALL.contains(&self),
358        }
359    }
360
361    /// The numbers this type can carry, for the types that carry
362    /// numbers. `None` for every other type.
363    ///
364    /// This is what decides whether a conversion between two types
365    /// keeps the value, so that the answer is read off the types
366    /// themselves rather than kept in a list of pairs beside them.
367    pub fn numeric_domain(self) -> Option<NumericDomain> {
368        use NumericDomain::{Float, Signed, Unsigned};
369        Some(match self {
370            Self::Bool => Unsigned { bits: 1 },
371            Self::U8 => Unsigned { bits: 8 },
372            Self::U16 => Unsigned { bits: 16 },
373            Self::U32 => Unsigned { bits: 32 },
374            Self::U64 => Unsigned { bits: 64 },
375            Self::U128 => Unsigned { bits: 128 },
376            Self::I8 => Signed { bits: 8 },
377            Self::I16 => Signed { bits: 16 },
378            Self::I32 => Signed { bits: 32 },
379            Self::I64 => Signed { bits: 64 },
380            Self::I128 => Signed { bits: 128 },
381            Self::F16 => Float {
382                mantissa: 11,
383                exponent: 5,
384            },
385            Self::F32 => Float {
386                mantissa: 24,
387                exponent: 8,
388            },
389            Self::F64 => Float {
390                mantissa: 53,
391                exponent: 11,
392            },
393            Self::Str
394            | Self::Bytes
395            | Self::Json
396            | Self::Ext
397            | Self::Handle
398            | Self::Reg128
399            | Self::RegI8x16
400            | Self::RegI16x8
401            | Self::RegI32x4
402            | Self::RegI64x2
403            | Self::RegF16x8
404            | Self::RegF32x4
405            | Self::RegF64x2
406            | Self::VecF32
407            | Self::VecI32
408            | Self::VecF64
409            | Self::VecI64
410            | Self::VecF16
411            | Self::VecI16
412            | Self::VecI8
413            | Self::Dyn => return None,
414        })
415    }
416
417    /// The canonical lowercase keyword for this `PortType`.
418    ///
419    /// This is the single source of truth for the str↔PortType
420    /// mapping used by every synthesizer and parser in the
421    /// workspace — synthesized polydat source (`extern <name>:
422    /// <keyword>`), the workload-author `{name:<keyword>}` lvalue
423    /// spec, and reverse parsing via [`Self::from_keyword`].
424    /// Inverse of [`Self::from_keyword`].
425    ///
426    /// Exhaustive over the enum — adding a new `PortType` variant
427    /// is a compile error here, forcing the addition of its
428    /// canonical keyword and the round-trip closure to update.
429    pub fn to_keyword(&self) -> &'static str {
430        match self {
431            Self::U64 => "u64",
432            Self::F64 => "f64",
433            Self::U32 => "u32",
434            Self::I32 => "i32",
435            Self::I64 => "i64",
436            Self::F32 => "f32",
437            Self::U8 => "u8",
438            Self::I8 => "i8",
439            Self::U16 => "u16",
440            Self::I16 => "i16",
441            Self::F16 => "f16",
442            Self::U128 => "u128",
443            Self::I128 => "i128",
444            Self::Reg128 => "reg128",
445            Self::RegI8x16 => "reg_i8x16",
446            Self::RegI16x8 => "reg_i16x8",
447            Self::RegI32x4 => "reg_i32x4",
448            Self::RegI64x2 => "reg_i64x2",
449            Self::RegF16x8 => "reg_f16x8",
450            Self::RegF32x4 => "reg_f32x4",
451            Self::RegF64x2 => "reg_f64x2",
452            Self::Bool => "bool",
453            Self::Str => "str",
454            Self::Bytes => "bytes",
455            Self::Json => "json",
456            Self::Ext => "ext",
457            Self::Handle => "handle",
458            Self::VecF32 => "vec_f32",
459            Self::VecI32 => "vec_i32",
460            Self::VecF64 => "vec_f64",
461            Self::VecI64 => "vec_i64",
462            Self::VecF16 => "vec_f16",
463            Self::VecI16 => "vec_i16",
464            Self::VecI8 => "vec_i8",
465            Self::Dyn => "dyn",
466        }
467    }
468
469    /// Parse a polydat type keyword into a `PortType`.
470    ///
471    /// Inverse of [`Self::to_keyword`]: accepts every keyword
472    /// that `to_keyword` emits, plus a small set of legacy aliases
473    /// (`"String"`, `"Json"`, `"Ext"`) that survive in older
474    /// hand-written workload source. Returns `None` for any
475    /// unrecognized keyword so callers can surface a loud
476    /// diagnostic rather than silently coercing to a default.
477    ///
478    /// Used by the DSL `extern <name>: <keyword>` parser
479    /// (`polydat-core/src/dsl/compile.rs`). Round-trips cleanly with
480    /// any source `to_keyword` emits.
481    pub fn from_keyword(name: &str) -> Option<Self> {
482        match name {
483            "u64" => Some(Self::U64),
484            "f64" => Some(Self::F64),
485            "u32" => Some(Self::U32),
486            "i32" => Some(Self::I32),
487            "i64" => Some(Self::I64),
488            "f32" => Some(Self::F32),
489            "u8" => Some(Self::U8),
490            "i8" => Some(Self::I8),
491            "u16" => Some(Self::U16),
492            "i16" => Some(Self::I16),
493            "f16" => Some(Self::F16),
494            "u128" => Some(Self::U128),
495            "i128" => Some(Self::I128),
496            "reg128" => Some(Self::Reg128),
497            "reg_i8x16" => Some(Self::RegI8x16),
498            "reg_i16x8" => Some(Self::RegI16x8),
499            "reg_i32x4" => Some(Self::RegI32x4),
500            "reg_i64x2" => Some(Self::RegI64x2),
501            "reg_f16x8" => Some(Self::RegF16x8),
502            "reg_f32x4" => Some(Self::RegF32x4),
503            "reg_f64x2" => Some(Self::RegF64x2),
504            "bool" => Some(Self::Bool),
505            "str" | "Str" | "String" => Some(Self::Str),
506            "bytes" => Some(Self::Bytes),
507            "json" | "Json" => Some(Self::Json),
508            "ext" | "Ext" => Some(Self::Ext),
509            "handle" => Some(Self::Handle),
510            "vec_f32" => Some(Self::VecF32),
511            "vec_i32" => Some(Self::VecI32),
512            "vec_f64" => Some(Self::VecF64),
513            "vec_i64" => Some(Self::VecI64),
514            "vec_f16" => Some(Self::VecF16),
515            "vec_i16" => Some(Self::VecI16),
516            "vec_i8" => Some(Self::VecI8),
517            "dyn" => Some(Self::Dyn),
518            _ => None,
519        }
520    }
521
522    /// Workload-author-facing parser for the `{name:<keyword>}`
523    /// lvalue-spec surface. Strict subset of [`Self::from_keyword`]
524    /// — `handle` and `ext` are rejected because they're
525    /// internal-only types a workload author should never assert.
526    ///
527    /// Returns `None` for any unrecognized name; the caller
528    /// surfaces the unknown spec as a workload-shape diagnostic.
529    pub fn from_workload_name(name: &str) -> Option<Self> {
530        match Self::from_keyword(name)? {
531            Self::Handle | Self::Ext => None,
532            pt => Some(pt),
533        }
534    }
535}