1use std::collections::HashSet;
4
5use crate::error::Span;
6use crate::ir::index::ProgramIndex;
7use crate::ir::{BlockId, InstrId, Op, Program};
8
9impl Program {
10 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 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
88fn 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 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 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 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 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 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 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 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}