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