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