Skip to main content

hidane_rules/
coverage.rs

1//! The coverage report of `GET /emulator/v1/projects/{p}:ruleCoverage`: what each expression
2//! of a ruleset evaluated to, as the official emulator reports it
3//! (`tools/oracle/rules_coverage.py`, `crates/hidane/tests/fixtures/rules_coverage.json`).
4//!
5//! - The report holds every expression of the ruleset's first file that is not a literal (null,
6//!   a bool, a number, a string or bytes), each with the ones in it. Its roots are the
7//!   functions' bodies and the `allow` conditions in the order the ruleset nests them: the
8//!   file's functions, then each service's functions, its `match` statements (each with its
9//!   functions, the `match` statements in it, then its `allow` statements) and its `allow`
10//!   statements. A body or a condition that is a literal is left out.
11//! - A function body with `let`s starts at its first `let`: each `let` holds its value and the
12//!   body from the next `let` on (or the result), and has the function's result.
13//! - The expressions in one come in source order, but `x in y` holds `y` first.
14//! - An expression starts at its first token, or at the outermost parenthesis before it (but
15//!   the object of a member, an index or a method leaves its outermost parenthesis to them), and
16//!   ends with its last part: a call with its last argument (or its name), a list or a map with
17//!   its last item (or its opening bracket), a path with its last segment, a member with its
18//!   name; closing brackets are left out. Offsets count code points, and the end is inclusive.
19//! - Each expression lists the distinct things it evaluated to, in the order first seen, with
20//!   how many times: a value, an error, or nothing (`{}`) when `&&`, `||` or `?:` decided
21//!   without it. An error is placed at a token of the expression that raised it (an operator,
22//!   a function's name, or the expression's first token) and keeps that place as it goes up;
23//!   a comparison a list's filter does not decide is an error without either.
24//! - The statements of a request's method count as a growing `||` of them: the first evaluated
25//!   once for each statement evaluated after it, and once more (`Trace`).
26//!
27//! The official emulator evaluates a condition again after an error, or when it reads a
28//! document, and its report counts both passes; hidane's counts each evaluation once. It also
29//! keeps at most [`MAX_VALUES`] distinct values per expression (`docs/parity-exceptions.md`).
30
31use std::{
32    collections::HashMap,
33    hash::{DefaultHasher, Hash, Hasher},
34    rc::Rc,
35};
36
37use crate::{
38    ast::{BinaryOp, Body, Callee, Expr, ExprKind, Function, PathPart, Ruleset, Span},
39    diag::Position,
40    eval::{NodeKey, Outcome, Trace, Traced, Val, hash_val, observe, shows},
41    lexer,
42};
43
44/// How many distinct values an expression keeps; later ones that differ from all of them are
45/// not counted.
46pub const MAX_VALUES: usize = 100;
47
48/// A ruleset's report: its expressions and what each evaluated to so far.
49pub struct Coverage {
50    nodes: Vec<Node>,
51    roots: Vec<usize>,
52    index: HashMap<NodeKey, usize>,
53    /// The code points before each byte of the file.
54    code_points: Vec<u32>,
55    /// Each token's end, line and column, by its start (bytes), to place errors.
56    tokens: HashMap<usize, (usize, u32, u32)>,
57}
58
59struct Node {
60    position: Position,
61    children: Vec<usize>,
62    values: Vec<(Outcome, u64)>,
63    /// Indexes into `values`, by the hash of the outcome.
64    seen: HashMap<u64, Vec<usize>>,
65}
66
67/// An expression of the report.
68pub struct Expression<'a> {
69    pub position: Position,
70    /// Each distinct outcome in the order first seen, and how many times it was seen.
71    pub values: &'a [(Outcome, u64)],
72    /// The expressions in it, in order ([`Coverage::expression`]).
73    pub children: &'a [usize],
74}
75
76/// What the report still has to add: an expression (and whether it is the object of a member,
77/// an index or a method, whose report leaves out its outermost parenthesis), or a function's
78/// body from its `i`th `let` on.
79enum Pending<'r> {
80    Expr(&'r Expr, bool),
81    Lets(&'r Function, usize),
82}
83
84impl Coverage {
85    /// The report of `ruleset`, compiled from `source` (its first file), with nothing counted.
86    pub fn new(ruleset: &Ruleset, source: &str) -> Self {
87        let mut code_points = Vec::with_capacity(source.len() + 1);
88        let mut count = 0u32;
89        for c in source.chars() {
90            code_points.extend(std::iter::repeat_n(count, c.len_utf8()));
91            count += 1;
92        }
93        code_points.push(count);
94        let tokens = lexer::lex(source)
95            .tokens
96            .into_iter()
97            .map(|t| (t.start, (t.end, t.line, t.column)))
98            .collect();
99        let mut coverage = Self {
100            nodes: Vec::new(),
101            roots: Vec::new(),
102            index: HashMap::new(),
103            code_points,
104            tokens,
105        };
106        let mut roots = Vec::new();
107        for f in &ruleset.functions {
108            roots.extend(coverage.function(ruleset, f));
109        }
110        for service in &ruleset.services {
111            coverage.body(ruleset, &service.body, &mut roots);
112        }
113        coverage.roots = roots;
114        coverage
115    }
116
117    fn body(&mut self, ruleset: &Ruleset, body: &Body, roots: &mut Vec<usize>) {
118        for f in &body.functions {
119            roots.extend(self.function(ruleset, f));
120        }
121        for m in &body.matches {
122            self.body(ruleset, &m.body, roots);
123        }
124        for allow in &body.allows {
125            if let Some(condition) = allow.condition.as_ref().filter(|c| is_node(c)) {
126                roots.push(self.tree(ruleset, Pending::Expr(condition, false)));
127            }
128        }
129    }
130
131    fn function(&mut self, ruleset: &Ruleset, f: &Function) -> Option<usize> {
132        if f.lets.is_empty() {
133            return is_node(&f.result).then(|| self.tree(ruleset, Pending::Expr(&f.result, false)));
134        }
135        Some(self.tree(ruleset, Pending::Lets(f, 0)))
136    }
137
138    /// Adds `root` and everything in it (without recursion: expressions nest thousands deep).
139    fn tree(&mut self, ruleset: &Ruleset, root: Pending<'_>) -> usize {
140        let mut stack: Vec<(Pending<'_>, Option<usize>)> = vec![(root, None)];
141        let mut added = None;
142        while let Some((pending, parent)) = stack.pop() {
143            let mut children = Vec::new();
144            let (key, position) = match pending {
145                Pending::Expr(e, object) => {
146                    children.extend(
147                        parts(e)
148                            .into_iter()
149                            .filter(|(part, _)| is_node(part))
150                            .map(|(part, object)| Pending::Expr(part, object)),
151                    );
152                    let start = if object {
153                        start_inside(&ruleset.parens, e)
154                    } else {
155                        start(&ruleset.parens, e)
156                    };
157                    (NodeKey::of(e), self.position(start, end(e)))
158                }
159                Pending::Lets(f, i) => {
160                    let binding = &f.lets[i];
161                    if is_node(&binding.value) {
162                        children.push(Pending::Expr(&binding.value, false));
163                    }
164                    if i + 1 < f.lets.len() {
165                        children.push(Pending::Lets(f, i + 1));
166                    } else if is_node(&f.result) {
167                        children.push(Pending::Expr(&f.result, false));
168                    }
169                    let position = self.position(binding.span, end(&f.result));
170                    (NodeKey::Body(binding.span.start), position)
171                }
172            };
173            self.nodes.push(Node {
174                position,
175                children: Vec::new(),
176                values: Vec::new(),
177                seen: HashMap::new(),
178            });
179            let index = self.nodes.len() - 1;
180            self.index.insert(key, index);
181            match parent {
182                Some(parent) => self.nodes[parent].children.push(index),
183                None => added = Some(index),
184            }
185            stack.extend(children.into_iter().rev().map(|c| (c, Some(index))));
186        }
187        added.expect("the root was added")
188    }
189
190    /// From the token of `start` to byte `end` (exclusive).
191    fn position(&self, start: Span, end: usize) -> Position {
192        let from = self.code_points[start.start];
193        Position {
194            line: start.line,
195            column: start.column,
196            current_offset: from,
197            end_offset: self.code_points[end].max(from + 1) - 1,
198        }
199    }
200
201    /// The token that starts at byte `at`.
202    fn token(&self, at: usize) -> Option<Position> {
203        let &(end, line, column) = self.tokens.get(&at)?;
204        Some(self.position(
205            Span {
206                start: at,
207                end,
208                line,
209                column,
210            },
211            end,
212        ))
213    }
214
215    /// Counts what `trace` saw of this ruleset's expressions. A value is only built for the
216    /// report when it is new.
217    pub fn record(&mut self, trace: &Trace) {
218        let Some(values) = &trace.values else {
219            return;
220        };
221        // The hashes of the maps and lists seen, by address: the same document or `request`
222        // comes up again and again.
223        let mut hashes: HashMap<usize, u64> = HashMap::new();
224        for (key, traced) in values {
225            let Some(&i) = self.index.get(key) else {
226                continue;
227            };
228            let mut hasher = DefaultHasher::new();
229            let address = match traced {
230                Traced::Value(Val::Map(map)) => Some(Rc::as_ptr(map).addr()),
231                Traced::Value(Val::List(items)) => Some(Rc::as_ptr(items).addr()),
232                _ => None,
233            };
234            let known = address.and_then(|a| hashes.get(&a).copied());
235            let position = match traced {
236                Traced::Value(v) => {
237                    if known.is_none() {
238                        0u8.hash(&mut hasher);
239                        hash_val(v, &mut hasher);
240                    }
241                    None
242                }
243                Traced::Error(e) => {
244                    let position = e.at.and_then(|at| self.token(at));
245                    (1u8, position, &e.message).hash(&mut hasher);
246                    position
247                }
248                Traced::Skipped => {
249                    2u8.hash(&mut hasher);
250                    None
251                }
252            };
253            let hash = known.unwrap_or_else(|| hasher.finish());
254            if let Some(address) = address {
255                hashes.insert(address, hash);
256            }
257            let node = &mut self.nodes[i];
258            let seen = node.seen.get(&hash).into_iter().flatten();
259            let same = |outcome: &Outcome| match (outcome, traced) {
260                (Outcome::Value(o), Traced::Value(v)) => shows(o, v),
261                (
262                    Outcome::Error {
263                        position: p,
264                        message,
265                    },
266                    Traced::Error(e),
267                ) => *p == position && *message == e.message,
268                (Outcome::Skipped, Traced::Skipped) => true,
269                _ => false,
270            };
271            if let Some(&found) = seen.into_iter().find(|&&j| same(&node.values[j].0)) {
272                node.values[found].1 += 1;
273            } else if node.values.len() < MAX_VALUES {
274                let outcome = match traced {
275                    Traced::Value(v) => Outcome::Value(observe(v)),
276                    Traced::Error(e) => Outcome::Error {
277                        position,
278                        message: e.message.clone(),
279                    },
280                    Traced::Skipped => Outcome::Skipped,
281                };
282                node.seen.entry(hash).or_default().push(node.values.len());
283                node.values.push((outcome, 1));
284            }
285        }
286    }
287
288    /// The report's expressions at the top, in order ([`Coverage::expression`]).
289    pub fn roots(&self) -> &[usize] {
290        &self.roots
291    }
292
293    /// The expression `i` of the report.
294    pub fn expression(&self, i: usize) -> Expression<'_> {
295        let node = &self.nodes[i];
296        Expression {
297            position: node.position,
298            values: &node.values,
299            children: &node.children,
300        }
301    }
302}
303
304/// Whether `e` is in the report: every expression but a literal.
305pub(crate) fn is_node(e: &Expr) -> bool {
306    !matches!(
307        e.kind,
308        ExprKind::Null
309            | ExprKind::Bool(_)
310            | ExprKind::Int(_)
311            | ExprKind::IntOverflow(_)
312            | ExprKind::Float(_)
313            | ExprKind::String(_)
314            | ExprKind::Bytes(_)
315    )
316}
317
318/// The expressions directly in `e`, in the official emulator's order, each with whether it is
319/// the object of a member, an index or a method.
320fn parts(e: &Expr) -> Vec<(&Expr, bool)> {
321    match &e.kind {
322        ExprKind::List(items) => items.iter().map(|e| (e, false)).collect(),
323        ExprKind::Map(entries) => entries
324            .iter()
325            .flat_map(|(k, v)| [(k, false), (v, false)])
326            .collect(),
327        ExprKind::Path(parts) => parts
328            .iter()
329            .filter_map(|part| match part {
330                PathPart::Expr(e) => Some((e, false)),
331                PathPart::Segment(_) => None,
332            })
333            .collect(),
334        ExprKind::Member { object, .. } => vec![(object, true)],
335        ExprKind::Index { object, index } => vec![(object, true), (index, false)],
336        ExprKind::Slice { object, start, end } => {
337            vec![(object, true), (start, false), (end, false)]
338        }
339        ExprKind::Call { callee, args } => {
340            let mut parts = Vec::with_capacity(args.len() + 1);
341            if let Callee::Method { receiver, .. } = callee {
342                parts.push((&**receiver, true));
343            }
344            parts.extend(args.iter().map(|a| (a, false)));
345            parts
346        }
347        ExprKind::Unary { operand, .. } => vec![(operand, false)],
348        // `x in y` is `y`'s method there.
349        ExprKind::Binary {
350            op: BinaryOp::In,
351            left,
352            right,
353            ..
354        } => vec![(right, false), (left, false)],
355        ExprKind::Binary { left, right, .. } => vec![(left, false), (right, false)],
356        ExprKind::Is { value, .. } => vec![(value, false)],
357        ExprKind::Ternary {
358            condition,
359            then,
360            otherwise,
361            ..
362        } => vec![(condition, false), (then, false), (otherwise, false)],
363        ExprKind::Null
364        | ExprKind::Bool(_)
365        | ExprKind::Int(_)
366        | ExprKind::IntOverflow(_)
367        | ExprKind::Float(_)
368        | ExprKind::String(_)
369        | ExprKind::Bytes(_)
370        | ExprKind::Ident(_) => Vec::new(),
371    }
372}
373
374/// Calls `f` on `e` and on every expression in it that is in the report.
375pub(crate) fn walk<'e>(e: &'e Expr, f: &mut dyn FnMut(&'e Expr)) {
376    let mut stack = vec![e];
377    while let Some(e) = stack.pop() {
378        if is_node(e) {
379            f(e);
380            stack.extend(parts(e).into_iter().map(|(part, _)| part));
381        }
382    }
383}
384
385/// The first token of `e` in the report: the outermost parenthesis around it or around the
386/// expression it starts with, or its own first token.
387pub(crate) fn start(parens: &HashMap<(usize, usize), Vec<Span>>, e: &Expr) -> Span {
388    match parens.get(&(e.span.start, e.span.end)) {
389        Some(around) if !around.is_empty() => around[0],
390        _ => leftmost(parens, e),
391    }
392}
393
394/// The first token of an expression that is the object of a member, an index or a method: its
395/// outermost parenthesis is the outer expression's.
396fn start_inside(parens: &HashMap<(usize, usize), Vec<Span>>, e: &Expr) -> Span {
397    match parens.get(&(e.span.start, e.span.end)) {
398        Some(around) if around.len() > 1 => around[1],
399        _ => leftmost(parens, e),
400    }
401}
402
403/// The first token of `e` but for parentheses around it: that of the expression it starts with.
404fn leftmost(parens: &HashMap<(usize, usize), Vec<Span>>, mut e: &Expr) -> Span {
405    loop {
406        e = match &e.kind {
407            ExprKind::Binary { left, .. } => left,
408            ExprKind::Member { object, .. }
409            | ExprKind::Index { object, .. }
410            | ExprKind::Slice { object, .. } => object,
411            ExprKind::Call {
412                callee: Callee::Method { receiver, .. },
413                ..
414            } => receiver,
415            ExprKind::Is { value, .. } => value,
416            ExprKind::Ternary { condition, .. } => condition,
417            _ => return e.span,
418        };
419        if let Some(around) = parens.get(&(e.span.start, e.span.end))
420            && let Some(outermost) = around.first()
421        {
422            return *outermost;
423        }
424    }
425}
426
427/// Where `e` ends in the report (bytes, exclusive): with its last part, closing brackets left
428/// out.
429fn end(mut e: &Expr) -> usize {
430    loop {
431        e = match &e.kind {
432            ExprKind::List(items) => match items.last() {
433                Some(last) => last,
434                None => return e.span.start + 1,
435            },
436            ExprKind::Map(entries) => match entries.last() {
437                Some((_, value)) => value,
438                None => return e.span.start + 1,
439            },
440            ExprKind::Path(parts) => match parts.last() {
441                Some(PathPart::Expr(last)) => last,
442                _ => return e.span.end,
443            },
444            ExprKind::Member { field, .. } => return field.span.end,
445            ExprKind::Index { index, .. } => index,
446            ExprKind::Slice { end, .. } => end,
447            ExprKind::Call { callee, args } => match args.last() {
448                Some(last) => last,
449                None => {
450                    return match callee {
451                        Callee::Global(name)
452                        | Callee::Namespaced { name, .. }
453                        | Callee::Method { name, .. } => name.span.end,
454                    };
455                }
456            },
457            ExprKind::Unary { operand, .. } => operand,
458            ExprKind::Binary { right, .. } => right,
459            ExprKind::Is { type_name, .. } => return type_name.span.end,
460            ExprKind::Ternary { otherwise, .. } => otherwise,
461            _ => return e.span.end,
462        };
463    }
464}