polydat_core/derive_support.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trait surface that the `#[polydat_node]` proc-macro
5//! (`polydat-derive`) calls into for boxing / unboxing wire
6//! values.
7//!
8//! ## Canonical trait surface (library_catalog.md "Shapes")
9//!
10//! - [`Wire`] — `Sized + 'static` Rust-type ↔ [`Value`] bridge.
11//! Owned types only; the macro recognises borrow shapes
12//! (`&str`, `&[u8]`, `&[T]`, `&serde_json::Value`)
13//! syntactically and emits direct `match`-on-`Value`
14//! extraction at the eval call site — no trait dispatch, no
15//! `unsafe` lifetime transmute.
16//!
17//! Combinator [`Wire`] impls cover [`Option<T>`] (`None`-aware
18//! pass-through) and [`Ext<T>`] (downcast through
19//! [`ReflectedValue`]).
20//!
21//! A `Const<T>` position is not a `Wire`: the macro classifies it
22//! syntactically into a `ConstShape` and emits the extraction from
23//! the `ConstArg` inline, so there is no trait for it to dispatch
24//! through.
25//!
26//! ## Why the trait surface lives here
27//!
28//! `polydat-derive` is a proc-macro crate — it can't define
29//! traits that are visible at the call site, only emit token
30//! streams referencing traits defined elsewhere. The macro
31//! emits `<T as polydat::derive_support::Wire>::extract(...)`
32//! paths; this module is what those paths resolve to.
33
34use std::sync::Arc;
35
36use crate::ast::SlotShape;
37use crate::ast::{PortType, ReflectedValue, SliceArc, Value};
38
39// =====================================================================
40// Wire — Rust-type ↔ Value bridge (owned types only)
41// =====================================================================
42
43/// Rust-type ↔ `Value` bridge.
44///
45/// Every owned Rust type the macro accepts in a wire position
46/// implements this trait. `PORT` is the static [`PortType`] the
47/// DSL type-checker uses to route a wire to this slot; which slots
48/// a value rides and how wide they are follows from that port type
49/// through [`SlotShape`], the one place that mapping lives. A second
50/// `JIT: Option<JitType>` const here said the same thing about the
51/// compiled buffer and nothing ever read it.
52///
53/// Borrow shapes (`&str`, `&[u8]`, `&[T]`,
54/// `&serde_json::Value`) and polymorphic `Value`-typed wires
55/// are NOT covered here — the macro recognises them
56/// syntactically and emits direct `match`-on-`Value` extraction
57/// at the eval call site. This keeps the trait surface free of
58/// lifetime parameters.
59///
60/// `extract` panics through [`extract_mismatch`] on a value its
61/// port type does not carry. The compiler's typing routes only
62/// well-typed `Value`s to each slot before `eval` runs, so the
63/// panic marks an internal error, never a workload error.
64pub trait Wire: Sized + 'static {
65 /// Static port type for the DSL type-checker.
66 const PORT: PortType;
67
68 /// Auto-resolver (library_catalog.md "Shapes", `Resolved<R, T>`)
69 /// for `Str`-typed upstream wires feeding this slot. `None`
70 /// (the default) disables auto-promotion; the workload must
71 /// supply the wire's actual port type directly. Set via the
72 /// [`Resolved<R, T>`] marker wrapper.
73 const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = None;
74
75 /// Cost class for this wire (library_catalog.md "Wire cost classes").
76 /// Defaults to [`WireCost::Data`](crate::ast::WireCost::Data) (cheap per-cycle input).
77 /// Set to [`WireCost::Config`](crate::ast::WireCost::Config) via the [`Config<T>`] marker
78 /// wrapper to signal that the wire is rarely-changing and
79 /// the compiler should warn on cycle-time binding.
80 const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Data;
81
82 /// Pull a typed value out of a `Value` wire.
83 fn extract(v: &Value) -> Self;
84
85 /// Push a typed value back into the `Value` outputs stream.
86 fn inject(self) -> Value;
87}
88
89/// The failure of a [`Wire::extract`] handed a value of a type its port
90/// does not carry. The compiler's typing routes only values of a port's
91/// type to that port, so reaching this is an internal error rather than
92/// a workload error. The message names the wire's Rust type, the port
93/// type, and the type of the value received; the engine that catches
94/// the panic adds the failing node's name (engines.md §3.4).
95#[cold]
96#[inline(never)]
97pub fn extract_mismatch(wire: &str, expected: PortType, got: &Value) -> ! {
98 panic!(
99 "Wire<{wire}>::extract: expected {expected}, got {}; the compiler's \
100 typing admits only {expected} to this port, so this is an internal error",
101 got.type_name()
102 )
103}
104
105// ── Scalar primitives ─────────────────────────────────────────
106
107impl Wire for u64 {
108 const PORT: PortType = PortType::U64;
109 fn extract(v: &Value) -> Self {
110 v.as_u64()
111 }
112 fn inject(self) -> Value {
113 Value::U64(self)
114 }
115}
116
117impl Wire for u32 {
118 const PORT: PortType = PortType::U32;
119 fn extract(v: &Value) -> Self {
120 v.as_u64() as u32
121 }
122 fn inject(self) -> Value {
123 Value::U64(self as u64)
124 }
125}
126
127impl Wire for i32 {
128 const PORT: PortType = PortType::I32;
129 // Lenient extract: an honest `Value::I64` (sign-extended I32
130 // storage convention) and the bit-stuffed `Value::U64` form that
131 // `inject` writes — same precedent as `Wire<bool>` accepting
132 // `U64(n != 0)`.
133 fn extract(v: &Value) -> Self {
134 v.as_i64() as i32
135 }
136 fn inject(self) -> Value {
137 Value::I64(self as i64)
138 }
139}
140
141impl Wire for i64 {
142 const PORT: PortType = PortType::I64;
143 // Lenient extract: see `Wire<i32>` note above.
144 fn extract(v: &Value) -> Self {
145 v.as_i64()
146 }
147 fn inject(self) -> Value {
148 Value::I64(self)
149 }
150}
151
152impl Wire for u8 {
153 const PORT: PortType = PortType::U8;
154 fn extract(v: &Value) -> Self {
155 v.as_u64() as u8
156 }
157 fn inject(self) -> Value {
158 Value::U64(self as u64)
159 }
160}
161
162impl Wire for u16 {
163 const PORT: PortType = PortType::U16;
164 fn extract(v: &Value) -> Self {
165 v.as_u64() as u16
166 }
167 fn inject(self) -> Value {
168 Value::U64(self as u64)
169 }
170}
171
172impl Wire for i8 {
173 const PORT: PortType = PortType::I8;
174 // Lenient extract through as_i64 (honest I64 or bit-stuffed
175 // U64), narrowed by truncation — sign survives
176 // because the storage convention is sign-extension.
177 fn extract(v: &Value) -> Self {
178 v.as_i64() as i8
179 }
180 fn inject(self) -> Value {
181 Value::I64(self as i64)
182 }
183}
184
185impl Wire for i16 {
186 const PORT: PortType = PortType::I16;
187 fn extract(v: &Value) -> Self {
188 v.as_i64() as i16
189 }
190 fn inject(self) -> Value {
191 Value::I64(self as i64)
192 }
193}
194
195impl Wire for u128 {
196 const PORT: PortType = PortType::U128;
197 // No named native lowering: a 128-bit value cannot ride the
198 // one-u64 JIT slot, so it crosses the compiled tiers as a limb
199 // pair (`Imm2`) and a node over it runs its closure on the
200 // closure tier and a slot call of its kit on the native engines
201 // (type_system_alignment.md §2). The two-slot ride is about the
202 // register, not about which engines carry the type — every one
203 // of them does, which `the_128_bit_carriers_agree_on_every_engine`
204 // pins.
205 fn extract(v: &Value) -> Self {
206 v.as_u128()
207 }
208 fn inject(self) -> Value {
209 Value::U128(crate::ast::Bits128::from_u128(self))
210 }
211}
212
213impl Wire for i128 {
214 const PORT: PortType = PortType::I128;
215 fn extract(v: &Value) -> Self {
216 v.as_i128()
217 }
218 fn inject(self) -> Value {
219 Value::I128(crate::ast::Bits128::from_i128(self))
220 }
221}
222
223// ── 128-bit register words (type_system_alignment.md §8.4 L2) ──
224//
225// The raw view extracts/injects the word itself; the lane-typed
226// `[T; N]` views extract through the free-bitcast rule (any
227// register view satisfies any register slot) and inject tagged
228// with their own lane typing.
229
230impl Wire for crate::ast::Bits128 {
231 const PORT: PortType = PortType::Reg128;
232 fn extract(v: &Value) -> Self {
233 v.as_reg_bits()
234 }
235 fn inject(self) -> Value {
236 Value::Reg128(self, crate::ast::RegLanes::Raw)
237 }
238}
239
240macro_rules! impl_wire_reg {
241 ($arr:ty, $port:ident, $view:ident, $to:ident, $from:ident) => {
242 impl Wire for $arr {
243 const PORT: PortType = PortType::$port;
244 fn extract(v: &Value) -> Self {
245 v.as_reg_bits().$to()
246 }
247 fn inject(self) -> Value {
248 Value::Reg128(
249 crate::ast::Bits128::$from(self),
250 crate::ast::RegLanes::$view,
251 )
252 }
253 }
254 };
255}
256
257impl_wire_reg!([i8; 16], RegI8x16, I8x16, lanes_i8, from_lanes_i8);
258impl_wire_reg!([i16; 8], RegI16x8, I16x8, lanes_i16, from_lanes_i16);
259impl_wire_reg!([i32; 4], RegI32x4, I32x4, lanes_i32, from_lanes_i32);
260impl_wire_reg!([i64; 2], RegI64x2, I64x2, lanes_i64, from_lanes_i64);
261impl_wire_reg!([half::f16; 8], RegF16x8, F16x8, lanes_f16, from_lanes_f16);
262impl_wire_reg!([f32; 4], RegF32x4, F32x4, lanes_f32, from_lanes_f32);
263impl_wire_reg!([f64; 2], RegF64x2, F64x2, lanes_f64, from_lanes_f64);
264
265impl Wire for f64 {
266 const PORT: PortType = PortType::F64;
267 fn extract(v: &Value) -> Self {
268 v.as_f64()
269 }
270 fn inject(self) -> Value {
271 Value::F64(self)
272 }
273}
274
275impl Wire for f32 {
276 const PORT: PortType = PortType::F32;
277 fn extract(v: &Value) -> Self {
278 f32::from_bits(v.as_u64() as u32)
279 }
280 fn inject(self) -> Value {
281 Value::U64(self.to_bits() as u64)
282 }
283}
284
285impl Wire for half::f16 {
286 const PORT: PortType = PortType::F16;
287 // Same bit-stuffing convention as f32: the binary16 pattern
288 // rides the low 16 bits of the u64 carrier.
289 fn extract(v: &Value) -> Self {
290 half::f16::from_bits(v.as_u64() as u16)
291 }
292 fn inject(self) -> Value {
293 Value::U64(self.to_bits() as u64)
294 }
295}
296
297impl Wire for bool {
298 const PORT: PortType = PortType::Bool;
299 fn extract(v: &Value) -> Self {
300 match v {
301 Value::Bool(b) => *b,
302 Value::U64(n) => *n != 0,
303 other => extract_mismatch("bool", Self::PORT, other),
304 }
305 }
306 fn inject(self) -> Value {
307 Value::Bool(self)
308 }
309}
310
311impl Wire for String {
312 const PORT: PortType = PortType::Str;
313 fn extract(v: &Value) -> Self {
314 // A non-Str input is an internal error, not a coercion
315 // opportunity. Nodes that want a display rendering of an
316 // arbitrary `Value` take a `Value`-typed (PolyWire) arg
317 // instead.
318 match v {
319 Value::Str(s) => s.to_string(),
320 other => extract_mismatch("String", Self::PORT, other),
321 }
322 }
323 fn inject(self) -> Value {
324 Value::Str(self.into())
325 }
326}
327
328/// `Arc<str>` — zero-copy shared string handle. Reading
329/// extracts the existing `Arc<str>` from `Value::Str` (refcount
330/// bump only); injecting wraps directly. Nodes whose hot path
331/// emits the same string per cycle (lookup table outputs,
332/// fixed-value selectors) should use this instead of `String`
333/// to avoid the per-cycle `to_string()` allocation.
334impl Wire for std::sync::Arc<str> {
335 const PORT: PortType = PortType::Str;
336 fn extract(v: &Value) -> Self {
337 match v {
338 Value::Str(s) => s.clone(),
339 other => extract_mismatch("Arc<str>", Self::PORT, other),
340 }
341 }
342 fn inject(self) -> Value {
343 Value::Str(self)
344 }
345}
346
347/// `Arc<dyn Any + Send + Sync>` — opaque Handle wire. The body
348/// receives the runtime-typed handle directly; downcast is the
349/// operator's responsibility. Use [`Resolved<R, T>`] when the
350/// node wants a typed Handle with source-string auto-promotion; use this raw shape when the body
351/// needs to handle multiple inner types via runtime dispatch.
352impl Wire for std::sync::Arc<dyn std::any::Any + Send + Sync> {
353 const PORT: PortType = PortType::Handle;
354 fn extract(v: &Value) -> Self {
355 match v {
356 Value::Handle(arc) => arc.clone(),
357 other => extract_mismatch("Arc<dyn Any>", Self::PORT, other),
358 }
359 }
360 fn inject(self) -> Value {
361 Value::Handle(self)
362 }
363}
364
365/// `Box<dyn ReflectedValue>` — Ext (adapter-typed) wire with
366/// dynamic downcast left to the body. Use [`Ext<T>`] when the
367/// inner type is known at codegen; use this when a node needs
368/// to dispatch on the runtime ReflectedValue::type_name.
369impl Wire for Box<dyn ReflectedValue> {
370 const PORT: PortType = PortType::Ext;
371 fn extract(v: &Value) -> Self {
372 match v {
373 Value::Ext(b) => b.clone_reflected(),
374 other => extract_mismatch("Box<dyn ReflectedValue>", Self::PORT, other),
375 }
376 }
377 fn inject(self) -> Value {
378 Value::Ext(self)
379 }
380}
381
382// ── Bytes ──────────────────────────────────────────────────────
383
384impl Wire for Arc<[u8]> {
385 const PORT: PortType = PortType::Bytes;
386 fn extract(v: &Value) -> Self {
387 match v {
388 Value::Bytes(b) => b.clone(),
389 other => extract_mismatch("Arc<[u8]>", Self::PORT, other),
390 }
391 }
392 fn inject(self) -> Value {
393 Value::Bytes(self)
394 }
395}
396
397impl Wire for Vec<u8> {
398 const PORT: PortType = PortType::Bytes;
399 fn extract(v: &Value) -> Self {
400 match v {
401 Value::Bytes(b) => b.to_vec(),
402 other => extract_mismatch("Vec<u8>", Self::PORT, other),
403 }
404 }
405 fn inject(self) -> Value {
406 Value::Bytes(self.into())
407 }
408}
409
410// ── Json ───────────────────────────────────────────────────────
411
412impl Wire for Arc<serde_json::Value> {
413 const PORT: PortType = PortType::Json;
414 fn extract(v: &Value) -> Self {
415 match v {
416 Value::Json(j) => j.clone(),
417 other => extract_mismatch("Arc<Json>", Self::PORT, other),
418 }
419 }
420 fn inject(self) -> Value {
421 Value::Json(self)
422 }
423}
424
425// ── Typed-element vectors ──────────────────────────────────────
426
427macro_rules! impl_wire_vec {
428 ($elem:ty, $variant:ident, $port:ident) => {
429 impl Wire for SliceArc<$elem> {
430 const PORT: PortType = PortType::$port;
431 fn extract(v: &Value) -> Self {
432 match v {
433 Value::$variant(arc) => arc.clone(),
434 other => extract_mismatch(
435 concat!("SliceArc<", stringify!($elem), ">"),
436 Self::PORT,
437 other,
438 ),
439 }
440 }
441 fn inject(self) -> Value {
442 Value::$variant(self)
443 }
444 }
445
446 impl Wire for Vec<$elem> {
447 const PORT: PortType = PortType::$port;
448 fn extract(v: &Value) -> Self {
449 match v {
450 Value::$variant(arc) => arc.as_slice().to_vec(),
451 other => {
452 extract_mismatch(concat!("Vec<", stringify!($elem), ">"), Self::PORT, other)
453 }
454 }
455 }
456 fn inject(self) -> Value {
457 Value::$variant(SliceArc::from_vec(self))
458 }
459 }
460 };
461}
462
463impl_wire_vec!(f32, VecF32, VecF32);
464impl_wire_vec!(i32, VecI32, VecI32);
465impl_wire_vec!(f64, VecF64, VecF64);
466impl_wire_vec!(i64, VecI64, VecI64);
467impl_wire_vec!(half::f16, VecF16, VecF16);
468impl_wire_vec!(i16, VecI16, VecI16);
469impl_wire_vec!(i8, VecI8, VecI8);
470
471// ── Combinators ────────────────────────────────────────────────
472
473/// None-aware wire combinator. Macro auto-emits
474/// `accepts_none_inputs() -> true` when any arg is `Option<_>`.
475impl<T: Wire> Wire for Option<T> {
476 const PORT: PortType = T::PORT;
477 fn extract(v: &Value) -> Self {
478 match v {
479 Value::None => None,
480 _ => Some(T::extract(v)),
481 }
482 }
483 fn inject(self) -> Value {
484 match self {
485 None => Value::None,
486 Some(t) => t.inject(),
487 }
488 }
489}
490
491/// Operator-side wrapper for adapter-typed wire arguments.
492/// `Ext<T>` signals "this arg comes from `Value::Ext(Box<dyn
493/// ReflectedValue>)`; downcast it to `T`." Implements `Deref` /
494/// `DerefMut` like [`Const<T>`] so the body can use `.method()`
495/// directly.
496#[derive(Clone)]
497pub struct Ext<T>(pub T);
498
499impl<T> std::ops::Deref for Ext<T> {
500 type Target = T;
501 fn deref(&self) -> &T {
502 &self.0
503 }
504}
505
506impl<T> std::ops::DerefMut for Ext<T> {
507 fn deref_mut(&mut self) -> &mut T {
508 &mut self.0
509 }
510}
511
512impl<T: ReflectedValue + Clone + 'static> Wire for Ext<T> {
513 const PORT: PortType = PortType::Ext;
514 fn extract(v: &Value) -> Self {
515 match v {
516 Value::Ext(boxed) => {
517 let any = boxed.as_any();
518 match any.downcast_ref::<T>() {
519 Some(t) => Ext(t.clone()),
520 None => panic!(
521 "Wire<Ext<{}>>::extract: ReflectedValue downcast failed; \
522 got runtime type {:?}",
523 std::any::type_name::<T>(),
524 boxed.type_name()
525 ),
526 }
527 }
528 other => extract_mismatch(std::any::type_name::<Self>(), Self::PORT, other),
529 }
530 }
531 fn inject(self) -> Value {
532 Value::Ext(Box::new(self.0))
533 }
534}
535
536// ── DynamicOutputs<T> — variable output port count ────────────
537
538/// Marker wrapper for node return types whose output port
539/// COUNT is determined at construction time from a
540/// const-list arg's length, not at codegen time.
541/// This return shape (library_catalog.md "Shapes") covers nodes like `mixed_radix`
542/// that emit one output per radix where `radix` count is a
543/// workload-supplied list.
544///
545/// Operator writes:
546///
547/// ```ignore
548/// #[polydat_node(category = Arithmetic)]
549/// fn mixed_radix(
550/// value: u64,
551/// radixes: Const<&[u64]>,
552/// ) -> DynamicOutputs<u64> {
553/// // body returns DynamicOutputs(Vec<u64>) with len == radixes.len()
554/// }
555/// ```
556///
557/// The macro emits one output port per element (named `d0`,
558/// `d1`, ...) at construction time using the const-list
559/// arg's length. `FuncSig.outputs` is `0` signalling dynamic.
560/// Requires exactly one const-list arg per function — `Const<&[C]>`
561/// or its owned spelling `Const<Vec<C>>` — and the
562/// macro errors at compile time otherwise.
563pub struct DynamicOutputs<T>(pub Vec<T>);
564
565impl<T> std::ops::Deref for DynamicOutputs<T> {
566 type Target = Vec<T>;
567 fn deref(&self) -> &Vec<T> {
568 &self.0
569 }
570}
571
572// ── Config<T> — wire arg marked as config-cost ────────────────
573
574/// Marker wrapper signalling that the wrapped wire is a
575/// configuration input — expensive to change because the node
576/// keeps internal state (LUTs, alias tables, parsed specs)
577/// derived from it. The macro emits the matching slot with
578/// `Port::config()` (library_catalog.md "Wire cost classes") so the
579/// compiler warns on cycle-time binding.
580///
581/// The operator declares the cost intent through the type system
582/// rather than an arg-level attribute. Body unwraps with `.0` or via `Deref`.
583pub struct Config<T>(pub T);
584
585impl<T> std::ops::Deref for Config<T> {
586 type Target = T;
587 fn deref(&self) -> &T {
588 &self.0
589 }
590}
591
592impl<T: Wire> Wire for Config<T> {
593 const PORT: PortType = T::PORT;
594 const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = T::RESOLVER;
595 const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Config;
596 fn extract(v: &Value) -> Self {
597 Config(T::extract(v))
598 }
599 fn inject(self) -> Value {
600 self.0.inject()
601 }
602}
603
604// ── Resolved<R, T> — Handle wire with an auto-resolver ────────
605
606/// A Handle wire that declares its default resolver through its
607/// type (library_catalog.md "Shapes"). The `R` parameter (a [`ResolverKind`] impl) carries
608/// the auto-resolver kind; the `T` parameter is the concrete
609/// `Handle`-inner type the body sees.
610///
611/// Operators write:
612///
613/// ```ignore
614/// fn matching_profiles(
615/// group: Resolved<GroupResolver, DatasetHandle>,
616/// prefix: &str,
617/// ) -> Vec<String> {
618/// let group: &TestDataGroup = group.resolve_group();
619/// // ... use group methods directly
620/// }
621/// ```
622///
623/// The macro reads `<Resolved<GroupResolver, T> as Wire>::RESOLVER`
624/// at codegen time and emits the matching `FuncSig.default_resolver`.
625/// No `#[polydat_node(default_resolver = ...)]` attribute is
626/// involved — the resolver information lives in the function
627/// signature where it belongs.
628pub struct Resolved<R: ResolverKind, T: 'static + Send + Sync> {
629 inner: std::sync::Arc<T>,
630 _r: std::marker::PhantomData<fn() -> R>,
631}
632
633impl<R: ResolverKind, T: 'static + Send + Sync> std::ops::Deref for Resolved<R, T> {
634 type Target = T;
635 fn deref(&self) -> &T {
636 &self.inner
637 }
638}
639
640impl<R: ResolverKind, T: 'static + Send + Sync> Resolved<R, T> {
641 /// Construct from a pre-resolved Arc — useful for tests
642 /// and programmatic graph assembly that bypasses the DSL
643 /// auto-resolver.
644 pub fn from_arc(inner: std::sync::Arc<T>) -> Self {
645 Self {
646 inner,
647 _r: std::marker::PhantomData,
648 }
649 }
650 /// Borrow the inner Arc.
651 pub fn as_arc(&self) -> &std::sync::Arc<T> {
652 &self.inner
653 }
654}
655
656/// Marker trait that names a kind of source-string auto-resolver
657/// for [`Resolved<R, T>`] wire args. The variants here mirror
658/// [`crate::dsl::registry::DefaultResolver`]; each impl picks
659/// one of them.
660pub trait ResolverKind: 'static {
661 /// The resolver this kind names.
662 const RESOLVER: crate::dsl::registry::DefaultResolver;
663}
664
665/// Splice `dataset_group_open(<source>)` upstream when the wire
666/// source is a `Str` (`DefaultResolver::Group`).
667pub struct GroupResolver;
668impl ResolverKind for GroupResolver {
669 const RESOLVER: crate::dsl::registry::DefaultResolver =
670 crate::dsl::registry::DefaultResolver::Group;
671}
672
673impl<R: ResolverKind, T: 'static + Send + Sync> Wire for Resolved<R, T> {
674 const PORT: PortType = PortType::Handle;
675 const RESOLVER: Option<crate::dsl::registry::DefaultResolver> =
676 Some(<R as ResolverKind>::RESOLVER);
677 fn extract(v: &Value) -> Self {
678 match v {
679 Value::Handle(arc) => {
680 let inner = arc.clone().downcast::<T>().unwrap_or_else(|_| {
681 panic!(
682 "Wire<Resolved<_, {}>>::extract: Handle downcast failed",
683 std::any::type_name::<T>()
684 )
685 });
686 Resolved {
687 inner,
688 _r: std::marker::PhantomData,
689 }
690 }
691 // The common fault, and the one worth naming: an
692 // upstream open did not resolve, so the handle this
693 // node reads is absent rather than wrong.
694 Value::None => panic!(
695 "a resolved handle is None — the upstream open failed to \
696 resolve. The audit log carries the underlying error and \
697 the name it was opening: a catalog miss, a facet missing \
698 on disk, or a transport failure. This is the most common \
699 fault when a workload runs on a system whose catalog is \
700 not configured for the source it asks for."
701 ),
702 other => extract_mismatch(std::any::type_name::<Self>(), Self::PORT, other),
703 }
704 }
705 fn inject(self) -> Value {
706 Value::Handle(self.inner)
707 }
708}
709
710// The `#[polydat_node]` macro dispatches every owned type through
711// `<T as Wire>::extract` / `::inject` and emits direct
712// `match`-on-`Value` extraction for borrow shapes (`&str`, `&[u8]`,
713// `&[T]`, `&serde_json::Value`), with no lifetime transmute.
714
715/// Marker wrapper for const arguments in
716/// `#[polydat_node]` function signatures.
717///
718/// Use in arg position to signal that the value is captured at
719/// node-construction time (assembly-time) rather than read
720/// per-cycle from a wire. The macro detects `Const<T>` in arg
721/// position and:
722///
723/// - Emits `Slot::Const { ... }` (not `Slot::Wire`) in the
724/// node's NodeMeta.
725/// - Emits `SlotType::ConstU64` / `ConstF64` / `ConstStr` in
726/// the corresponding `FuncSig.params` entry (the const
727/// variant matching `T`).
728/// - Adds a struct field to hold the captured value.
729/// - Generates a `new(const_values...)` constructor.
730/// - Wires the build closure to pull from `consts: &[ConstArg]`
731/// and pass values to `new()`.
732/// - Constructs a `Const<T>(...)` wrapper around the struct
733/// field at eval time so the user's function body sees the
734/// wrapped type matching its signature.
735///
736/// Body code accesses the wrapped value via `.0` or via the
737/// `Deref` impl below:
738///
739/// ```ignore
740/// #[polydat_node(category = String)]
741/// fn combinations(input: u64, pattern: Const<&str>) -> String {
742/// apply(input, pattern.0) // pattern.0 is &str
743/// }
744/// ```
745///
746/// Type-shape dispatch table:
747///
748/// | `Const<T>` form | `SlotType` variant | Struct field type | ConstArg accessor |
749/// |---|---|---|---|
750/// | `Const<u64>` | `ConstU64` | `u64` | `as_u64()` |
751/// | `Const<f64>` | `ConstF64` | `f64` | `as_f64()` |
752/// | `Const<bool>` | `ConstU64` | `bool` | `as_u64() != 0` |
753/// | `Const<&str>` | `ConstStr` | `String` | `as_str().to_string()` |
754pub struct Const<T>(pub T);
755
756impl<T> std::ops::Deref for Const<T> {
757 type Target = T;
758 fn deref(&self) -> &T {
759 &self.0
760 }
761}
762
763impl<T> std::ops::DerefMut for Const<T> {
764 fn deref_mut(&mut self) -> &mut T {
765 &mut self.0
766 }
767}
768
769/// Construction-time setup contract for nodes
770/// that derive a pre-computed runtime state from their const
771/// args (e.g. `combinations` parsing a charset pattern into
772/// segments + modulus, `regex_match` compiling a pattern,
773/// `histribution` parsing a distribution spec).
774///
775/// **The contract**: the operator-provided setup function is
776/// called EXACTLY ONCE per node instance, at construction time
777/// (`new()`). Its result is stored in a struct field; eval-
778/// time access is a plain `&T` borrow.
779///
780/// **Type-level enforcement**: the `#[poly_const(...)]`
781/// attribute on a `&T` argument tells the macro to generate
782/// this construction pattern. The macro is the sole party
783/// emitting `setup_fn(...)` calls and it generates the call
784/// exactly once inside `new()`. The contract is inviolable
785/// because no other code path can reach the setup function —
786/// the macro hides it inside the constructor.
787///
788/// In effect, the function pointer behaves as `FnOnce` —
789/// invoked one time, by one site, never again. The FnOnce
790/// semantics aren't expressed as a trait bound because they
791/// don't need to be: the macro is the only caller, and the
792/// macro respects single-call by construction.
793///
794/// Library author idiom:
795///
796/// ```ignore
797/// pub struct ParsedPattern {
798/// pub segments: Vec<Segment>,
799/// pub modulus: u64,
800/// }
801///
802/// impl ParsedPattern {
803/// /// Single-call setup. Macro invokes once in `new()`.
804/// fn from_pattern(pattern: &str) -> Self { /* parse */ }
805/// }
806///
807/// #[polydat_node(category = String)]
808/// fn combinations(
809/// input: u64,
810/// pattern: Const<&str>,
811/// #[poly_const(ParsedPattern::from_pattern, from = pattern)]
812/// parsed: &ParsedPattern,
813/// ) -> String {
814/// // parsed is a borrow of the cached struct field —
815/// // no recomputation, no clone, no ceremony at the call site.
816/// let mut r = input % parsed.modulus;
817/// /* ... */
818/// }
819/// ```
820///
821/// Marker trait — purely a documentation handle for types
822/// intended to be polydat-setup targets. The macro doesn't
823/// dispatch on this; the attribute is the dispatch surface.
824/// Implementing the trait gives library authors a way to
825/// signal intent and improve `cargo doc` discoverability.
826pub trait PolydatSetup {}
827
828// ── The slot kit's run-time helpers ──────────────────────────────
829// Called by the closures `#[polydat_node]` emits for its `compiled_slot`
830// kit; public because generated code in other crates calls them, not
831// because hosts should.
832
833/// The value a `Ref2` pair at the head of `slots` holds by reference:
834/// the one-element slice a JSON, extension, or handle producer
835/// published (jit_boundary.md, axiom S7: one dereference).
836///
837/// The slots must hold a pair a producer published into storage that
838/// is alive: its own scratch, an extern's stored value, or a boundary
839/// value alive for the call (axioms S3, S4). A pair of length zero,
840/// an unset extern, reads as [`Value::None`].
841#[inline]
842pub fn ref_value(slots: &[u64]) -> &Value {
843 static NONE: Value = Value::None;
844 if slots.get(1).copied().unwrap_or(0) == 0 {
845 return &NONE;
846 }
847 // SAFETY: as documented; the producer's storage outlives the read.
848 unsafe { &*(slots[0] as usize as *const Value) }
849}
850
851/// An empty buffer with room for `n` elements, for a node whose buffer
852/// size comes from a *value* — a wire, a constant, or arithmetic on
853/// either.
854///
855/// `Vec::with_capacity(n as usize)` is the obvious spelling and it is
856/// wrong twice over for a value the node did not choose. A size the
857/// machine cannot hold makes the allocator **abort the process**, which
858/// no `catch_unwind` sees: not an error on the program that asked, but
859/// the host gone. And `as usize` truncates on a 32-bit target, so a
860/// large size silently becomes a small one. Here a size that does not
861/// fit `usize`, or cannot be reserved, is a failure of the node like
862/// any other — caught, attributed to the node and its inputs, and the
863/// same on every engine.
864///
865/// There is no cap. A size that *can* be allocated is allocated, however
866/// slow filling it is; how large a string a host asks for is the host's
867/// business. What this refuses is only what could never have
868/// succeeded. Arithmetic on a size belongs in `u64` with
869/// `saturating_add`, so an overflow reaches here as a size that cannot
870/// be reserved rather than wrapping to a small one first.
871pub fn buffer_for<T>(n: u64, what: &str) -> Vec<T> {
872 try_buffer_for(n, what).unwrap_or_else(|e| panic!("{e}"))
873}
874
875/// [`buffer_for`] for a caller that reports failure as a value rather
876/// than as a node's panic: a spec parser or a compile step, whose size
877/// comes from program text. The error is the same sentence.
878pub fn try_buffer_for<T>(n: u64, what: &str) -> Result<Vec<T>, String> {
879 let mut v = Vec::new();
880 match usize::try_from(n) {
881 Ok(k) if v.try_reserve_exact(k).is_ok() => Ok(v),
882 _ => Err(size_refusal(n, what)),
883 }
884}
885
886/// [`buffer_for`] for text: an empty `String` with room for `n` bytes.
887pub fn string_for(n: u64, what: &str) -> String {
888 let mut s = String::new();
889 match usize::try_from(n) {
890 Ok(n) if s.try_reserve_exact(n).is_ok() => s,
891 _ => refuse_size(n, what),
892 }
893}
894
895/// [`buffer_for`] for a buffer that is reused across evaluations:
896/// `out` is cleared and then has room for `n` elements. A native
897/// producer writing into step-owned scratch takes this shape, so its
898/// refusal reads the same as the node's.
899pub fn reserve_for<T>(out: &mut Vec<T>, n: u64, what: &str) {
900 out.clear();
901 match usize::try_from(n) {
902 Ok(n) if out.try_reserve_exact(n).is_ok() => {}
903 _ => refuse_size(n, what),
904 }
905}
906
907#[cold]
908fn refuse_size(n: u64, what: &str) -> ! {
909 panic!("{}", size_refusal(n, what))
910}
911
912fn size_refusal(n: u64, what: &str) -> String {
913 format!("{what}: a buffer of {n} elements cannot be allocated on this machine")
914}
915
916/// A polymorphic port's slots as the owned `Value` the wire type
917/// names: a scalar from its bits, a `Ref2` kind copied out of the
918/// pair its producer published.
919#[inline]
920pub fn read_poly(ty: PortType, slots: &[u64]) -> Value {
921 crate::compile::marshal::decode_slot(slots, ty)
922}
923
924/// A polymorphic return written by the node's resolved output type: a
925/// scalar as its bits into `outputs[0]`, a `Ref2` kind into
926/// `scratch[0]` with its pair republished (axiom S3). The value must
927/// be the port type's carrier: the graph colored the slot by the
928/// node's resolved output type, and a value of another type would be
929/// read by every consumer as something it is not, where the
930/// interpreter would have carried it. A `None` has no slot form on a
931/// compiled engine (engines.md §3.3).
932///
933/// The comparison is with the carrier, not the port type, because a
934/// `u32` or `f32` value *is* a `U64` in flight. Comparing with the port
935/// type refused every narrow integer and small float that reached a
936/// polymorphic node, on every compiled engine.
937#[inline]
938pub fn write_poly(
939 ty: PortType,
940 v: Value,
941 scratch: &mut [crate::ast::ScratchBuf],
942 outputs: &mut [u64],
943) {
944 // A `Dyn` port carries any value as written (input_variance.md).
945 if ty != PortType::Dyn && v.port_type() != crate::compile::marshal::carrier_port(ty) {
946 panic!(
947 "a node produced a {:?} on an output the graph typed {:?}; a compiled engine \
948 cannot carry a value of another type than the slot's (engines.md §3.4)",
949 v.port_type(),
950 ty
951 );
952 }
953 // The slot form is a property of the *type*, and the type is
954 // resolved: `ty` is what the graph coloured this output. So the
955 // write is chosen by the colour, of which there are three, rather
956 // than by the value's variant — a match over `Value` has to be
957 // extended every time the language grows a carrier, and a missing
958 // arm is not a compile error but a panic at the first pull. This
959 // one went through three rounds of that: the by-reference singles,
960 // then the numeric vectors, then the 128-bit words.
961 match ty.slot_color() {
962 // One slot of immediate data: the carrier's bits.
963 crate::ast::SlotColor::Imm1 => outputs[0] = carrier_slot_bits(&v),
964 // Two slots of immediate limb data, low word first
965 // (`Bits128`), which is what every 128-bit and register value
966 // is underneath.
967 crate::ast::SlotColor::Imm2 => {
968 // `as_reg_bits` is the register reading and refuses a
969 // 128-bit integer, so the limbs are taken from whichever
970 // two-slot carrier this is.
971 let words = match v {
972 Value::U128(b) | Value::I128(b) | Value::Reg128(b, _) => b.0,
973 other => unreachable!("a {:?} is not a two-slot carrier", other.port_type()),
974 };
975 outputs[0] = words[0];
976 outputs[1] = words[1];
977 }
978 // A (ptr, len) pair into the step's own scratch entry. Filling
979 // the entry can move its buffer, so the pair is republished
980 // every time — a slot left pointing at the old allocation is
981 // what the S9 validator catches (axiom S9(a)).
982 crate::ast::SlotColor::Ref2 => {
983 scratch[0].set_from_value(&v);
984 let (p, l) = scratch[0].ptr_len();
985 outputs[0] = p;
986 outputs[1] = l;
987 }
988 }
989}
990
991/// The slot bits of a one-slot carrier.
992///
993/// `Value::None` has no slot form on a compiled engine (engines.md
994/// §3.3); it never reaches here, because an unset output is tracked
995/// beside the buffer rather than written into it.
996#[inline]
997fn carrier_slot_bits(v: &Value) -> u64 {
998 match v {
999 Value::U64(x) => *x,
1000 Value::I64(x) => *x as u64,
1001 Value::F64(x) => x.to_bits(),
1002 Value::Bool(b) => *b as u64,
1003 other => panic!(
1004 "a {:?} value is not a one-slot carrier; the graph coloured its output Imm1",
1005 other.port_type()
1006 ),
1007 }
1008}
1009
1010// Macro-generated nodes register through the `NodeRegistration`
1011// inventory channel (`polydat::dsl::registry::NodeRegistration`,
1012// library_catalog.md "Registration"), the same channel
1013// `register_nodes!` uses. The proc-macro
1014// emits a `NodeRegistration` per `#[polydat_node]` site, so
1015// every consumer that already iterates the registry
1016// (`registry()`, `lookup()`, the compile pipeline's
1017// `factory::build_node`) sees macro-generated nodes
1018// automatically — no parallel collection, no separate dispatch
1019// surface. See `polydat::dsl::registry` for the load-bearing
1020// data structures.
1021
1022#[cfg(test)]
1023mod tests {
1024 use super::*;
1025
1026 fn refusal(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
1027 let err = std::panic::catch_unwind(f).expect_err("the size must be refused");
1028 err.downcast_ref::<String>().cloned().unwrap_or_default()
1029 }
1030
1031 /// A size no machine can hold is a caught failure naming the node,
1032 /// not an allocator abort that takes the host down with it.
1033 #[test]
1034 fn an_impossible_size_is_refused_not_aborted() {
1035 let m = refusal(|| drop(buffer_for::<u32>(u64::MAX, "probe")));
1036 assert!(
1037 m.starts_with("probe: a buffer of 18446744073709551615"),
1038 "{m}"
1039 );
1040 let m = refusal(|| drop(string_for((1 << 53) + 1, "probe")));
1041 assert!(m.contains("cannot be allocated"), "{m}");
1042 let m = refusal(|| reserve_for(&mut vec![1.0f32; 4], u64::MAX, "probe"));
1043 assert!(m.contains("cannot be allocated"), "{m}");
1044 }
1045
1046 #[derive(Debug, Clone)]
1047 struct Probe;
1048 impl ReflectedValue for Probe {
1049 fn type_name(&self) -> &str {
1050 "probe"
1051 }
1052 fn display(&self) -> String {
1053 "probe".into()
1054 }
1055 fn as_any(&self) -> &dyn std::any::Any {
1056 self
1057 }
1058 fn clone_reflected(&self) -> Box<dyn ReflectedValue> {
1059 Box::new(self.clone())
1060 }
1061 }
1062
1063 /// A value of every port type, as its producer's `Wire` impl would
1064 /// carry it. The match has no wildcard, so a type added to the
1065 /// language does not compile here until it has one.
1066 fn sample(ty: PortType) -> Value {
1067 use crate::ast::{Bits128, RegLanes};
1068 let limbs = Bits128([0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210]);
1069 let reg = |lanes| Value::Reg128(limbs, lanes);
1070 match ty {
1071 PortType::U64 | PortType::U32 | PortType::U16 | PortType::U8 => Value::U64(200),
1072 PortType::F32 => Value::U64(1.5f32.to_bits() as u64),
1073 PortType::F16 => Value::U64(half::f16::from_f32(1.5).to_bits() as u64),
1074 PortType::I64 | PortType::I32 | PortType::I16 | PortType::I8 => Value::I64(-7),
1075 PortType::F64 => Value::F64(-2.25),
1076 PortType::Bool => Value::Bool(true),
1077 PortType::U128 => Value::U128(limbs),
1078 PortType::I128 => Value::I128(limbs),
1079 PortType::Reg128 => reg(RegLanes::Raw),
1080 PortType::RegI8x16 => reg(RegLanes::I8x16),
1081 PortType::RegI16x8 => reg(RegLanes::I16x8),
1082 PortType::RegI32x4 => reg(RegLanes::I32x4),
1083 PortType::RegI64x2 => reg(RegLanes::I64x2),
1084 PortType::RegF16x8 => reg(RegLanes::F16x8),
1085 PortType::RegF32x4 => reg(RegLanes::F32x4),
1086 PortType::RegF64x2 => reg(RegLanes::F64x2),
1087 PortType::Str => Value::Str("héllo".into()),
1088 PortType::Bytes => Value::Bytes(vec![0u8, 1, 255].into()),
1089 PortType::Json => Value::Json(Arc::new(serde_json::json!({"k": [1, 2]}))),
1090 PortType::Ext => Value::Ext(Box::new(Probe)),
1091 PortType::Handle => Value::Handle(Arc::new(42u32)),
1092 PortType::VecF32 => Value::VecF32(SliceArc::from_vec(vec![1.0, -0.5])),
1093 PortType::VecI32 => Value::VecI32(SliceArc::from_vec(vec![-3, 4])),
1094 PortType::VecF64 => Value::VecF64(SliceArc::from_vec(vec![1e300, -0.0])),
1095 PortType::VecI64 => Value::VecI64(SliceArc::from_vec(vec![i64::MIN, 9])),
1096 PortType::VecF16 => Value::VecF16(SliceArc::from_vec(vec![half::f16::from_f32(0.5)])),
1097 PortType::VecI16 => Value::VecI16(SliceArc::from_vec(vec![-300i16, 300])),
1098 PortType::VecI8 => Value::VecI8(SliceArc::from_vec(vec![-8i8, 8])),
1099 // A `Dyn` port carries a value of any type, as written.
1100 PortType::Dyn => Value::Str("any".into()),
1101 }
1102 }
1103
1104 /// Reading a polymorphic port inverts writing one, for every port
1105 /// type the language has: what a polymorphic node returns is what
1106 /// the next polymorphic node takes, on every compiled engine.
1107 ///
1108 /// A register reaching a polymorphic node's input arrives whole,
1109 /// with its limbs reassembled as the host's output read reassembles
1110 /// them, not as its low limb typed `U64`
1111 /// (`log_warn(reg_splat_f64(..))` is the case).
1112 #[test]
1113 fn every_port_type_reads_back_what_was_written() {
1114 for &ty in PortType::ALL {
1115 let v = sample(ty);
1116 // A `Dyn` port carries a value of any type, as written.
1117 if ty != PortType::Dyn {
1118 assert_eq!(
1119 v.port_type(),
1120 crate::compile::marshal::carrier_port(ty),
1121 "{ty:?}: the sample is not what the port carries"
1122 );
1123 }
1124 let mut scratch: Vec<crate::ast::ScratchBuf> = ty
1125 .scratch_elem()
1126 .map(crate::ast::ScratchBuf::new)
1127 .into_iter()
1128 .collect();
1129 let mut slots = [0u64; 2];
1130 // Written as the macro writes it: by the port's resolved
1131 // type, which for a narrow or float-in-carrier type is not
1132 // the port type of the carrier value its `Wire` impl injects.
1133 write_poly(ty, v.clone(), &mut scratch, &mut slots);
1134 let back = read_poly(ty, &slots[..ty.slot_width()]);
1135 assert_eq!(back, v, "{ty:?}");
1136 }
1137 }
1138
1139 /// A size that fits is reserved exactly, and a reused buffer comes
1140 /// back cleared.
1141 #[test]
1142 fn a_feasible_size_is_reserved() {
1143 let v: Vec<u8> = buffer_for(1000, "probe");
1144 assert!(v.is_empty() && v.capacity() >= 1000);
1145 let mut w = vec![7u8; 3];
1146 reserve_for(&mut w, 64, "probe");
1147 assert!(w.is_empty() && w.capacity() >= 64);
1148 }
1149}