Skip to main content

polydat/kernel/
api_impl.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trait implementations of the Polydat context API ([`Metadata`],
5//! [`Dataflow`], [`Construction`]) on [`PolydatKernel`].
6//!
7//! PolydatKernel is the singular caller-facing interface that fuses
8//! the compiled context (program) and per-fiber state. All
9//! external (non-GK-internal) callers should reach the kernel
10//! exclusively through these three traits — `state()` /
11//! `state_ref()` / `program()` are kernel-internal hooks.
12
13use crate::kernel::{Dataflow, PolydatKernel, Metadata, Construction};
14use crate::ast::{PortType, Value};
15
16impl Metadata for PolydatKernel {
17    #[inline]
18    fn find_input(&self, name: &str) -> Option<usize> {
19        self.program().find_input(name)
20    }
21
22    #[inline]
23    fn input_names(&self) -> Vec<String> {
24        self.program().input_names()
25    }
26
27    #[inline]
28    fn output_names(&self) -> Vec<String> {
29        self.program().output_names().iter().map(|s| s.to_string()).collect()
30    }
31
32    #[inline]
33    fn coord_count(&self) -> usize {
34        self.program().coord_count()
35    }
36
37    #[inline]
38    fn input_port_type(&self, name: &str) -> Option<PortType> {
39        self.program().input_port_type(name)
40    }
41
42    #[inline]
43    fn input_port_type_by_idx(&self, idx: usize) -> Option<PortType> {
44        self.program().input_port_type_by_idx(idx)
45    }
46
47    #[inline]
48    fn output_port_type(&self, name: &str) -> Option<PortType> {
49        self.program().output_port_type(name)
50    }
51}
52
53impl Dataflow for PolydatKernel {
54    fn set_wire_idx(&mut self, idx: usize, value: Value) -> Result<(), crate::kernel::api::WriteError> {
55        use crate::kernel::api::WriteError;
56
57        // Look up the slot's declared port type. An out-of-range
58        // index is an unknown-wire error rather than a panic —
59        // the typed boundary surfaces the diagnostic uniformly
60        // for callers who computed the index from external
61        // metadata.
62        let slot_type = match self.program().input_port_type_by_idx(idx) {
63            Some(t) => t,
64            None => return Err(WriteError::UnknownWire { key: format!("wire[{idx}]") }),
65        };
66
67        let slot_name = self.program()
68            .input_name_by_idx(idx)
69            .map(|s| s.to_string())
70            .unwrap_or_else(|| format!("wire[{idx}]"));
71
72        // Per S4 / T2: try direct write, fall back to the
73        // boundary auto-adapter, then report a TypeMismatch
74        // diagnostic if no adapter healed the mismatch. The
75        // `adapt_boundary_value` helper currently passes
76        // unhealable mismatches through with a warning; here we
77        // detect that case by checking whether the adapted value
78        // still has the wrong port type, and surface a typed
79        // error instead of letting silent corruption propagate
80        // to downstream readers.
81        let got = value.port_type();
82        let adapted = crate::kernel::state::adapt_boundary_value(&slot_name, slot_type, value);
83        // `Value::None` is the absent sentinel — always permitted
84        // regardless of slot type (per none_semantics.md / SRD-74
85        // Rule 1). For non-None values, the residual check uses
86        // the bit-stuffing equivalence helper
87        // (`Value::satisfies_slot`) so a narrowing adapter that
88        // outputs `Value::U64` for a U32 slot — the runtime
89        // bit-stuffed form per type_system.md §1 — passes
90        // validation. The pre-adapter check in
91        // `adapt_boundary_value` remains strict, so an unadapted
92        // Value::U64 cannot silently truncate into a U32 slot.
93        if !adapted.satisfies_slot(slot_type) {
94            return Err(WriteError::TypeMismatch {
95                slot: slot_name,
96                expected: slot_type,
97                got,
98            });
99        }
100        self.state().set_input(idx, adapted);
101        Ok(())
102    }
103
104    #[inline]
105    fn get_wire_idx(&self, idx: usize) -> Value {
106        self.state_ref().get_input(idx)
107    }
108}
109
110impl Construction for PolydatKernel {
111    type Error = crate::kernel::subcontext::ContractViolation;
112
113    fn root(matter: crate::kernel::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error> {
114        use crate::kernel::subcontext::PolydatMatterInner;
115        match matter.inner {
116            PolydatMatterInner::Source(s) => {
117                crate::dsl::compile::compile_polydat_with_libs_and_limit(
118                    &s.body,
119                    s.options.workload_dir.as_deref(),
120                    s.options.polydat_lib_paths,
121                    &s.options.required_outputs,
122                    s.options.strict,
123                    s.options.context_label.as_deref().unwrap_or(&s.label),
124                    s.options.cursor_limit,
125                )
126                .map_err(crate::kernel::subcontext::ContractViolation::Compile)
127            }
128            PolydatMatterInner::Statements(s) => {
129                // Pre-parsed AST — go through the compile-from-AST
130                // path. The `PolydatFile` AST root takes the statements
131                // verbatim; the same options surface as the source
132                // path.
133                let file = crate::dsl::ast::PolydatFile { statements: s.statements };
134                crate::dsl::compile::compile_ast_with_libs(
135                    &file,
136                    s.options.workload_dir.as_deref(),
137                    s.options.polydat_lib_paths,
138                    &s.options.required_outputs,
139                    s.options.strict,
140                    s.options.context_label.as_deref().unwrap_or(&s.label),
141                )
142                .map_err(crate::kernel::subcontext::ContractViolation::Compile)
143            }
144            PolydatMatterInner::Program(p) => {
145                let mut k = PolydatKernel::from_program(p.program);
146                for (var, value) in p.iter_bindings {
147                    if let Some(idx) = k.program().find_input(var) {
148                        k.state().set_input(idx, value.clone());
149                    }
150                }
151                Ok(k)
152            }
153        }
154    }
155
156    fn subscope(
157        &self,
158        matter: crate::kernel::subcontext::PolydatMatter<'_>,
159    ) -> Result<Self, Self::Error> {
160        // Delegate to PolydatKernel's existing typed subscope path.
161        PolydatKernel::build_subscope(self, matter)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::dsl::compile::compile_polydat;
169
170    /// Indexed wire access works.
171    #[test]
172    fn dataflow_indexed_set_get() {
173        let mut k = compile_polydat(
174            "input cycle: u64\nconst x := 7\n"
175        ).unwrap();
176        // cycle is index 0
177        k.set_wire(0_usize, Value::U64(42)).expect("typed write");
178        assert_eq!(k.get_wire(0_usize), Some(Value::U64(42)));
179    }
180
181    /// Named wire access resolves through metadata.
182    #[test]
183    fn dataflow_named_set_get() {
184        let mut k = compile_polydat(
185            "input cycle: u64\nextern n: u64\n"
186        ).unwrap();
187        k.set_wire("n", Value::U64(5)).expect("typed write");
188        match k.get_wire("n") {
189            Some(Value::U64(5)) => {}
190            other => panic!("expected U64(5), got {other:?}"),
191        }
192    }
193
194    /// String key works alongside &str.
195    #[test]
196    fn dataflow_string_key() {
197        let mut k = compile_polydat(
198            "input cycle: u64\nextern n: u64\n"
199        ).unwrap();
200        let name = String::from("n");
201        k.set_wire(&name, Value::U64(99)).expect("typed write");
202        assert_eq!(k.get_wire(name.clone()), Some(Value::U64(99)));
203    }
204
205    /// Unknown name returns Err(UnknownWire) / None — no panic.
206    #[test]
207    fn dataflow_unknown_name_safe() {
208        let mut k = compile_polydat("input cycle: u64\n").unwrap();
209        let err = k.set_wire("nonexistent", Value::U64(1)).unwrap_err();
210        assert!(matches!(err, crate::kernel::api::WriteError::UnknownWire { .. }));
211        assert!(k.get_wire("nonexistent").is_none());
212    }
213
214    /// S4 type-check: writing the wrong Value variant to a typed
215    /// slot returns Err(TypeMismatch) when no boundary adapter
216    /// can heal the mismatch.
217    ///
218    /// `VecF32 → U64` is intentionally absent from the polyfill
219    /// matrix (type_system.md §3 — collection → scalar requires
220    /// explicit choice), so it is a stable "no adapter exists"
221    /// pair for testing the diagnostic.
222    #[test]
223    fn dataflow_type_mismatch_rejected() {
224        let mut k = compile_polydat(
225            "input cycle: u64\nextern n: u64\n"
226        ).unwrap();
227        let err = k.set_wire(
228            "n",
229            Value::VecF32(crate::ast::SliceArc::from_vec(vec![1.0_f32, 2.0])),
230        ).unwrap_err();
231        match err {
232            crate::kernel::api::WriteError::TypeMismatch { slot, expected, got } => {
233                assert_eq!(slot, "n");
234                assert_eq!(expected, PortType::U64);
235                assert_eq!(got, PortType::VecF32);
236            }
237            other => panic!("expected TypeMismatch, got {other:?}"),
238        }
239    }
240
241    /// The `WriteError::TypeMismatch` Display impl includes a
242    /// vec → scalar hint pointing at the explicit helpers when
243    /// the rejected `got` is a Vec type and the `expected` is
244    /// not a collection-compatible type.
245    #[test]
246    fn vec_to_scalar_diagnostic_mentions_explicit_helpers() {
247        let err = crate::kernel::api::WriteError::TypeMismatch {
248            slot: "score".into(),
249            expected: PortType::F64,
250            got: PortType::VecF32,
251        };
252        let msg = err.to_string();
253        assert!(msg.contains("vec_len"), "missing vec_len hint: {msg}");
254        assert!(msg.contains("vec_first"), "missing vec_first hint: {msg}");
255    }
256
257    /// S4 type-adapt: a healable mismatch (u64 → f64) routes
258    /// through the boundary auto-adapter rather than rejecting.
259    #[test]
260    fn dataflow_healable_mismatch_adapts() {
261        let mut k = compile_polydat(
262            "input cycle: u64\nextern x: f64\n"
263        ).unwrap();
264        // u64 → f64 has an auto-adapter (lossless widening); the
265        // typed-write API should accept this transparently.
266        k.set_wire("x", Value::U64(42)).expect("u64→f64 boundary adapter");
267        match k.get_wire("x") {
268            Some(Value::F64(42.0)) => {}
269            other => panic!("expected adapted F64(42.0), got {other:?}"),
270        }
271    }
272
273    /// S4 None pass-through: Value::None is the absent sentinel
274    /// and always permitted at the boundary regardless of slot
275    /// type (per none_semantics.md).
276    #[test]
277    fn dataflow_none_passes_through_any_slot() {
278        let mut k = compile_polydat(
279            "input cycle: u64\nextern n: u64\n"
280        ).unwrap();
281        k.set_wire("n", Value::None).expect("None always permitted");
282    }
283
284    /// Metadata trait surfaces names + types.
285    #[test]
286    fn metadata_listings() {
287        let k = compile_polydat(
288            "input (cycle: u64, thread: u64)\nextern n: u64\nconst x := 7\n"
289        ).unwrap();
290        let inputs: Vec<String> = k.input_names();
291        assert!(inputs.iter().any(|s| s == "cycle"));
292        assert!(inputs.iter().any(|s| s == "n"));
293        assert_eq!(k.coord_count(), 2); // cycle + thread
294        assert!(k.find_input("n").is_some());
295        assert_eq!(k.input_port_type("n"), Some(PortType::U64));
296    }
297
298    /// Construction trait — both paths take the same polydat
299    /// matter type. Verify symmetry: root from source, then
300    /// subscope from source against the root.
301    #[test]
302    fn construction_symmetric_paths() {
303        let root_opts = crate::kernel::subcontext::CompileOptions {
304            workload_dir: None,
305            polydat_lib_paths: Vec::new(),
306            strict: false,
307            required_outputs: Vec::new(),
308            context_label: Some("root".to_string()),
309            cursor_limit: None,
310            ..Default::default()
311        };
312        let root_matter = crate::kernel::subcontext::PolydatMatter::builder()
313            .label("root")
314            .source("input cycle: u64\nshared flag := 0\n")
315            .options(root_opts)
316            .build()
317            .expect("matter build");
318        let root = <PolydatKernel as Construction>::root(root_matter)
319            .expect("root from source matter");
320
321        let sub_opts = crate::kernel::subcontext::CompileOptions {
322            workload_dir: None,
323            polydat_lib_paths: Vec::new(),
324            strict: false,
325            required_outputs: Vec::new(),
326            context_label: Some("sub".to_string()),
327            cursor_limit: None,
328            ..Default::default()
329        };
330        let sub_matter = crate::kernel::subcontext::PolydatMatter::builder()
331            .label("sub")
332            .source("input cycle: u64\n")
333            .options(sub_opts)
334            .build()
335            .expect("matter build");
336        let _sub = root
337            .subscope(sub_matter)
338            .expect("subscope from source matter");
339    }
340
341    /// Root construction also accepts pre-compiled program
342    /// matter (re-instance with fresh state). Verifies via
343    /// the input slot — `n` is an extern input.
344    #[test]
345    fn construction_root_from_program() {
346        let template = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
347        let program = template.program().clone();
348        let matter = crate::kernel::subcontext::PolydatMatter::builder()
349            .program(program)
350            .build()
351            .expect("matter build");
352        let mut root = <PolydatKernel as Construction>::root(matter)
353            .expect("root from program matter");
354        root.set_wire("n", Value::U64(13)).expect("set_wire");
355        assert_eq!(root.get_wire("n"), Some(Value::U64(13)));
356    }
357
358    /// Builder rejects ambiguous matter (multiple input forms).
359    #[test]
360    fn builder_rejects_multiple_forms() {
361        let template = compile_polydat("input cycle: u64\n").unwrap();
362        match crate::kernel::subcontext::PolydatMatter::builder()
363            .source("input cycle: u64\n")
364            .program(template.program().clone())
365            .build()
366        {
367            Err(msg) => assert!(msg.contains("multiple"), "expected multiple-forms error, got: {msg}"),
368            Ok(_) => panic!("multiple forms must error"),
369        }
370    }
371
372    /// Builder rejects empty matter.
373    #[test]
374    fn builder_rejects_empty() {
375        match crate::kernel::subcontext::PolydatMatter::builder().build() {
376            Err(msg) => assert!(msg.contains("no input form"), "expected no-form error, got: {msg}"),
377            Ok(_) => panic!("empty matter must error"),
378        }
379    }
380}