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