Skip to main content

stix_pattern/ir/
instr.rs

1//! The IR instruction set and its container types.
2
3use serde::{Deserialize, Serialize};
4
5use crate::ast::{ComparisonOperator, Literal, ObjectPath};
6use crate::error::Span;
7
8/// Version of the serialized IR schema. Checked by
9/// [`Program::validate`](crate::ir::Program::validate).
10pub const SCHEMA_VERSION: u32 = 1;
11
12/// Identifies one instruction, and therefore the value it produces.
13///
14/// The IR is in SSA form: there is no separate destination field, so a value is
15/// named by the id of the instruction that computes it.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct InstrId(pub u32);
18
19/// Identifies one block within a [`Program`](crate::ir::Program).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21pub struct BlockId(pub u32);
22
23/// Right-hand side of a [`Op::Compare`].
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub enum Operand {
26    /// A single literal value.
27    Literal(Literal),
28    /// A set of literals, for `IN`.
29    Set(Vec<Literal>),
30    /// No operand, for `EXISTS`.
31    Absent,
32}
33
34/// One three-address operation.
35///
36/// Instructions in a [`BlockKind::Comparison`] block use the comparison-tier ops
37/// (`Load`, `Compare`, `And`, `Or`, `Yield`); instructions in the
38/// [`BlockKind::Main`] block use the observation-tier ops. `And` and `Or` are
39/// shared between the tiers and take their evaluation semantics from the
40/// enclosing block's kind.
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(tag = "op", rename_all = "snake_case")]
43pub enum Op {
44    /// Resolve an object path to a value.
45    Load {
46        /// The path to resolve.
47        path: ObjectPath,
48    },
49    /// Apply a comparison operator to a loaded value and an operand.
50    Compare {
51        /// The comparison operator.
52        operator: ComparisonOperator,
53        /// `true` if a `NOT` preceded the operator in the source.
54        negated: bool,
55        /// The loaded value being compared.
56        lhs: InstrId,
57        /// The right-hand side.
58        rhs: Operand,
59    },
60    /// Boolean conjunction of two values.
61    And {
62        /// Left operand.
63        lhs: InstrId,
64        /// Right operand.
65        rhs: InstrId,
66    },
67    /// Boolean disjunction of two values.
68    Or {
69        /// Left operand.
70        lhs: InstrId,
71        /// Right operand.
72        rhs: InstrId,
73    },
74    /// Terminator of a comparison block: its result.
75    Yield {
76        /// The value the block evaluates to.
77        value: InstrId,
78    },
79    /// Evaluate a comparison block against an observation.
80    Observe {
81        /// The comparison block to evaluate.
82        block: BlockId,
83    },
84    /// `FOLLOWEDBY`: the left result's observations precede the right's.
85    FollowedBy {
86        /// Left operand.
87        lhs: InstrId,
88        /// Right operand.
89        rhs: InstrId,
90    },
91    /// `WITHIN <seconds> SECONDS` applied to a result.
92    Within {
93        /// The result being qualified.
94        input: InstrId,
95        /// The window length in seconds.
96        seconds: f64,
97    },
98    /// `REPEATS <count> TIMES` applied to a result.
99    Repeats {
100        /// The result being qualified.
101        input: InstrId,
102        /// The required repetition count.
103        count: u64,
104    },
105    /// `START <start> STOP <stop>` applied to a result.
106    StartStop {
107        /// The result being qualified.
108        input: InstrId,
109        /// Inclusive window start, RFC3339.
110        start: String,
111        /// Exclusive window stop, RFC3339.
112        stop: String,
113    },
114    /// Terminator of the main block: the program's result.
115    Ret {
116        /// The value the program evaluates to.
117        value: InstrId,
118    },
119}
120
121/// One instruction: an id, an operation, and optionally where it came from.
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct Instruction {
124    /// Unique id, which also names this instruction's result.
125    pub id: InstrId,
126    /// The operation.
127    pub op: Op,
128    /// Byte range in the source pattern, when this was lowered from text.
129    pub span: Option<Span>,
130}
131
132/// Which tier a block belongs to.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum BlockKind {
136    /// A `[...]` observation's comparisons, evaluated once per binding set.
137    Comparison,
138    /// The observation-tier block combining observation results.
139    Main,
140}
141
142/// A straight-line sequence of instructions ending in a terminator.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct Block {
145    /// Unique id.
146    pub id: BlockId,
147    /// Which tier this block belongs to.
148    pub kind: BlockKind,
149    /// The instructions, in evaluation order.
150    pub instructions: Vec<Instruction>,
151}
152
153/// A lowered pattern: comparison blocks plus the observation-tier block.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct Program {
156    /// Serialized schema version; see [`SCHEMA_VERSION`].
157    pub schema_version: u32,
158    /// The comparison blocks, in lowering order.
159    pub blocks: Vec<Block>,
160    /// The observation-tier block.
161    pub main: Block,
162}
163
164/// Counts calls to the linear-scan lookups, so tests can assert that the hot
165/// paths never make one. Thread-local, so parallel tests do not disturb each
166/// other.
167#[cfg(test)]
168pub(crate) mod scans {
169    use std::cell::Cell;
170
171    thread_local!(static COUNT: Cell<usize> = const { Cell::new(0) });
172
173    pub(crate) fn record() {
174        COUNT.with(|c| c.set(c.get() + 1));
175    }
176
177    /// Scans made on this thread since it started.
178    pub(crate) fn count() -> usize {
179        COUNT.with(Cell::get)
180    }
181}
182
183impl Program {
184    /// The comparison block with the given id, if present.
185    pub fn block(&self, id: BlockId) -> Option<&Block> {
186        #[cfg(test)]
187        scans::record();
188        self.blocks.iter().find(|b| b.id == id)
189    }
190}
191
192impl Block {
193    /// The instruction with the given id, if present in this block.
194    pub fn instruction(&self, id: InstrId) -> Option<&Instruction> {
195        #[cfg(test)]
196        scans::record();
197        self.instructions.iter().find(|i| i.id == id)
198    }
199
200    /// The block's terminator — its last instruction — if it has one.
201    pub fn terminator(&self) -> Option<&Instruction> {
202        self.instructions.last()
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::ast::{ComparisonOperator, Literal, ObjectPath, PathStep};
210    use crate::error::Span;
211
212    fn sample_program() -> Program {
213        let path = ObjectPath {
214            object_type: "file".to_string(),
215            steps: vec![PathStep::Key("size".to_string())],
216            span: Span::default(),
217        };
218        Program {
219            schema_version: SCHEMA_VERSION,
220            blocks: vec![Block {
221                id: BlockId(0),
222                kind: BlockKind::Comparison,
223                instructions: vec![
224                    Instruction {
225                        id: InstrId(0),
226                        op: Op::Load { path },
227                        span: None,
228                    },
229                    Instruction {
230                        id: InstrId(1),
231                        op: Op::Compare {
232                            operator: ComparisonOperator::GreaterThan,
233                            negated: false,
234                            lhs: InstrId(0),
235                            rhs: Operand::Literal(Literal::Integer(1024)),
236                        },
237                        span: None,
238                    },
239                    Instruction {
240                        id: InstrId(2),
241                        op: Op::Yield { value: InstrId(1) },
242                        span: None,
243                    },
244                ],
245            }],
246            main: Block {
247                id: BlockId(1),
248                kind: BlockKind::Main,
249                instructions: vec![
250                    Instruction {
251                        id: InstrId(3),
252                        op: Op::Observe { block: BlockId(0) },
253                        span: None,
254                    },
255                    Instruction {
256                        id: InstrId(4),
257                        op: Op::Ret { value: InstrId(3) },
258                        span: None,
259                    },
260                ],
261            },
262        }
263    }
264
265    #[test]
266    fn program_round_trips_through_json() {
267        let p = sample_program();
268        let json = serde_json::to_string(&p).unwrap();
269        let back: Program = serde_json::from_str(&json).unwrap();
270        assert_eq!(p, back);
271    }
272
273    #[test]
274    fn ops_serialize_internally_tagged() {
275        let p = sample_program();
276        let json = serde_json::to_string(&p).unwrap();
277        assert!(json.contains(r#""op":"load""#), "got: {json}");
278        assert!(json.contains(r#""op":"compare""#), "got: {json}");
279        assert!(json.contains(r#""op":"observe""#), "got: {json}");
280    }
281
282    #[test]
283    fn schema_version_is_one() {
284        assert_eq!(SCHEMA_VERSION, 1);
285    }
286}