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