Skip to main content

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//! - [`ConstSource`] — `Sized + 'static` typed-extraction from
17//!   [`ConstArg`] for owned `Const<T>` positions.
18//!
19//! Combinator [`Wire`] impls cover [`Option<T>`] (`None`-aware
20//! pass-through) and [`Ext<T>`] (downcast through
21//! [`ReflectedValue`]); `ConstSource for Vec<C: ConstSource>`
22//! handles workload-list constants.
23//!
24//! ## Why the trait surface lives here
25//!
26//! `polydat-derive` is a proc-macro crate — it can't define
27//! traits that are visible at the call site, only emit token
28//! streams referencing traits defined elsewhere. The macro
29//! emits `<T as polydat::derive_support::Wire>::extract(...)`
30//! and `<T as polydat::derive_support::ConstSource>::extract(...)`
31//! paths; this module is what those paths resolve to.
32
33use std::sync::Arc;
34
35use crate::ast::SlotShape;
36use crate::ast::{JitType, PortType, ReflectedValue, SliceArc, SlotType, Value};
37use crate::dsl::factory::ConstArg;
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; `JIT`
48/// tags the type as ridable on the Phase-2 `u64` buffer (or
49/// `None` if it stays on the Phase-1 typed-eval path).
50///
51/// Borrow shapes (`&str`, `&[u8]`, `&[T]`,
52/// `&serde_json::Value`) and polymorphic `Value`-typed wires
53/// are NOT covered here — the macro recognises them
54/// syntactically and emits direct `match`-on-`Value` extraction
55/// at the eval call site. This keeps the trait surface free of
56/// lifetime parameters.
57///
58/// `extract` panics on type mismatch — the DSL type-checker is
59/// responsible for routing well-typed `Value`s to each slot
60/// before `eval` runs. A panic here is a "type-checker was
61/// lied to" bug, not a normal path.
62pub trait Wire: Sized + 'static {
63    /// Static port type for the DSL type-checker.
64    const PORT: PortType;
65
66    /// JIT carrier classification. `Some(_)` means the type
67    /// rides the Phase-2 `u64` buffer; `None` means typed-eval
68    /// only.
69    const JIT: Option<JitType>;
70
71    /// SRD-53 §"Source-string call-site sugar" — auto-resolver
72    /// for `Str`-typed upstream wires feeding this slot. `None`
73    /// (the default) disables auto-promotion; the workload must
74    /// supply the wire's actual port type directly. Set via the
75    /// [`Resolved<R, T>`] marker wrapper.
76    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = None;
77
78    /// SRD-15 §"WireCost::Config" — cost class for this wire.
79    /// Defaults to [`WireCost::Data`](crate::ast::WireCost::Data) (cheap per-cycle input).
80    /// Set to [`WireCost::Config`](crate::ast::WireCost::Config) via the [`Config<T>`] marker
81    /// wrapper to signal that the wire is rarely-changing and
82    /// the compiler should warn on cycle-time binding.
83    const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Data;
84
85    /// Pull a typed value out of a `Value` wire.
86    fn extract(v: &Value) -> Self;
87
88    /// Push a typed value back into the `Value` outputs stream.
89    fn inject(self) -> Value;
90}
91
92// ── Scalar primitives ─────────────────────────────────────────
93
94impl Wire for u64 {
95    const PORT: PortType = PortType::U64;
96    const JIT: Option<JitType> = Some(JitType::U64);
97    fn extract(v: &Value) -> Self {
98        v.as_u64()
99    }
100    fn inject(self) -> Value {
101        Value::U64(self)
102    }
103}
104
105impl Wire for u32 {
106    const PORT: PortType = PortType::U32;
107    const JIT: Option<JitType> = Some(JitType::U64);
108    fn extract(v: &Value) -> Self {
109        v.as_u64() as u32
110    }
111    fn inject(self) -> Value {
112        Value::U64(self as u64)
113    }
114}
115
116impl Wire for i32 {
117    const PORT: PortType = PortType::I32;
118    const JIT: Option<JitType> = Some(JitType::I64);
119    // Lenient extract: honest `Value::I64` (sign-extended I32
120    // storage convention) plus the legacy bit-stuffed `Value::U64`
121    // form during the alignment migration — same precedent as
122    // `Wire<bool>` accepting `U64(n != 0)`.
123    fn extract(v: &Value) -> Self {
124        v.as_i64() as i32
125    }
126    fn inject(self) -> Value {
127        Value::I64(self as i64)
128    }
129}
130
131impl Wire for i64 {
132    const PORT: PortType = PortType::I64;
133    const JIT: Option<JitType> = Some(JitType::I64);
134    // Lenient extract: see `Wire<i32>` note above.
135    fn extract(v: &Value) -> Self {
136        v.as_i64()
137    }
138    fn inject(self) -> Value {
139        Value::I64(self)
140    }
141}
142
143impl Wire for u8 {
144    const PORT: PortType = PortType::U8;
145    const JIT: Option<JitType> = Some(JitType::U64);
146    fn extract(v: &Value) -> Self {
147        v.as_u64() as u8
148    }
149    fn inject(self) -> Value {
150        Value::U64(self as u64)
151    }
152}
153
154impl Wire for u16 {
155    const PORT: PortType = PortType::U16;
156    const JIT: Option<JitType> = Some(JitType::U64);
157    fn extract(v: &Value) -> Self {
158        v.as_u64() as u16
159    }
160    fn inject(self) -> Value {
161        Value::U64(self as u64)
162    }
163}
164
165impl Wire for i8 {
166    const PORT: PortType = PortType::I8;
167    const JIT: Option<JitType> = Some(JitType::I64);
168    // Lenient extract through as_i64 (honest I64 or legacy
169    // stuffed U64), narrowed by truncation — sign survives
170    // because the storage convention is sign-extension.
171    fn extract(v: &Value) -> Self {
172        v.as_i64() as i8
173    }
174    fn inject(self) -> Value {
175        Value::I64(self as i64)
176    }
177}
178
179impl Wire for i16 {
180    const PORT: PortType = PortType::I16;
181    const JIT: Option<JitType> = Some(JitType::I64);
182    fn extract(v: &Value) -> Self {
183        v.as_i64() as i16
184    }
185    fn inject(self) -> Value {
186        Value::I64(self as i64)
187    }
188}
189
190impl Wire for u128 {
191    const PORT: PortType = PortType::U128;
192    // Interpreter-only: a 128-bit value cannot ride the one-u64
193    // JIT slot; the two-slot ABI is a Phase-5 concern
194    // (type_system_alignment.md §8.1).
195    const JIT: Option<JitType> = None;
196    fn extract(v: &Value) -> Self {
197        v.as_u128()
198    }
199    fn inject(self) -> Value {
200        Value::U128(crate::ast::Bits128::from_u128(self))
201    }
202}
203
204impl Wire for i128 {
205    const PORT: PortType = PortType::I128;
206    const JIT: Option<JitType> = None;
207    fn extract(v: &Value) -> Self {
208        v.as_i128()
209    }
210    fn inject(self) -> Value {
211        Value::I128(crate::ast::Bits128::from_i128(self))
212    }
213}
214
215// ── 128-bit register words (type_system_alignment.md §8.4 L2) ──
216//
217// The raw view extracts/injects the word itself; the lane-typed
218// `[T; N]` views extract through the free-bitcast rule (any
219// register view satisfies any register slot) and inject tagged
220// with their own lane typing. JIT is None until the layer-1
221// two-slot ride lands.
222
223impl Wire for crate::ast::Bits128 {
224    const PORT: PortType = PortType::Reg128;
225    const JIT: Option<JitType> = None;
226    fn extract(v: &Value) -> Self {
227        v.as_reg_bits()
228    }
229    fn inject(self) -> Value {
230        Value::Reg128(self, crate::ast::RegLanes::Raw)
231    }
232}
233
234macro_rules! impl_wire_reg {
235    ($arr:ty, $port:ident, $view:ident, $to:ident, $from:ident) => {
236        impl Wire for $arr {
237            const PORT: PortType = PortType::$port;
238            const JIT: Option<JitType> = None;
239            fn extract(v: &Value) -> Self {
240                v.as_reg_bits().$to()
241            }
242            fn inject(self) -> Value {
243                Value::Reg128(
244                    crate::ast::Bits128::$from(self),
245                    crate::ast::RegLanes::$view,
246                )
247            }
248        }
249    };
250}
251
252impl_wire_reg!([i8; 16], RegI8x16, I8x16, lanes_i8, from_lanes_i8);
253impl_wire_reg!([i16; 8], RegI16x8, I16x8, lanes_i16, from_lanes_i16);
254impl_wire_reg!([i32; 4], RegI32x4, I32x4, lanes_i32, from_lanes_i32);
255impl_wire_reg!([i64; 2], RegI64x2, I64x2, lanes_i64, from_lanes_i64);
256impl_wire_reg!([half::f16; 8], RegF16x8, F16x8, lanes_f16, from_lanes_f16);
257impl_wire_reg!([f32; 4], RegF32x4, F32x4, lanes_f32, from_lanes_f32);
258impl_wire_reg!([f64; 2], RegF64x2, F64x2, lanes_f64, from_lanes_f64);
259
260impl Wire for f64 {
261    const PORT: PortType = PortType::F64;
262    const JIT: Option<JitType> = Some(JitType::F64);
263    fn extract(v: &Value) -> Self {
264        v.as_f64()
265    }
266    fn inject(self) -> Value {
267        Value::F64(self)
268    }
269}
270
271impl Wire for f32 {
272    const PORT: PortType = PortType::F32;
273    const JIT: Option<JitType> = Some(JitType::U64);
274    fn extract(v: &Value) -> Self {
275        f32::from_bits(v.as_u64() as u32)
276    }
277    fn inject(self) -> Value {
278        Value::U64(self.to_bits() as u64)
279    }
280}
281
282impl Wire for half::f16 {
283    const PORT: PortType = PortType::F16;
284    const JIT: Option<JitType> = Some(JitType::U64);
285    // Same bit-stuffing convention as f32: the binary16 pattern
286    // rides the low 16 bits of the u64 carrier.
287    fn extract(v: &Value) -> Self {
288        half::f16::from_bits(v.as_u64() as u16)
289    }
290    fn inject(self) -> Value {
291        Value::U64(self.to_bits() as u64)
292    }
293}
294
295impl Wire for bool {
296    const PORT: PortType = PortType::Bool;
297    const JIT: Option<JitType> = Some(JitType::Bool);
298    fn extract(v: &Value) -> Self {
299        match v {
300            Value::Bool(b) => *b,
301            Value::U64(n) => *n != 0,
302            other => panic!(
303                "Wire<bool>::extract: type-checker routed {other:?} \
304                 to a Bool slot"
305            ),
306        }
307    }
308    fn inject(self) -> Value {
309        Value::Bool(self)
310    }
311}
312
313impl Wire for String {
314    const PORT: PortType = PortType::Str;
315    const JIT: Option<JitType> = None;
316    fn extract(v: &Value) -> Self {
317        // SRD-80b: panic on shape mismatch — the type-checker is
318        // responsible for routing well-typed values to each slot,
319        // and a non-Str input here is a "type system was lied to"
320        // bug, not a coercion opportunity. Nodes that want a
321        // display rendering of an arbitrary `Value` take a
322        // `Value`-typed (PolyWire) arg instead.
323        match v {
324            Value::Str(s) => s.to_string(),
325            other => panic!("Wire<String>::extract: expected Str, got {other:?}"),
326        }
327    }
328    fn inject(self) -> Value {
329        Value::Str(self.into())
330    }
331}
332
333/// `Arc<str>` — zero-copy shared string handle. Reading
334/// extracts the existing `Arc<str>` from `Value::Str` (refcount
335/// bump only); injecting wraps directly. Nodes whose hot path
336/// emits the same string per cycle (lookup table outputs,
337/// fixed-value selectors) should use this instead of `String`
338/// to avoid the per-cycle `to_string()` allocation.
339impl Wire for std::sync::Arc<str> {
340    const PORT: PortType = PortType::Str;
341    const JIT: Option<JitType> = None;
342    fn extract(v: &Value) -> Self {
343        match v {
344            Value::Str(s) => s.clone(),
345            other => panic!("Wire<Arc<str>>::extract: expected Str, got {other:?}"),
346        }
347    }
348    fn inject(self) -> Value {
349        Value::Str(self)
350    }
351}
352
353/// `Arc<dyn Any + Send + Sync>` — opaque Handle wire. The body
354/// receives the runtime-typed handle directly; downcast is the
355/// operator's responsibility. Use [`Resolved<R, T>`] when the
356/// node wants a typed Handle with SRD-53 source-string
357/// auto-promotion sugar; use this raw shape when the body
358/// needs to handle multiple inner types via runtime dispatch.
359impl Wire for std::sync::Arc<dyn std::any::Any + Send + Sync> {
360    const PORT: PortType = PortType::Handle;
361    const JIT: Option<JitType> = None;
362    fn extract(v: &Value) -> Self {
363        match v {
364            Value::Handle(arc) => arc.clone(),
365            other => panic!("Wire<Arc<dyn Any>>::extract: expected Handle, got {other:?}"),
366        }
367    }
368    fn inject(self) -> Value {
369        Value::Handle(self)
370    }
371}
372
373/// `Box<dyn ReflectedValue>` — Ext (adapter-typed) wire with
374/// dynamic downcast left to the body. Use [`Ext<T>`] when the
375/// inner type is known at codegen; use this when a node needs
376/// to dispatch on the runtime ReflectedValue::type_name.
377impl Wire for Box<dyn ReflectedValue> {
378    const PORT: PortType = PortType::Ext;
379    const JIT: Option<JitType> = None;
380    fn extract(v: &Value) -> Self {
381        match v {
382            Value::Ext(b) => b.clone_reflected(),
383            other => panic!("Wire<Box<dyn ReflectedValue>>::extract: expected Ext, got {other:?}"),
384        }
385    }
386    fn inject(self) -> Value {
387        Value::Ext(self)
388    }
389}
390
391// ── Bytes ──────────────────────────────────────────────────────
392
393impl Wire for Arc<[u8]> {
394    const PORT: PortType = PortType::Bytes;
395    const JIT: Option<JitType> = None;
396    fn extract(v: &Value) -> Self {
397        match v {
398            Value::Bytes(b) => b.clone(),
399            other => panic!("Wire<Arc<[u8]>>::extract: expected Bytes, got {other:?}"),
400        }
401    }
402    fn inject(self) -> Value {
403        Value::Bytes(self)
404    }
405}
406
407impl Wire for Vec<u8> {
408    const PORT: PortType = PortType::Bytes;
409    const JIT: Option<JitType> = None;
410    fn extract(v: &Value) -> Self {
411        match v {
412            Value::Bytes(b) => b.to_vec(),
413            other => panic!("Wire<Vec<u8>>::extract: expected Bytes, got {other:?}"),
414        }
415    }
416    fn inject(self) -> Value {
417        Value::Bytes(self.into())
418    }
419}
420
421// ── Json ───────────────────────────────────────────────────────
422
423impl Wire for Arc<serde_json::Value> {
424    const PORT: PortType = PortType::Json;
425    const JIT: Option<JitType> = None;
426    fn extract(v: &Value) -> Self {
427        match v {
428            Value::Json(j) => j.clone(),
429            other => panic!("Wire<Arc<Json>>::extract: expected Json, got {other:?}"),
430        }
431    }
432    fn inject(self) -> Value {
433        Value::Json(self)
434    }
435}
436
437// ── Typed-element vectors ──────────────────────────────────────
438
439macro_rules! impl_wire_vec {
440    ($elem:ty, $variant:ident, $port:ident) => {
441        impl Wire for SliceArc<$elem> {
442            const PORT: PortType = PortType::$port;
443            const JIT: Option<JitType> = None;
444            fn extract(v: &Value) -> Self {
445                match v {
446                    Value::$variant(arc) => arc.clone(),
447                    other => panic!(
448                        concat!(
449                            "Wire<SliceArc<",
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(self)
461            }
462        }
463
464        impl Wire for Vec<$elem> {
465            const PORT: PortType = PortType::$port;
466            const JIT: Option<JitType> = None;
467            fn extract(v: &Value) -> Self {
468                match v {
469                    Value::$variant(arc) => arc.as_slice().to_vec(),
470                    other => panic!(
471                        concat!(
472                            "Wire<Vec<",
473                            stringify!($elem),
474                            ">>::extract: expected ",
475                            stringify!($variant),
476                            ", got {:?}"
477                        ),
478                        other
479                    ),
480                }
481            }
482            fn inject(self) -> Value {
483                Value::$variant(SliceArc::from_vec(self))
484            }
485        }
486    };
487}
488
489impl_wire_vec!(f32, VecF32, VecF32);
490impl_wire_vec!(i32, VecI32, VecI32);
491impl_wire_vec!(f64, VecF64, VecF64);
492impl_wire_vec!(i64, VecI64, VecI64);
493impl_wire_vec!(half::f16, VecF16, VecF16);
494impl_wire_vec!(i16, VecI16, VecI16);
495impl_wire_vec!(i8, VecI8, VecI8);
496
497// ── Phase C combinators ────────────────────────────────────────
498
499/// None-aware wire combinator. Macro auto-emits
500/// `accepts_none_inputs() -> true` when any arg is `Option<_>`.
501impl<T: Wire> Wire for Option<T> {
502    const PORT: PortType = T::PORT;
503    const JIT: Option<JitType> = None;
504    fn extract(v: &Value) -> Self {
505        match v {
506            Value::None => None,
507            _ => Some(T::extract(v)),
508        }
509    }
510    fn inject(self) -> Value {
511        match self {
512            None => Value::None,
513            Some(t) => t.inject(),
514        }
515    }
516}
517
518/// Operator-side wrapper for adapter-typed wire arguments.
519/// `Ext<T>` signals "this arg comes from `Value::Ext(Box<dyn
520/// ReflectedValue>)`; downcast it to `T`." Implements `Deref` /
521/// `DerefMut` like [`Const<T>`] so the body can use `.method()`
522/// directly.
523#[derive(Clone)]
524pub struct Ext<T>(pub T);
525
526impl<T> std::ops::Deref for Ext<T> {
527    type Target = T;
528    fn deref(&self) -> &T {
529        &self.0
530    }
531}
532
533impl<T> std::ops::DerefMut for Ext<T> {
534    fn deref_mut(&mut self) -> &mut T {
535        &mut self.0
536    }
537}
538
539impl<T: ReflectedValue + Clone + 'static> Wire for Ext<T> {
540    const PORT: PortType = PortType::Ext;
541    const JIT: Option<JitType> = None;
542    fn extract(v: &Value) -> Self {
543        match v {
544            Value::Ext(boxed) => {
545                let any = boxed.as_any();
546                match any.downcast_ref::<T>() {
547                    Some(t) => Ext(t.clone()),
548                    None => panic!(
549                        "Wire<Ext<{}>>::extract: ReflectedValue downcast failed; \
550                         got runtime type {:?}",
551                        std::any::type_name::<T>(),
552                        boxed.type_name()
553                    ),
554                }
555            }
556            other => panic!("Wire<Ext>::extract: expected Ext, got {other:?}"),
557        }
558    }
559    fn inject(self) -> Value {
560        Value::Ext(Box::new(self.0))
561    }
562}
563
564// ── DynamicOutputs<T> — variable output port count ────────────
565
566/// Marker wrapper for node return types whose output port
567/// COUNT is determined at construction time from a
568/// `Const<Vec<C>>` arg's length, not at codegen time.
569/// SRD-80b shape extension covering nodes like `mixed_radix`
570/// that emit one output per radix where `radix` count is a
571/// workload-supplied list.
572///
573/// Operator writes:
574///
575/// ```ignore
576/// #[polydat_node(category = Arithmetic)]
577/// fn mixed_radix(
578///     value: u64,
579///     radixes: Const<Vec<u64>>,
580/// ) -> DynamicOutputs<u64> {
581///     // body returns DynamicOutputs(Vec<u64>) with len == radixes.len()
582/// }
583/// ```
584///
585/// The macro emits one output port per element (named `d0`,
586/// `d1`, ...) at construction time using the `Const<Vec<C>>`
587/// arg's length. `FuncSig.outputs` is `0` signalling dynamic.
588/// Requires exactly one `Const<Vec<C>>` arg per function; the
589/// macro errors at compile time otherwise.
590pub struct DynamicOutputs<T>(pub Vec<T>);
591
592impl<T> std::ops::Deref for DynamicOutputs<T> {
593    type Target = Vec<T>;
594    fn deref(&self) -> &Vec<T> {
595        &self.0
596    }
597}
598
599// ── Config<T> — wire arg marked as config-cost ────────────────
600
601/// Marker wrapper signalling that the wrapped wire is a
602/// configuration input — expensive to change because the node
603/// keeps internal state (LUTs, alias tables, parsed specs)
604/// derived from it. The macro emits the matching slot with
605/// `Port::config()` (SRD 15 §"WireCost::Config") so the
606/// compiler warns on cycle-time binding.
607///
608/// In-spirit replacement for a `#[wire_cost(Config)]` arg-level
609/// attribute — operator declares the cost intent via the type
610/// system. Body unwraps with `.0` or via `Deref`.
611pub struct Config<T>(pub T);
612
613impl<T> std::ops::Deref for Config<T> {
614    type Target = T;
615    fn deref(&self) -> &T {
616        &self.0
617    }
618}
619
620impl<T: Wire> Wire for Config<T> {
621    const PORT: PortType = T::PORT;
622    const JIT: Option<JitType> = T::JIT;
623    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = T::RESOLVER;
624    const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Config;
625    fn extract(v: &Value) -> Self {
626        Config(T::extract(v))
627    }
628    fn inject(self) -> Value {
629        self.0.inject()
630    }
631}
632
633// ── Resolved<R, T> — Handle wire with SRD-53 auto-resolver ────
634
635/// SRD-80b in-spirit replacement for the `default_resolver`
636/// attribute. The `R` parameter (a [`ResolverKind`] impl) carries
637/// the auto-resolver kind; the `T` parameter is the concrete
638/// `Handle`-inner type the body sees.
639///
640/// Operators write:
641///
642/// ```ignore
643/// fn matching_profiles(
644///     group: Resolved<GroupResolver, vectordata::TestDataGroup>,
645///     prefix: &str,
646/// ) -> Vec<String> {
647///     let group: &vectordata::TestDataGroup = &group;
648///     // ... use group methods directly
649/// }
650/// ```
651///
652/// The macro reads `<Resolved<GroupResolver, T> as Wire>::RESOLVER`
653/// at codegen time and emits the matching `FuncSig.default_resolver`.
654/// No `#[polydat_node(default_resolver = ...)]` attribute is
655/// involved — the resolver information lives in the function
656/// signature where it belongs.
657pub struct Resolved<R: ResolverKind, T: 'static + Send + Sync> {
658    inner: std::sync::Arc<T>,
659    _r: std::marker::PhantomData<fn() -> R>,
660}
661
662impl<R: ResolverKind, T: 'static + Send + Sync> std::ops::Deref for Resolved<R, T> {
663    type Target = T;
664    fn deref(&self) -> &T {
665        &self.inner
666    }
667}
668
669impl<R: ResolverKind, T: 'static + Send + Sync> Resolved<R, T> {
670    /// Construct from a pre-resolved Arc — useful for tests
671    /// and programmatic graph assembly that bypasses the DSL
672    /// auto-resolver.
673    pub fn from_arc(inner: std::sync::Arc<T>) -> Self {
674        Self {
675            inner,
676            _r: std::marker::PhantomData,
677        }
678    }
679    /// Borrow the inner Arc.
680    pub fn as_arc(&self) -> &std::sync::Arc<T> {
681        &self.inner
682    }
683}
684
685/// Marker trait that names a kind of source-string auto-resolver
686/// for [`Resolved<R, T>`] wire args. The variants here mirror
687/// [`crate::dsl::registry::DefaultResolver`]; each impl picks
688/// one of them.
689pub trait ResolverKind: 'static {
690    /// The resolver this kind names.
691    const RESOLVER: crate::dsl::registry::DefaultResolver;
692}
693
694/// Splice `dataset_group_open(<source>)` upstream when the wire
695/// source is a `Str` (SRD-53 `DefaultResolver::Group`).
696pub struct GroupResolver;
697impl ResolverKind for GroupResolver {
698    const RESOLVER: crate::dsl::registry::DefaultResolver =
699        crate::dsl::registry::DefaultResolver::Group;
700}
701
702impl<R: ResolverKind, T: 'static + Send + Sync> Wire for Resolved<R, T> {
703    const PORT: PortType = PortType::Handle;
704    const JIT: Option<JitType> = None;
705    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> =
706        Some(<R as ResolverKind>::RESOLVER);
707    fn extract(v: &Value) -> Self {
708        match v {
709            Value::Handle(arc) => {
710                let inner = arc.clone().downcast::<T>().unwrap_or_else(|_| {
711                    panic!(
712                        "Wire<Resolved<_, {}>>::extract: Handle downcast failed",
713                        std::any::type_name::<T>()
714                    )
715                });
716                Resolved {
717                    inner,
718                    _r: std::marker::PhantomData,
719                }
720            }
721            other => panic!("Wire<Resolved>::extract: expected Handle, got {other:?}"),
722        }
723    }
724    fn inject(self) -> Value {
725        Value::Handle(self.inner)
726    }
727}
728
729// =====================================================================
730// ConstSource — ConstArg → typed extraction (owned types only)
731// =====================================================================
732
733/// `ConstArg` → typed-Rust-value bridge for owned types in
734/// `Const<T>` position.
735///
736/// Borrow shapes (`Const<&str>`) are handled by the macro at
737/// codegen — it stores `String` via `ConstSource for String`
738/// and emits `Const(self.field.as_str())` at the eval call site
739/// to satisfy the operator-side `Const<&str>` signature.
740pub trait ConstSource: Sized + 'static {
741    /// The slot type the constant occupies.
742    const SLOT: SlotType;
743    /// The value from a build-time constant argument.
744    fn extract(arg: &ConstArg) -> Self;
745}
746
747impl ConstSource for u64 {
748    const SLOT: SlotType = SlotType::ConstU64;
749    fn extract(arg: &ConstArg) -> Self {
750        match arg {
751            ConstArg::Int(v) => *v,
752            other => panic!("ConstSource<u64>::extract: expected Int, got {other:?}"),
753        }
754    }
755}
756
757impl ConstSource for f64 {
758    const SLOT: SlotType = SlotType::ConstF64;
759    fn extract(arg: &ConstArg) -> Self {
760        match arg {
761            ConstArg::Float(v) => *v,
762            ConstArg::Int(v) => *v as f64,
763            other => panic!("ConstSource<f64>::extract: expected Float or Int, got {other:?}"),
764        }
765    }
766}
767
768impl ConstSource for bool {
769    const SLOT: SlotType = SlotType::ConstU64;
770    fn extract(arg: &ConstArg) -> Self {
771        match arg {
772            ConstArg::Int(v) => *v != 0,
773            other => panic!("ConstSource<bool>::extract: expected Int, got {other:?}"),
774        }
775    }
776}
777
778impl ConstSource for String {
779    const SLOT: SlotType = SlotType::ConstStr;
780    fn extract(arg: &ConstArg) -> Self {
781        match arg {
782            ConstArg::Str(s) => s.clone(),
783            other => panic!("ConstSource<String>::extract: expected Str, got {other:?}"),
784        }
785    }
786}
787
788/// SRD-80b Phase C — workload-list const combinator. Used by the
789/// macro when it sees `Const<Vec<C>>` in an operator's signature
790/// (e.g. `Const<Vec<u64>>`, `Const<Vec<String>>`). The macro
791/// emits one `Slot::Const { name, slot_type: ConstVec, .. }`
792/// for the position and packages the trailing `consts[..]`
793/// slice into a `ConstArg::List` at build time; this impl
794/// walks the list, dispatching `C::extract` per element.
795impl<C: ConstSource> ConstSource for Vec<C> {
796    const SLOT: SlotType = SlotType::ConstVec;
797    fn extract(arg: &ConstArg) -> Self {
798        match arg {
799            ConstArg::List(items) => items.iter().map(C::extract).collect(),
800            other => panic!("ConstSource<Vec<_>>::extract: expected List, got {other:?}"),
801        }
802    }
803}
804
805// FromValue / IntoValue retired 2026-06-05 — the `#[polydat_node]`
806// macro now dispatches every owned type through `<T as Wire>::extract`
807// / `::inject` and emits direct `match`-on-`Value` extraction for
808// borrow shapes (`&str`, `&[u8]`, `&[T]`, `&serde_json::Value`).
809// Per SRD-80b Phase B; the old trait pair plus their borrow-impls'
810// `unsafe { transmute }` lifetime-extension hack are gone.
811//
812// [PLACEHOLDER_PHASE_B_DELETE]
813
814/// SRD-80 PR B.5 — marker wrapper for const arguments in
815/// `#[polydat_node]` function signatures.
816///
817/// Use in arg position to signal that the value is captured at
818/// node-construction time (assembly-time) rather than read
819/// per-cycle from a wire. The macro detects `Const<T>` in arg
820/// position and:
821///
822/// - Emits `Slot::Const { ... }` (not `Slot::Wire`) in the
823///   node's NodeMeta.
824/// - Emits `SlotType::ConstU64` / `ConstF64` / `ConstStr` in
825///   the corresponding `FuncSig.params` entry (the const
826///   variant matching `T`).
827/// - Adds a struct field to hold the captured value.
828/// - Generates a `new(const_values...)` constructor.
829/// - Wires the build closure to pull from `consts: &[ConstArg]`
830///   and pass values to `new()`.
831/// - Constructs a `Const<T>(...)` wrapper around the struct
832///   field at eval time so the user's function body sees the
833///   wrapped type matching its signature.
834///
835/// Body code accesses the wrapped value via `.0` or via the
836/// `Deref` impl below:
837///
838/// ```ignore
839/// #[polydat_node(category = String)]
840/// fn combinations(input: u64, pattern: Const<&str>) -> String {
841///     apply(input, pattern.0)  // pattern.0 is &str
842/// }
843/// ```
844///
845/// Type-shape dispatch table:
846///
847/// | `Const<T>` form | `SlotType` variant | Struct field type | ConstArg accessor |
848/// |---|---|---|---|
849/// | `Const<u64>`  | `ConstU64`  | `u64`    | `as_u64()`  |
850/// | `Const<f64>`  | `ConstF64`  | `f64`    | `as_f64()`  |
851/// | `Const<bool>` | `ConstU64`  | `bool`   | `as_u64() != 0` |
852/// | `Const<&str>` | `ConstStr`  | `String` | `as_str().to_string()` |
853pub struct Const<T>(pub T);
854
855impl<T> std::ops::Deref for Const<T> {
856    type Target = T;
857    fn deref(&self) -> &T {
858        &self.0
859    }
860}
861
862impl<T> std::ops::DerefMut for Const<T> {
863    fn deref_mut(&mut self) -> &mut T {
864        &mut self.0
865    }
866}
867
868/// SRD-80 PR B.6 — construction-time setup contract for nodes
869/// that derive a pre-computed runtime state from their const
870/// args (e.g. `combinations` parsing a charset pattern into
871/// segments + modulus, `regex_match` compiling a pattern,
872/// `histribution` parsing a distribution spec).
873///
874/// **The contract**: the operator-provided setup function is
875/// called EXACTLY ONCE per node instance, at construction time
876/// (`new()`). Its result is stored in a struct field; eval-
877/// time access is a plain `&T` borrow.
878///
879/// **Type-level enforcement**: the `#[poly_const(...)]`
880/// attribute on a `&T` argument tells the macro to generate
881/// this construction pattern. The macro is the sole party
882/// emitting `setup_fn(...)` calls and it generates the call
883/// exactly once inside `new()`. The contract is inviolable
884/// because no other code path can reach the setup function —
885/// the macro hides it inside the constructor.
886///
887/// In effect, the function pointer behaves as `FnOnce` —
888/// invoked one time, by one site, never again. The FnOnce
889/// semantics aren't expressed as a trait bound because they
890/// don't need to be: the macro is the only caller, and the
891/// macro respects single-call by construction.
892///
893/// Library author idiom:
894///
895/// ```ignore
896/// pub struct ParsedPattern {
897///     pub segments: Vec<Segment>,
898///     pub modulus: u64,
899/// }
900///
901/// impl ParsedPattern {
902///     /// Single-call setup. Macro invokes once in `new()`.
903///     fn from_pattern(pattern: &str) -> Self { /* parse */ }
904/// }
905///
906/// #[polydat_node(category = String)]
907/// fn combinations(
908///     input: u64,
909///     pattern: Const<&str>,
910///     #[poly_const(ParsedPattern::from_pattern, from = pattern)]
911///     parsed: &ParsedPattern,
912/// ) -> String {
913///     // parsed is a borrow of the cached struct field —
914///     // no recomputation, no clone, no ceremony at the call site.
915///     let mut r = input % parsed.modulus;
916///     /* ... */
917/// }
918/// ```
919///
920/// Marker trait — purely a documentation handle for types
921/// intended to be polydat-setup targets. The macro doesn't
922/// dispatch on this; the attribute is the dispatch surface.
923/// Implementing the trait gives library authors a way to
924/// signal intent and improve `cargo doc` discoverability.
925pub trait PolydatSetup {}
926
927// ── The slot kit's run-time helpers ──────────────────────────────
928// Called by the closures `#[polydat_node]` emits for its `compiled_slot`
929// kit; public because generated code in other crates calls them, not
930// because hosts should.
931
932/// The value a `Ref2` pair at the head of `slots` holds by reference:
933/// the one-element slice a JSON, extension, or handle producer
934/// published (jit_boundary.md, axiom S7: one dereference).
935///
936/// The slots must hold a pair a producer published into storage that
937/// is alive: its own scratch, an extern's stored value, or a boundary
938/// value alive for the call (axioms S3, S4). A pair of length zero,
939/// an unset extern, reads as [`Value::None`].
940#[inline]
941pub fn ref_value(slots: &[u64]) -> &Value {
942    static NONE: Value = Value::None;
943    if slots.get(1).copied().unwrap_or(0) == 0 {
944        return &NONE;
945    }
946    // SAFETY: as documented; the producer's storage outlives the read.
947    unsafe { &*(slots[0] as usize as *const Value) }
948}
949
950/// A polymorphic port's slots as the owned `Value` the wire type
951/// names: a scalar from its bits, a `Ref2` kind copied out of the
952/// pair its producer published.
953#[inline]
954pub fn read_poly(ty: PortType, slots: &[u64]) -> Value {
955    crate::compile::marshal::decode_slot(slots, ty)
956}
957
958/// A polymorphic return written by the node's resolved output type: a
959/// scalar as its bits into `outputs[0]`, a `Ref2` kind into
960/// `scratch[0]` with its pair republished (axiom S3). The value must
961/// be of the port's type: the graph colored the slot by the node's
962/// resolved output type, and a value of another type would be read by
963/// every consumer as something it is not, where the interpreter would
964/// have carried it. A `None` has no slot form on a compiled engine
965/// (engine_parity.md, A12).
966#[inline]
967pub fn write_poly(
968    ty: PortType,
969    v: Value,
970    scratch: &mut [crate::ast::ScratchBuf],
971    outputs: &mut [u64],
972) {
973    use crate::ast::ScratchBuf;
974    if v.port_type() != ty {
975        panic!(
976            "a node produced a {:?} on an output the graph typed {:?}; a compiled engine \
977             cannot carry a value of another type than the slot's (engine_parity.md, A7)",
978            v.port_type(),
979            ty
980        );
981    }
982    match v {
983        Value::U64(x) => outputs[0] = x,
984        Value::I64(x) => outputs[0] = x as u64,
985        Value::F64(x) => outputs[0] = x.to_bits(),
986        Value::Bool(b) => outputs[0] = b as u64,
987        Value::Str(s) => scratch[0].set_str(&s),
988        Value::Bytes(b) => scratch[0].set_bytes(&b),
989        Value::Json(_) | Value::Ext(_) | Value::Handle(_) => scratch[0].set_value(v),
990        other => panic!("a {:?} value has no compiled slot form", other.port_type()),
991    }
992    if matches!(
993        scratch.first(),
994        Some(ScratchBuf::Str(_) | ScratchBuf::Bytes(_) | ScratchBuf::Value(_))
995    ) && ty.slot_color() == crate::ast::SlotColor::Ref2
996    {
997        let (p, l) = scratch[0].ptr_len();
998        outputs[0] = p;
999        outputs[1] = l;
1000    }
1001}
1002
1003// SRD-80 PR B.2/B.3 — macro-generated nodes register through
1004// the existing `NodeRegistration` inventory channel
1005// (`polydat::dsl::registry::NodeRegistration`), the same
1006// channel `register_nodes!` already uses. The proc-macro
1007// emits a `NodeRegistration` per `#[polydat_node]` site, so
1008// every consumer that already iterates the registry
1009// (`registry()`, `lookup()`, the compile pipeline's
1010// `factory::build_node`) sees macro-generated nodes
1011// automatically — no parallel collection, no separate dispatch
1012// surface. See `polydat::dsl::registry` for the load-bearing
1013// data structures.