Skip to main content

polydat_core/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::ast::{PortType, Value};
14use crate::kernel::{Construction, Dataflow, Metadata, PolydatKernel};
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()
30            .output_names()
31            .iter()
32            .map(|s| s.to_string())
33            .collect()
34    }
35
36    #[inline]
37    fn coord_count(&self) -> usize {
38        self.program().coord_count()
39    }
40
41    #[inline]
42    fn input_port_type(&self, name: &str) -> Option<PortType> {
43        self.program().input_port_type(name)
44    }
45
46    #[inline]
47    fn input_port_type_by_idx(&self, idx: usize) -> Option<PortType> {
48        self.program().input_port_type_by_idx(idx)
49    }
50
51    #[inline]
52    fn output_port_type(&self, name: &str) -> Option<PortType> {
53        self.program().output_port_type(name)
54    }
55}
56
57impl Dataflow for PolydatKernel {
58    fn set_wire_idx(
59        &mut self,
60        idx: usize,
61        value: Value,
62    ) -> Result<(), crate::kernel::api::WriteError> {
63        use crate::kernel::api::WriteError;
64
65        // Look up the slot's declared port type. An out-of-range
66        // index is an unknown-wire error rather than a panic —
67        // the typed boundary surfaces the diagnostic uniformly
68        // for callers who computed the index from external
69        // metadata.
70        let slot_type = match self.program().input_port_type_by_idx(idx) {
71            Some(t) => t,
72            None => {
73                return Err(WriteError::UnknownWire {
74                    key: format!("wire[{idx}]"),
75                });
76            }
77        };
78
79        let slot_name = self
80            .program()
81            .input_name_by_idx(idx)
82            .map(|s| s.to_string())
83            .unwrap_or_else(|| format!("wire[{idx}]"));
84
85        // Per S4 / T2: try direct write, fall back to the
86        // boundary auto-adapter, then report a TypeMismatch
87        // diagnostic if no adapter healed the mismatch. The
88        // `adapt_boundary_value` helper currently passes
89        // unhealable mismatches through with a warning; here we
90        // detect that case by checking whether the adapted value
91        // still has the wrong port type, and surface a typed
92        // error instead of letting silent corruption propagate
93        // to downstream readers.
94        let got = value.port_type();
95        let adapted = crate::kernel::state::adapt_boundary_value(&slot_name, slot_type, value);
96        // `Value::None` is the absent sentinel — always permitted
97        // regardless of slot type (per none_semantics.md / SRD-74
98        // Rule 1). For non-None values, the residual check uses
99        // the bit-stuffing equivalence helper
100        // (`Value::satisfies_slot`) so a narrowing adapter that
101        // outputs `Value::U64` for a U32 slot — the runtime
102        // bit-stuffed form per type_system.md §1 — passes
103        // validation. The pre-adapter check in
104        // `adapt_boundary_value` remains strict, so an unadapted
105        // Value::U64 cannot silently truncate into a U32 slot.
106        if !adapted.satisfies_slot(slot_type) {
107            return Err(WriteError::TypeMismatch {
108                slot: slot_name,
109                expected: slot_type,
110                got,
111            });
112        }
113        self.state().set_input(idx, adapted);
114        Ok(())
115    }
116
117    #[inline]
118    fn get_wire_idx(&self, idx: usize) -> Value {
119        self.state_ref().get_input(idx)
120    }
121}
122
123impl Construction for PolydatKernel {
124    type Error = crate::kernel::subcontext::ContractViolation;
125
126    fn root(matter: crate::kernel::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error> {
127        use crate::kernel::subcontext::PolydatMatterInner;
128        match matter.inner {
129            PolydatMatterInner::Source(s) => {
130                let options = crate::dsl::compile::CompileOptions {
131                    source_dir: s.options.workload_dir.clone(),
132                    lib_paths: s.options.polydat_lib_paths,
133                    required_outputs: s.options.required_outputs.clone(),
134                    strict: s.options.strict,
135                    context: s
136                        .options
137                        .context_label
138                        .clone()
139                        .unwrap_or_else(|| s.label.clone()),
140                    cursor_limit: s.options.cursor_limit,
141                };
142                crate::dsl::compile::compile_polydat_with_options(&s.body, &options, None)
143                    .map_err(crate::kernel::subcontext::ContractViolation::Compile)
144            }
145            PolydatMatterInner::Statements(s) => {
146                // Pre-parsed AST — go through the compile-from-AST
147                // path. The `PolydatFile` AST root takes the statements
148                // verbatim; the same options surface as the source
149                // path.
150                let file = crate::dsl::ast::PolydatFile {
151                    statements: s.statements,
152                };
153                let options = crate::dsl::compile::CompileOptions {
154                    source_dir: s.options.workload_dir.clone(),
155                    lib_paths: s.options.polydat_lib_paths,
156                    required_outputs: s.options.required_outputs.clone(),
157                    strict: s.options.strict,
158                    context: s
159                        .options
160                        .context_label
161                        .clone()
162                        .unwrap_or_else(|| s.label.clone()),
163                    cursor_limit: None,
164                };
165                crate::dsl::compile::compile_ast_with_options(&file, "", &options, None)
166                    .map_err(crate::kernel::subcontext::ContractViolation::Compile)
167            }
168            PolydatMatterInner::Program(p) => {
169                let mut k = PolydatKernel::from_program(p.program);
170                for (var, value) in p.iter_bindings {
171                    if let Some(idx) = k.program().find_input(var) {
172                        k.state().set_input(idx, value.clone());
173                    }
174                }
175                Ok(k)
176            }
177        }
178    }
179
180    fn subscope(
181        &self,
182        matter: crate::kernel::subcontext::PolydatMatter<'_>,
183    ) -> Result<Self, Self::Error> {
184        // Delegate to PolydatKernel's existing typed subscope path.
185        PolydatKernel::build_subscope(self, matter)
186    }
187}
188
189// ── The interpreter kernel on the engine-independent surface ────────
190
191impl crate::kernel::Kernel for PolydatKernel {
192    fn engine(&self) -> crate::compile::select::Engine {
193        crate::compile::select::Engine::Interpreter(self.program().cone_mode())
194    }
195    fn set_inputs(&mut self, coords: &[u64]) {
196        PolydatKernel::set_inputs(self, coords);
197    }
198    fn set_input(&mut self, name: &str, value: Value) -> Result<(), String> {
199        PolydatKernel::set_input(self, name, value)
200    }
201    fn set_cursor(
202        &mut self,
203        name: &str,
204        partition: &crate::iteration::cursor_partition::Partition,
205    ) -> Result<(), String> {
206        PolydatKernel::set_cursor(self, name, partition)
207    }
208    /// Every output is pulled, so what a side channel observes is what
209    /// it observes on a compiled kernel's run.
210    fn eval(&mut self) {
211        for name in Metadata::output_names(self) {
212            let _ = PolydatKernel::pull(self, &name);
213        }
214    }
215    fn pull(&mut self, name: &str) -> Value {
216        PolydatKernel::pull(self, name).clone()
217    }
218    fn input_names(&self) -> Vec<String> {
219        Metadata::input_names(self)
220    }
221    fn output_names(&self) -> Vec<String> {
222        Metadata::output_names(self)
223    }
224    fn output_type(&self, name: &str) -> Option<PortType> {
225        Metadata::output_port_type(self, name)
226    }
227    fn externs(&self) -> Vec<(String, PortType)> {
228        let program = self.program();
229        Metadata::input_names(self)
230            .into_iter()
231            .enumerate()
232            .filter(|(i, _)| program.input_kind(*i) != Some(crate::kernel::InputKind::Coordinate))
233            .filter_map(|(i, name)| Metadata::input_port_type_by_idx(self, i).map(|t| (name, t)))
234            .collect()
235    }
236    fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
237        self.program().cursor_schemas()
238    }
239    fn input_value(&self, name: &str) -> Option<Value> {
240        let idx = self.program().find_input(name)?;
241        Some(self.state_ref().get_input(idx))
242    }
243    fn input_index(&self, name: &str) -> Option<usize> {
244        self.program().find_input(name)
245    }
246    fn set_input_at(&mut self, index: usize, value: Value) -> Result<(), String> {
247        PolydatKernel::set_input_at(self, index, value)
248    }
249    fn output_index(&self, name: &str) -> Option<usize> {
250        self.program().output_index(name)
251    }
252    fn pull_at(&mut self, index: usize) -> Value {
253        PolydatKernel::pull_by_index(self, index).clone()
254    }
255    fn traversals(&self) -> &[crate::dsl::traversal::Traversal] {
256        self.program().traversals()
257    }
258    fn plan(&self) -> crate::EnginePlan {
259        self.program().engine_plan()
260    }
261    fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String> {
262        PolydatKernel::traverse(self, index)
263    }
264    fn invalidate_all(&mut self) {
265        self.state().invalidate_all();
266    }
267    fn shared_cells(&self) -> Vec<crate::kernel::SharedCellEntry> {
268        self.shared_cells_in_scope()
269    }
270    fn attach_shared_cell(
271        &mut self,
272        name: &str,
273        cell: crate::kernel::SharedCell,
274    ) -> Result<(), String> {
275        let program = self.program().clone();
276        let shared = program.shared_outputs();
277        let idx = program.find_input(name).filter(|_| shared.contains(&name));
278        let Some(idx) = idx else {
279            return Err(format!(
280                "no `shared` binding named '{name}'; this kernel's shared bindings are {shared:?}"
281            ));
282        };
283        self.state().attach_shared_cell(idx, cell);
284        Ok(())
285    }
286    fn into_program(self: Box<Self>) -> std::sync::Arc<dyn crate::kernel::KernelProgram> {
287        PolydatKernel::into_program(*self)
288    }
289}
290
291impl crate::kernel::KernelInternals for PolydatKernel {
292    fn set_traversals(
293        &mut self,
294        traversals: Vec<crate::dsl::traversal::Traversal>,
295        producers: Vec<crate::dsl::traversal::Producer>,
296    ) {
297        PolydatKernel::set_traversals(self, traversals, producers);
298    }
299    fn folded_value(&self, name: &str) -> Option<Value> {
300        self.get_constant(name).cloned()
301    }
302    fn set_cursor_extent(&mut self, index: usize, extent: u64) {
303        let mut schemas = self.program().cursor_schemas().to_vec();
304        if let Some(schema) = schemas.get_mut(index) {
305            schema.extent = Some(extent);
306            self.set_cursor_schemas(schemas);
307        }
308    }
309}
310
311impl crate::kernel::KernelProgram for crate::kernel::PolydatProgram {
312    fn engine(&self) -> crate::compile::select::Engine {
313        crate::compile::select::Engine::Interpreter(self.cone_mode())
314    }
315    fn as_interpreter(self: std::sync::Arc<Self>) -> Option<std::sync::Arc<Self>> {
316        Some(self)
317    }
318    fn create_kernel(self: std::sync::Arc<Self>) -> Box<dyn crate::kernel::Kernel> {
319        Box::new(PolydatKernel::from_program(self))
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::dsl::compile::compile_polydat;
327
328    /// Indexed wire access works.
329    #[test]
330    fn dataflow_indexed_set_get() {
331        let mut k = compile_polydat("input cycle: u64\nconst x := 7\n").unwrap();
332        // cycle is index 0
333        k.set_wire(0_usize, Value::U64(42)).expect("typed write");
334        assert_eq!(k.get_wire(0_usize), Some(Value::U64(42)));
335    }
336
337    /// Named wire access resolves through metadata.
338    #[test]
339    fn dataflow_named_set_get() {
340        let mut k = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
341        k.set_wire("n", Value::U64(5)).expect("typed write");
342        match k.get_wire("n") {
343            Some(Value::U64(5)) => {}
344            other => panic!("expected U64(5), got {other:?}"),
345        }
346    }
347
348    /// String key works alongside &str.
349    #[test]
350    fn dataflow_string_key() {
351        let mut k = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
352        let name = String::from("n");
353        k.set_wire(&name, Value::U64(99)).expect("typed write");
354        assert_eq!(k.get_wire(name.clone()), Some(Value::U64(99)));
355    }
356
357    /// Unknown name returns Err(UnknownWire) / None — no panic.
358    #[test]
359    fn dataflow_unknown_name_safe() {
360        let mut k = compile_polydat("input cycle: u64\n").unwrap();
361        let err = k.set_wire("nonexistent", Value::U64(1)).unwrap_err();
362        assert!(matches!(
363            err,
364            crate::kernel::api::WriteError::UnknownWire { .. }
365        ));
366        assert!(k.get_wire("nonexistent").is_none());
367    }
368
369    /// S4 type-check: writing the wrong Value variant to a typed
370    /// slot returns Err(TypeMismatch) when no boundary adapter
371    /// can heal the mismatch.
372    ///
373    /// `VecF32 → U64` is intentionally absent from the polyfill
374    /// matrix (type_system.md §3 — collection → scalar requires
375    /// explicit choice), so it is a stable "no adapter exists"
376    /// pair for testing the diagnostic.
377    #[test]
378    fn dataflow_type_mismatch_rejected() {
379        let mut k = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
380        let err = k
381            .set_wire(
382                "n",
383                Value::VecF32(crate::ast::SliceArc::from_vec(vec![1.0_f32, 2.0])),
384            )
385            .unwrap_err();
386        match err {
387            crate::kernel::api::WriteError::TypeMismatch {
388                slot,
389                expected,
390                got,
391            } => {
392                assert_eq!(slot, "n");
393                assert_eq!(expected, PortType::U64);
394                assert_eq!(got, PortType::VecF32);
395            }
396            other => panic!("expected TypeMismatch, got {other:?}"),
397        }
398    }
399
400    /// The `WriteError::TypeMismatch` Display impl includes a
401    /// vec → scalar hint pointing at the explicit helpers when
402    /// the rejected `got` is a Vec type and the `expected` is
403    /// not a collection-compatible type.
404    #[test]
405    fn vec_to_scalar_diagnostic_mentions_explicit_helpers() {
406        let err = crate::kernel::api::WriteError::TypeMismatch {
407            slot: "score".into(),
408            expected: PortType::F64,
409            got: PortType::VecF32,
410        };
411        let msg = err.to_string();
412        assert!(msg.contains("vec_len"), "missing vec_len hint: {msg}");
413        assert!(msg.contains("vec_first"), "missing vec_first hint: {msg}");
414    }
415
416    /// S4 type-adapt: a healable mismatch (u64 → f64) routes
417    /// through the boundary auto-adapter rather than rejecting.
418    #[test]
419    fn dataflow_healable_mismatch_adapts() {
420        let mut k = compile_polydat("input cycle: u64\nextern x: f64\n").unwrap();
421        // u64 → f64 has an auto-adapter (lossless widening); the
422        // typed-write API should accept this transparently.
423        k.set_wire("x", Value::U64(42))
424            .expect("u64→f64 boundary adapter");
425        match k.get_wire("x") {
426            Some(Value::F64(42.0)) => {}
427            other => panic!("expected adapted F64(42.0), got {other:?}"),
428        }
429    }
430
431    /// S4 None pass-through: Value::None is the absent sentinel
432    /// and always permitted at the boundary regardless of slot
433    /// type (per none_semantics.md).
434    #[test]
435    fn dataflow_none_passes_through_any_slot() {
436        let mut k = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
437        k.set_wire("n", Value::None).expect("None always permitted");
438    }
439
440    /// Metadata trait surfaces names + types.
441    #[test]
442    fn metadata_listings() {
443        let k = compile_polydat("input (cycle: u64, thread: u64)\nextern n: u64\nconst x := 7\n")
444            .unwrap();
445        let inputs: Vec<String> = k.input_names();
446        assert!(inputs.iter().any(|s| s == "cycle"));
447        assert!(inputs.iter().any(|s| s == "n"));
448        assert_eq!(k.coord_count(), 2); // cycle + thread
449        assert!(k.find_input("n").is_some());
450        assert_eq!(k.input_port_type("n"), Some(PortType::U64));
451    }
452
453    /// Construction trait — both paths take the same polydat
454    /// matter type. Verify symmetry: root from source, then
455    /// subscope from source against the root.
456    #[test]
457    fn construction_symmetric_paths() {
458        let root_opts = crate::kernel::subcontext::CompileOptions {
459            workload_dir: None,
460            polydat_lib_paths: Vec::new(),
461            strict: false,
462            required_outputs: Vec::new(),
463            context_label: Some("root".to_string()),
464            cursor_limit: None,
465            ..Default::default()
466        };
467        let root_matter = crate::kernel::subcontext::PolydatMatter::builder()
468            .label("root")
469            .source("input cycle: u64\nshared flag := 0\n")
470            .options(root_opts)
471            .build()
472            .expect("matter build");
473        let root =
474            <PolydatKernel as Construction>::root(root_matter).expect("root from source matter");
475
476        let sub_opts = crate::kernel::subcontext::CompileOptions {
477            workload_dir: None,
478            polydat_lib_paths: Vec::new(),
479            strict: false,
480            required_outputs: Vec::new(),
481            context_label: Some("sub".to_string()),
482            cursor_limit: None,
483            ..Default::default()
484        };
485        let sub_matter = crate::kernel::subcontext::PolydatMatter::builder()
486            .label("sub")
487            .source("input cycle: u64\n")
488            .options(sub_opts)
489            .build()
490            .expect("matter build");
491        let _sub = root
492            .subscope(sub_matter)
493            .expect("subscope from source matter");
494    }
495
496    /// Root construction also accepts pre-compiled program
497    /// matter (re-instance with fresh state). Verifies via
498    /// the input slot — `n` is an extern input.
499    #[test]
500    fn construction_root_from_program() {
501        let template = compile_polydat("input cycle: u64\nextern n: u64\n").unwrap();
502        let program = template.program().clone();
503        let matter = crate::kernel::subcontext::PolydatMatter::builder()
504            .program(program)
505            .build()
506            .expect("matter build");
507        let mut root =
508            <PolydatKernel as Construction>::root(matter).expect("root from program matter");
509        root.set_wire("n", Value::U64(13)).expect("set_wire");
510        assert_eq!(root.get_wire("n"), Some(Value::U64(13)));
511    }
512
513    /// Builder rejects ambiguous matter (multiple input forms).
514    #[test]
515    fn builder_rejects_multiple_forms() {
516        let template = compile_polydat("input cycle: u64\n").unwrap();
517        match crate::kernel::subcontext::PolydatMatter::builder()
518            .source("input cycle: u64\n")
519            .program(template.program().clone())
520            .build()
521        {
522            Err(msg) => assert!(
523                msg.contains("multiple"),
524                "expected multiple-forms error, got: {msg}"
525            ),
526            Ok(_) => panic!("multiple forms must error"),
527        }
528    }
529
530    /// Builder rejects empty matter.
531    #[test]
532    fn builder_rejects_empty() {
533        match crate::kernel::subcontext::PolydatMatter::builder().build() {
534            Err(msg) => assert!(
535                msg.contains("no input form"),
536                "expected no-form error, got: {msg}"
537            ),
538            Ok(_) => panic!("empty matter must error"),
539        }
540    }
541}