Skip to main content

petr_dispatch/
table.rs

1use petr_core::SelectorWord;
2
3use crate::{DispatchError, DispatchResult};
4
5/// Small value frame passed through operations.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct OpFrame {
8    /// Text payload for octal/octet/string examples.
9    pub text: String,
10    /// Integer accumulator for numeric examples.
11    pub acc: i64,
12}
13
14impl OpFrame {
15    /// Create a new frame from text.
16    pub fn text(text: impl Into<String>) -> Self { Self { text: text.into(), acc: 0 } }
17
18    /// Create a new frame from an accumulator value.
19    pub fn acc(acc: i64) -> Self { Self { text: String::new(), acc } }
20}
21
22/// Dynamic operation argument.
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
24pub struct OpArg {
25    /// Optional numeric parameter.
26    pub n: i64,
27    /// Optional string parameter.
28    pub text: String,
29}
30
31impl OpArg {
32    /// Build a numeric argument.
33    pub fn n(n: i64) -> Self { Self { n, text: String::new() } }
34
35    /// Build a text argument.
36    pub fn text(text: impl Into<String>) -> Self { Self { n: 0, text: text.into() } }
37}
38
39/// Function pointer used by the dispatch table.
40pub type OpFn = fn(OpFrame, &OpArg) -> DispatchResult<OpFrame>;
41
42/// One dispatch table entry.
43#[derive(Clone)]
44pub struct OpEntry {
45    /// Function to execute.
46    pub func: OpFn,
47    /// Bound argument.
48    pub arg: OpArg,
49    /// Human-readable operation name.
50    pub name: String,
51}
52
53impl core::fmt::Debug for OpEntry {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        f.debug_struct("OpEntry").field("name", &self.name).field("arg", &self.arg).finish()
56    }
57}
58
59/// 256-entry selector dispatch table.
60#[derive(Debug, Clone)]
61pub struct OperationTable {
62    entries: Vec<Option<OpEntry>>,
63}
64
65impl Default for OperationTable {
66    fn default() -> Self { Self::new() }
67}
68
69impl OperationTable {
70    /// Create an empty 256-entry table.
71    pub fn new() -> Self { Self { entries: vec![None; 256] } }
72
73    /// Register an operation at a raw byte selector.
74    pub fn register(&mut self, selector: u8, name: impl Into<String>, func: OpFn, arg: OpArg) {
75        self.entries[selector as usize] = Some(OpEntry { func, arg, name: name.into() });
76    }
77
78    /// Register using a 4/4/8 selector word.
79    pub fn register_word(&mut self, selector: SelectorWord, name: impl Into<String>, func: OpFn, arg: OpArg) {
80        self.register(selector.function_id(), name, func, arg);
81    }
82
83    /// Execute by raw selector.
84    pub fn execute(&self, selector: u8, frame: OpFrame) -> DispatchResult<OpFrame> {
85        match &self.entries[selector as usize] {
86            Some(entry) => (entry.func)(frame, &entry.arg),
87            None => Err(DispatchError::OperationNotFound(selector)),
88        }
89    }
90
91    /// Execute by selector word. Runtime uses the low 8 function bits as the table index.
92    pub fn execute_word(&self, selector: SelectorWord, frame: OpFrame) -> DispatchResult<OpFrame> {
93        self.execute(selector.function_id(), frame)
94    }
95
96    /// Execute a selector without a branch in user code by falling back to NOP for missing entries.
97    ///
98    /// This still uses an internal Option branch; use it as an ergonomic hot-loop shape, not a
99    /// hard side-channel guarantee.
100    pub fn execute_or_nop(&self, selector: u8, frame: OpFrame) -> OpFrame {
101        if let Some(entry) = &self.entries[selector as usize] {
102            (entry.func)(frame, &entry.arg).unwrap_or_else(|_| OpFrame::text("<op-error>"))
103        } else {
104            frame
105        }
106    }
107
108    /// Return true when an entry is registered.
109    pub fn contains(&self, selector: u8) -> bool { self.entries[selector as usize].is_some() }
110}