Skip to main content

stix_pattern/ir/
span.rs

1//! Source extents for instructions that carry no span of their own.
2
3use std::collections::HashSet;
4
5use crate::error::Span;
6use crate::ir::index::ProgramIndex;
7use crate::ir::{BlockId, InstrId, Op, Program};
8
9impl Program {
10    /// The source extent of an instruction: its own span when it has one,
11    /// otherwise the union of its operands' extents.
12    ///
13    /// The boolean combinators carry no span of their own — their extent is
14    /// exactly the span of the text their operands cover — so this computes it on
15    /// demand rather than storing redundant state that structural edits would
16    /// have to keep consistent.
17    ///
18    /// Returns `None` when neither the instruction nor anything it references has
19    /// a span, and when `block` or `instr` names nothing in this program.
20    ///
21    /// `Observe` normally answers from its own span, which is the extent of the
22    /// whole `[...]`; only when that is missing does this descend into the
23    /// comparison block it names.
24    ///
25    /// This is safe to call on a program that has not been validated. Descent is
26    /// iterative, so depth costs heap rather than stack, and it visits each
27    /// instruction at most once, so it terminates on cyclic or shared input and
28    /// does work linear in the size of the program.
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use stix_pattern::{ir, parse};
34    ///
35    /// let src = "[file:size > 1] AND [file:name = 'a']";
36    /// let program = ir::lower(&parse(src).unwrap());
37    /// // The `and` in `main` has no span of its own.
38    /// let and = program.main.instructions[2].id;
39    /// let span = program.span_of(program.main.id, and).unwrap();
40    /// assert_eq!(&src[span.start..span.end], src);
41    /// ```
42    pub fn span_of(&self, block: BlockId, instr: InstrId) -> Option<Span> {
43        let index = ProgramIndex::new(self);
44        let mut extent: Option<Span> = None;
45        // A validated program is acyclic, but this is meant to be safe on one
46        // that is not, so never visit the same instruction twice.
47        let mut visited: HashSet<(BlockId, InstrId)> = HashSet::new();
48        let mut stack: Vec<(BlockId, InstrId)> = vec![(block, instr)];
49
50        while let Some((block_id, instr_id)) = stack.pop() {
51            if !visited.insert((block_id, instr_id)) {
52                continue;
53            }
54            let Some(b) = index.block_including_main(block_id) else {
55                continue;
56            };
57            let Some(i) = b.instruction(instr_id) else {
58                continue;
59            };
60            if let Some(span) = i.span {
61                extent = Some(union(extent, span));
62                continue;
63            }
64            match &i.op {
65                Op::Load { .. } => {}
66                Op::Compare { lhs, .. } => stack.push((block_id, *lhs)),
67                Op::And { lhs, rhs } | Op::Or { lhs, rhs } | Op::FollowedBy { lhs, rhs } => {
68                    stack.push((block_id, *lhs));
69                    stack.push((block_id, *rhs));
70                }
71                Op::Within { input, .. }
72                | Op::Repeats { input, .. }
73                | Op::StartStop { input, .. } => stack.push((block_id, *input)),
74                Op::Yield { value } | Op::Ret { value } => stack.push((block_id, *value)),
75                Op::Observe { block: target } => {
76                    if let Some(cb) = index.block(*target) {
77                        if let Some(Op::Yield { value }) = cb.block().terminator().map(|t| &t.op) {
78                            stack.push((*target, *value));
79                        }
80                    }
81                }
82            }
83        }
84        extent
85    }
86}
87
88/// The smallest span covering both inputs.
89fn union(acc: Option<Span>, span: Span) -> Span {
90    match acc {
91        None => span,
92        Some(a) => Span {
93            start: a.start.min(span.start),
94            end: a.end.max(span.end),
95        },
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use crate::ir::{lower, BlockId, InstrId, Op};
102    use crate::parse;
103
104    /// The source text an instruction's extent covers.
105    fn text_of(src: &str, block: BlockId, instr: InstrId) -> String {
106        let program = lower(&parse(src).unwrap());
107        let span = program
108            .span_of(block, instr)
109            .expect("instruction should have an extent");
110        src[span.start..span.end].to_string()
111    }
112
113    #[test]
114    fn a_combinator_extent_covers_both_operands() {
115        let src = "[file:size > 1] AND [file:name = 'a']";
116        let program = lower(&parse(src).unwrap());
117        let and = program
118            .main
119            .instructions
120            .iter()
121            .find(|i| matches!(i.op, Op::And { .. }))
122            .expect("an and");
123        assert!(and.span.is_none(), "and should carry no span of its own");
124        assert_eq!(text_of(src, program.main.id, and.id), src);
125    }
126
127    #[test]
128    fn a_comparison_tier_combinator_extent_covers_both_operands() {
129        let src = "[file:size > 1 AND file:name = 'a']";
130        let program = lower(&parse(src).unwrap());
131        let block = &program.blocks[0];
132        let and = block
133            .instructions
134            .iter()
135            .find(|i| matches!(i.op, Op::And { .. }))
136            .expect("an and");
137        assert_eq!(
138            text_of(src, block.id, and.id),
139            "file:size > 1 AND file:name = 'a'"
140        );
141    }
142
143    #[test]
144    fn an_instruction_with_its_own_span_returns_it_unchanged() {
145        let src = "[file:size > 1] AND [file:name = 'a']";
146        let program = lower(&parse(src).unwrap());
147        let observe = &program.main.instructions[0];
148        assert_eq!(program.span_of(program.main.id, observe.id), observe.span);
149        assert_eq!(text_of(src, program.main.id, observe.id), "[file:size > 1]");
150    }
151
152    #[test]
153    fn a_terminator_extent_covers_the_whole_program() {
154        let src = "[file:size > 1] OR [file:name = 'a']";
155        let program = lower(&parse(src).unwrap());
156        let ret = program.main.terminator().expect("a ret");
157        assert!(ret.span.is_none());
158        assert_eq!(text_of(src, program.main.id, ret.id), src);
159    }
160
161    #[test]
162    fn nested_combinators_union_transitively() {
163        let src = "[file:size > 1] OR [file:name = 'a'] OR [file:name = 'b']";
164        let program = lower(&parse(src).unwrap());
165        // The outer OR is the last combinator in `main`.
166        let outer = program
167            .main
168            .instructions
169            .iter()
170            .rfind(|i| matches!(i.op, Op::Or { .. }))
171            .expect("two ors");
172        assert_eq!(text_of(src, program.main.id, outer.id), src);
173    }
174
175    #[test]
176    fn an_unknown_block_or_instruction_has_no_extent() {
177        let program = lower(&parse("[file:size > 1]").unwrap());
178        assert_eq!(program.span_of(BlockId(999), InstrId(0)), None);
179        assert_eq!(program.span_of(program.main.id, InstrId(999)), None);
180    }
181
182    /// `[file:size > 1]` with `levels` `WITHIN` qualifiers chained onto it, every
183    /// span stripped except, optionally, the innermost compare's.
184    fn deep_chain(levels: u32, keep_compare_span: bool) -> (crate::ir::Program, InstrId) {
185        let mut program = lower(&parse("[file:size > 1]").unwrap());
186        for instr in &mut program.blocks[0].instructions {
187            if !(keep_compare_span && matches!(instr.op, Op::Compare { .. })) {
188                instr.span = None;
189            }
190        }
191        for instr in &mut program.main.instructions {
192            instr.span = None;
193        }
194        let mut acc = program.main.instructions[0].id;
195        for next in 100..100 + levels {
196            let id = InstrId(next);
197            program.main.instructions.push(crate::ir::Instruction {
198                id,
199                op: Op::Within {
200                    input: acc,
201                    seconds: 1.0,
202                },
203                span: None,
204            });
205            acc = id;
206        }
207        (program, acc)
208    }
209
210    /// Run `f` on a thread with a deliberately small stack.
211    fn on_small_stack(f: impl FnOnce() + Send + 'static) {
212        std::thread::Builder::new()
213            .stack_size(256 * 1024)
214            .spawn(f)
215            .expect("spawn test thread")
216            .join()
217            .expect("test thread panicked");
218    }
219
220    #[test]
221    fn a_deep_chain_terminates_rather_than_overflowing() {
222        // No spans anywhere: descent walks the whole chain iteratively and finds
223        // nothing.
224        on_small_stack(|| {
225            let (program, top) = deep_chain(10_000, false);
226            assert_eq!(program.span_of(program.main.id, top), None);
227        });
228    }
229
230    #[test]
231    fn a_deep_chain_reaches_a_span_at_the_bottom() {
232        // The only span is 10,000 levels down; with no depth bound it is found.
233        on_small_stack(|| {
234            let (program, top) = deep_chain(10_000, true);
235            let compare = program.blocks[0]
236                .instructions
237                .iter()
238                .find(|i| matches!(i.op, Op::Compare { .. }))
239                .expect("a compare");
240            assert!(compare.span.is_some());
241            assert_eq!(program.span_of(program.main.id, top), compare.span);
242        });
243    }
244
245    #[test]
246    fn a_cyclic_program_terminates() {
247        // `span_of` may run on an unvalidated program; an `and` naming itself
248        // must not loop forever.
249        let mut program = lower(&parse("[file:size > 1] AND [file:size > 2]").unwrap());
250        let and = program
251            .main
252            .instructions
253            .iter_mut()
254            .find(|i| matches!(i.op, Op::And { .. }))
255            .expect("an and");
256        let id = and.id;
257        and.op = Op::And { lhs: id, rhs: id };
258        assert_eq!(program.span_of(program.main.id, id), None);
259    }
260}