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