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