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