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.