1use std::collections::BTreeMap;
8use std::ffi::c_char;
9use std::sync::Arc;
10
11use crate::kernel::PortTable;
12use crate::packet::Packet;
13use crate::runtime::{CStrArena, GraphShared};
14use crate::status::{Error, Result};
15use crate::timestamp::Timestamp;
16
17pub struct Options {
19 root: serde_yaml::Value,
20 strings: CStrArena,
21 json: String,
22}
23
24impl Options {
25 pub fn new(root: serde_yaml::Value) -> Self {
26 let json = serde_json::to_string(&root).unwrap_or_else(|_| "{}".to_string());
27 let json = if json == "null" {
28 "{}".to_string()
29 } else {
30 json
31 };
32 Self {
33 root,
34 strings: CStrArena::default(),
35 json,
36 }
37 }
38
39 pub fn get(&self, path: &str) -> Option<&serde_yaml::Value> {
41 let mut cur = &self.root;
42 for seg in path.split('.') {
43 cur = cur.get(seg)?;
44 }
45 Some(cur)
46 }
47
48 pub fn has(&self, path: &str) -> bool {
49 self.get(path).is_some()
50 }
51
52 pub fn i64(&self, path: &str) -> Option<i64> {
53 let v = self.get(path)?;
54 v.as_i64().or_else(|| v.as_u64().map(|u| u as i64))
55 }
56 pub fn f64(&self, path: &str) -> Option<f64> {
57 let v = self.get(path)?;
58 v.as_f64().or_else(|| v.as_i64().map(|i| i as f64))
60 }
61 pub fn bool(&self, path: &str) -> Option<bool> {
62 self.get(path)?.as_bool()
63 }
64 pub fn str(&self, path: &str) -> Option<&str> {
65 self.get(path)?.as_str()
66 }
67
68 pub fn str_cstr(&self, path: &str) -> Option<*const c_char> {
70 self.str(path).map(|s| self.strings.intern(s))
71 }
72
73 pub fn seq(&self, path: &str) -> Option<&Vec<serde_yaml::Value>> {
74 self.get(path)?.as_sequence()
75 }
76
77 pub fn count(&self, path: &str) -> usize {
78 self.seq(path).map_or(0, |s| s.len())
79 }
80
81 pub fn i64_array(&self, path: &str, out: &mut [i64]) -> usize {
82 let Some(seq) = self.seq(path) else { return 0 };
83 for (slot, v) in out.iter_mut().zip(seq.iter()) {
84 *slot = v
85 .as_i64()
86 .or_else(|| v.as_u64().map(|u| u as i64))
87 .unwrap_or(0);
88 }
89 seq.len()
90 }
91
92 pub fn f64_array(&self, path: &str, out: &mut [f64]) -> usize {
93 let Some(seq) = self.seq(path) else { return 0 };
94 for (slot, v) in out.iter_mut().zip(seq.iter()) {
95 *slot = v
96 .as_f64()
97 .or_else(|| v.as_i64().map(|i| i as f64))
98 .unwrap_or(0.0);
99 }
100 seq.len()
101 }
102
103 pub fn str_array(&self, path: &str, out: &mut [*const c_char]) -> usize {
104 let Some(seq) = self.seq(path) else { return 0 };
105 let items: Vec<Option<&str>> = seq.iter().map(|v| v.as_str()).collect();
107 for (slot, item) in out.iter_mut().zip(items.iter()) {
108 *slot = match item {
109 Some(s) => self.strings.intern(s),
110 None => self.strings.intern(""),
111 };
112 }
113 seq.len()
114 }
115
116 pub fn json(&self) -> &str {
117 &self.json
118 }
119 pub fn json_cstr(&self) -> *const c_char {
120 self.strings.intern(&self.json)
121 }
122}
123
124pub struct Context {
126 pub inputs: Vec<Option<Packet>>,
128 pub input_batches: Vec<Vec<Packet>>,
130 pub staging: Vec<Vec<Packet>>,
132 pub next_bounds: Vec<Option<Timestamp>>,
134 pub input_ts: Timestamp,
135
136 pub node_name: String,
137 pub kernel_name: String,
138 pub in_ports: Arc<PortTable>,
139 pub out_ports: Arc<PortTable>,
140 pub options: Arc<Options>,
141 pub side_packets: Arc<BTreeMap<String, Packet>>,
142 pub shared: Arc<GraphShared>,
143 pub inputs_done: Vec<bool>,
145 pub error_msg: Option<String>,
147 pub(crate) output_failed: bool,
149 pub close_reason: i32,
151 pub source_done: bool,
153 pub source_yield: Option<std::time::Duration>,
155 names: CStrArena,
156}
157
158impl Context {
159 pub fn new(
160 node_name: String,
161 kernel_name: String,
162 in_ports: Arc<PortTable>,
163 out_ports: Arc<PortTable>,
164 options: Arc<Options>,
165 side_packets: Arc<BTreeMap<String, Packet>>,
166 shared: Arc<GraphShared>,
167 ) -> Self {
168 let ni = in_ports.len();
169 let no = out_ports.len();
170 Self {
171 inputs: vec![None; ni],
172 input_batches: (0..ni).map(|_| Vec::new()).collect(),
173 staging: (0..no).map(|_| Vec::new()).collect(),
174 next_bounds: vec![None; no],
175 input_ts: Timestamp::unset(),
176 node_name,
177 kernel_name,
178 in_ports,
179 out_ports,
180 options,
181 side_packets,
182 shared,
183 inputs_done: vec![false; ni],
184 error_msg: None,
185 output_failed: false,
186 close_reason: crate::runtime::CLOSE_NORMAL,
187 source_done: false,
188 source_yield: None,
189 names: CStrArena::default(),
190 }
191 }
192
193 pub fn reset(&mut self) {
195 for slot in &mut self.inputs {
196 *slot = None;
197 }
198 for b in &mut self.input_batches {
199 b.clear();
200 }
201 for s in &mut self.staging {
202 s.clear();
203 }
204 self.next_bounds.fill(None);
205 self.input_ts = Timestamp::unset();
206 self.error_msg = None;
207 self.output_failed = false;
208 self.source_done = false;
209 self.source_yield = None;
210 self.inputs_done.fill(false);
214 self.close_reason = crate::runtime::CLOSE_NORMAL;
215 }
216
217 pub fn clear_inputs(&mut self) {
224 for slot in &mut self.inputs {
225 *slot = None;
226 }
227 for b in &mut self.input_batches {
228 b.clear();
229 }
230 }
231
232 pub fn discard_staging(&mut self) {
234 for s in &mut self.staging {
235 s.clear();
236 }
237 self.next_bounds.fill(None);
238 }
239
240 pub fn node_name_cstr(&self) -> *const c_char {
241 self.names.intern(&self.node_name)
242 }
243 pub fn kernel_name_cstr(&self) -> *const c_char {
244 self.names.intern(&self.kernel_name)
245 }
246 pub fn intern(&self, s: &str) -> *const c_char {
247 self.names.intern(s)
248 }
249
250 pub fn input(&self, idx: usize) -> Option<&Packet> {
251 self.input_at(idx, 0)
252 }
253
254 pub fn input_count(&self, idx: usize) -> usize {
256 match self.input_batches.get(idx) {
257 Some(b) if !b.is_empty() => b.len(),
258 _ => self.inputs.get(idx).map_or(0, |s| s.is_some() as usize),
259 }
260 }
261
262 pub fn input_at(&self, idx: usize, k: usize) -> Option<&Packet> {
264 match self.input_batches.get(idx) {
265 Some(b) if !b.is_empty() => b.get(k),
266 _ if k == 0 => self.inputs.get(idx)?.as_ref(),
267 _ => None,
268 }
269 }
270
271 pub fn take_input(&mut self, idx: usize) -> Packet {
274 match self.inputs.get_mut(idx) {
275 Some(slot) => slot.take().unwrap_or_default(),
276 None => Packet::empty(),
277 }
278 }
279
280 pub fn emit(&mut self, out_idx: usize, mut pkt: Packet) -> Result<()> {
281 if out_idx >= self.staging.len() {
282 return Err(Error::InvalidArg(format!(
283 "node `{}`: emit output port index {out_idx} out of range (of {} total)",
284 self.node_name,
285 self.staging.len()
286 )));
287 }
288 if pkt.timestamp() == Timestamp::unset() {
290 pkt.set_timestamp(self.input_ts);
291 }
292 if !pkt.timestamp().is_allowed_in_stream() {
293 return Err(Error::InvalidArg(format!(
294 "node `{}`: emit output port {out_idx} has invalid packet timestamp {}; specify a legal stream timestamp",
295 self.node_name,
296 pkt.timestamp()
297 )));
298 }
299 self.staging[out_idx].push(pkt);
300 Ok(())
301 }
302
303 pub fn forward(&mut self, in_idx: usize, out_idx: usize) -> Result<()> {
305 let pkt = self
306 .inputs
307 .get(in_idx)
308 .and_then(|s| s.clone())
309 .ok_or_else(|| {
310 Error::InvalidArg(format!(
311 "node `{}`: forward input port {in_idx} is empty or out of range",
312 self.node_name
313 ))
314 })?;
315 self.emit(out_idx, pkt)
316 }
317
318 pub fn set_next_bound(&mut self, out_idx: usize, bound: Timestamp) {
319 if let Some(slot) = self.next_bounds.get_mut(out_idx) {
320 *slot = Some(bound);
321 }
322 }
323
324 pub fn log(&self, level: i32, msg: &str) {
325 if !crate::runtime::log_enabled() {
328 return;
329 }
330 crate::runtime::log(level, &format!("[{}] {}", self.node_name, msg));
331 }
332
333 pub fn set_error(&mut self, msg: &str) {
334 self.error_msg = Some(msg.to_string());
335 }
336
337 pub(crate) fn callback_status(&mut self, status: i32) -> i32 {
339 let failed = std::mem::take(&mut self.output_failed);
340 if status == 0 && failed {
341 crate::status::code::KERNEL
342 } else {
343 status
344 }
345 }
346
347 pub fn take_error(&mut self, code: i32) -> Error {
349 let detail = self
350 .error_msg
351 .take()
352 .unwrap_or_else(|| format!("returned status code {code}"));
353 Error::Kernel(format!("[{}] {}", self.node_name, detail))
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::config::GraphConfig;
361
362 fn ports(decls: &[&str]) -> Arc<PortTable> {
363 let v: Vec<String> = decls.iter().map(|s| s.to_string()).collect();
364 Arc::new(PortTable::build(&v, "test").unwrap())
365 }
366
367 fn ctx(opts: &str) -> Context {
368 let value: serde_yaml::Value = serde_yaml::from_str(opts).unwrap();
369 Context::new(
370 "n".into(),
371 "K".into(),
372 ports(&["a"]),
373 ports(&["b", "c"]),
374 Arc::new(Options::new(value)),
375 Arc::new(BTreeMap::new()),
376 Arc::new(GraphShared::new(
377 GraphConfig::from_yaml("nodes: []").unwrap(),
378 )),
379 )
380 }
381
382 #[test]
383 fn options_scalars_and_dotted_path() {
384 let c = ctx("threshold: 0.5\nn: 7\nflag: true\nname: hi\nroi:\n x: 8\n y: 9\n");
385 assert_eq!(c.options.f64("threshold"), Some(0.5));
386 assert_eq!(c.options.i64("n"), Some(7));
387 assert_eq!(c.options.bool("flag"), Some(true));
388 assert_eq!(c.options.str("name"), Some("hi"));
389 assert_eq!(c.options.i64("roi.x"), Some(8), "dotted path reads nested");
390 assert_eq!(c.options.i64("roi.y"), Some(9));
391 assert_eq!(c.options.i64("nope"), None);
392 assert_eq!(c.options.i64("roi.nope"), None);
393 }
394
395 #[test]
396 fn integer_reads_as_float() {
397 let c = ctx("scale: 2");
399 assert_eq!(c.options.f64("scale"), Some(2.0));
400 }
401
402 #[test]
403 fn options_arrays() {
404 let c = ctx("mean: [0.1, 0.2, 0.3]\nsizes: [8, 16]\nnames: [a, b]");
405 assert_eq!(c.options.count("mean"), 3);
406 let mut f = [0.0f64; 4];
407 assert_eq!(c.options.f64_array("mean", &mut f), 3);
408 assert_eq!(&f[..3], &[0.1, 0.2, 0.3]);
409
410 let mut i = [0i64; 2];
411 assert_eq!(c.options.i64_array("sizes", &mut i), 2);
412 assert_eq!(i, [8, 16]);
413
414 let mut small = [0.0f64; 2];
416 assert_eq!(c.options.f64_array("mean", &mut small), 3);
417 assert_eq!(&small, &[0.1, 0.2]);
418
419 assert_eq!(c.options.count("missing"), 0);
420 assert_eq!(c.options.f64_array("missing", &mut f), 0);
421 }
422
423 #[test]
424 fn options_json_fallback() {
425 let c = ctx("a: 1\nnested:\n deep:\n x: [1, 2]\n");
426 let j = c.options.json();
427 assert!(j.contains("\"a\":1"), "{j}");
428 assert!(j.contains("deep"), "{j}");
429 }
430
431 #[test]
432 fn empty_options_json_is_object() {
433 let c = ctx("~"); assert_eq!(
435 c.options.json(),
436 "{}",
437 "empty options should be {{}} not null"
438 );
439 }
440
441 #[test]
442 fn emit_inherits_input_timestamp() {
443 let mut c = ctx("{}");
444 c.input_ts = Timestamp(42);
445 c.emit(0, Packet::new(1i32)).unwrap();
446 assert_eq!(c.staging[0][0].timestamp(), Timestamp(42));
447 c.emit(1, Packet::new(2i32).at(Timestamp(7))).unwrap();
449 assert_eq!(c.staging[1][0].timestamp(), Timestamp(7));
450 }
451
452 #[test]
453 fn emit_out_of_range_is_error() {
454 let mut c = ctx("{}");
455 let err = c.emit(9, Packet::new(1i32)).unwrap_err();
456 assert!(err.to_string().contains("out of range"), "{err}");
457 }
458
459 #[test]
460 fn forward_clones_reference_only() {
461 let mut c = ctx("{}");
462 let p = Packet::new(5i32).at(Timestamp(0));
463 c.inputs[0] = Some(p);
464 c.forward(0, 0).unwrap();
465 assert_eq!(c.inputs[0].as_ref().unwrap().ref_count(), 2);
467 assert_eq!(c.staging[0][0].get::<i32>(), Some(&5));
468 }
469
470 #[test]
471 fn forward_from_empty_slot_is_error() {
472 let mut c = ctx("{}");
473 assert!(
474 c.forward(0, 0).is_err(),
475 "forward from empty slot must error"
476 );
477 }
478
479 #[test]
480 fn take_input_empties_slot_enabling_cow() {
481 let mut c = ctx("{}");
482 c.inputs[0] = Some(Packet::new(5i32));
483 let p = c.take_input(0);
484 assert!(c.inputs[0].is_none(), "slot must be empty after take");
485 assert_eq!(
486 p.ref_count(),
487 1,
488 "must be exclusive after take so CoW is zero-copy"
489 );
490 assert!(c.take_input(0).is_empty());
492 }
493
494 #[test]
495 fn discard_staging_on_failure() {
496 let mut c = ctx("{}");
497 c.input_ts = Timestamp(0);
498 c.emit(0, Packet::new(1i32)).unwrap();
499 c.set_next_bound(1, Timestamp(3));
500 c.discard_staging();
501 assert!(
502 c.staging[0].is_empty(),
503 "must not propagate half-finished output on failure"
504 );
505 assert!(c.next_bounds[1].is_none());
506 }
507
508 #[test]
509 fn error_carries_node_name() {
510 let mut c = ctx("{}");
511 c.set_error("model load failed");
512 let e = c.take_error(3);
513 assert!(
514 e.to_string().contains("[n]"),
515 "error should carry the node name: {e}"
516 );
517 assert!(e.to_string().contains("model load failed"), "{e}");
518 }
519
520 #[test]
521 fn error_without_message_still_useful() {
522 let mut c = ctx("{}");
523 let e = c.take_error(3);
524 assert!(e.to_string().contains("status code 3"), "{e}");
525 }
526}