Skip to main content

supercov_engine/
rust_instrumenter.rs

1//! Supercov-owned Rust parsing and obligation discovery.
2//!
3//! The first private frontend deliberately starts from a lossless concrete
4//! syntax tree. LLVM/rustc coverage remains a development oracle; it is not a
5//! product input. Probe insertion and Cargo execution build on this exact
6//! source denominator.
7
8use std::collections::BTreeSet;
9
10use ra_ap_syntax::{
11    AstNode, Edition, SourceFile, SyntaxKind, TextRange,
12    ast::{self, BinaryOp, HasAttrs, HasLoopBody, HasName, LogicOp},
13};
14use serde_json::json;
15use sha2::{Digest, Sha256};
16
17use crate::{
18    coverage_analysis::PointKind,
19    coverage_report::{
20        BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
21    },
22};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum RustInstrumenterError {
26    SourceTooLarge,
27    Parse(Vec<String>),
28    InvalidRange,
29    InvalidRuntimePath,
30}
31
32impl std::fmt::Display for RustInstrumenterError {
33    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::SourceTooLarge => write!(formatter, "Rust source exceeds the parser range"),
36            Self::Parse(errors) => write!(formatter, "Rust parse failed: {}", errors.join("; ")),
37            Self::InvalidRange => write!(formatter, "Rust parser returned an invalid range"),
38            Self::InvalidRuntimePath => write!(formatter, "invalid generated Rust runtime path"),
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct RustInstrumentedSource {
45    pub code: String,
46    pub manifest: CoverageManifest,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum InsertionKind {
51    End,
52    Direct,
53    Start,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57struct Insertion {
58    offset: usize,
59    kind: InsertionKind,
60    scope_len: usize,
61    rank: usize,
62    text: String,
63}
64
65fn valid_runtime_path(path: &str) -> bool {
66    let mut parts = path.split("::");
67    if !matches!(parts.next(), Some("crate")) {
68        return false;
69    }
70    let parts = parts.collect::<Vec<_>>();
71    !parts.is_empty()
72        && parts.into_iter().all(|part| {
73            !part.is_empty()
74                && part.bytes().enumerate().all(|(index, byte)| {
75                    byte == b'_'
76                        || byte.is_ascii_alphabetic()
77                        || (index > 0 && byte.is_ascii_digit())
78                })
79        })
80}
81
82/// Report whether rustc will evaluate this node at compile time.
83///
84/// Runtime probes cannot appear anywhere this is true: `condition`, `decision`
85/// and `hit` are not `const fn`, so emitting a call here is not a bad
86/// measurement but a build failure (E0015). bytes-1.12.1 hit exactly that with
87/// `const ITERS: usize = if cfg!(miri) { 100 } else { 1_000 };`.
88///
89/// `ConstArg` is the shared node for enum discriminants, array lengths, const
90/// generic arguments and const parameter defaults, so matching it covers all
91/// four. The remaining case is an array repeat expression, `[value; count]`,
92/// where only the count after the semicolon is const-evaluated.
93/// Report whether the source's doc comments contain a fenced code block.
94///
95/// rustdoc turns fenced blocks in `///`, `//!` and `#[doc]` text into doctest
96/// crates. The scan is line-based and deliberately coarse: a fence inside a
97/// doc comment declares the limitation even when the fence is `ignore`d, which
98/// over-declares the unmeasured surface rather than ever under-declaring it.
99fn in_const_context(node: &ra_ap_syntax::SyntaxNode) -> bool {
100    let start = node.text_range().start();
101    node.ancestors().any(|ancestor| {
102        ast::Fn::cast(ancestor.clone()).is_some_and(|function| function.const_token().is_some())
103            || ast::BlockExpr::cast(ancestor.clone())
104                .is_some_and(|block| block.const_token().is_some())
105            || ast::Const::can_cast(ancestor.kind())
106            || ast::Static::can_cast(ancestor.kind())
107            || ast::ConstArg::can_cast(ancestor.kind())
108            || ast::ArrayExpr::cast(ancestor).is_some_and(|array| {
109                array
110                    .semicolon_token()
111                    .is_some_and(|semicolon| start >= semicolon.text_range().end())
112            })
113    })
114}
115
116/// Report whether this node sits inside a `GlobalAlloc` implementation.
117///
118/// The probe runtime allocates, so a probe inside `alloc` calls back into
119/// `alloc`, which probes again, until the stack is gone. bytes-1.12.1's
120/// tests/test_bytes_odd_alloc.rs installs a `#[global_allocator]`, and the
121/// instrumented binary died with SIGSEGV before libtest could even list its
122/// tests -- while the uninstrumented one listed them fine.
123///
124/// The general rule this enforces is that nothing the runtime itself calls can
125/// carry a probe, and `#[global_allocator]` is the one way a user crate gets
126/// onto that path. A `GlobalAlloc` impl is skipped whether or not it is the
127/// registered allocator, because the registering `static` may live in another
128/// file: declining a handful of allocator bodies costs almost no exactness,
129/// while instrumenting the live one costs the whole run.
130fn in_global_allocator(node: &ra_ap_syntax::SyntaxNode) -> bool {
131    node.ancestors().any(|ancestor| {
132        ast::Impl::cast(ancestor).is_some_and(|block| {
133            block.trait_().is_some_and(|implemented| {
134                implemented
135                    .syntax()
136                    .descendants_with_tokens()
137                    .filter_map(|element| element.into_token())
138                    .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
139            })
140        })
141    })
142}
143
144/// Report whether a probe placed at this node could not run correctly.
145fn cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
146    in_const_context(node) || in_global_allocator(node)
147}
148
149fn range_offsets(range: TextRange) -> (usize, usize) {
150    (usize::from(range.start()), usize::from(range.end()))
151}
152
153fn push_wrapper(
154    insertions: &mut Vec<Insertion>,
155    range: TextRange,
156    scope: TextRange,
157    rank: usize,
158    prefix: String,
159    suffix: String,
160) {
161    let (start, end) = range_offsets(range);
162    let (scope_start, scope_end) = range_offsets(scope);
163    let scope_len = scope_end - scope_start;
164    insertions.push(Insertion {
165        offset: start,
166        kind: InsertionKind::Start,
167        scope_len,
168        rank,
169        text: prefix,
170    });
171    insertions.push(Insertion {
172        offset: end,
173        kind: InsertionKind::End,
174        scope_len,
175        rank,
176        text: suffix,
177    });
178}
179
180fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
181    insertions.push(Insertion {
182        offset,
183        kind: InsertionKind::Direct,
184        scope_len: 0,
185        rank: 0,
186        text,
187    });
188}
189
190fn apply_insertions(
191    source: &str,
192    mut insertions: Vec<Insertion>,
193) -> Result<String, RustInstrumenterError> {
194    if insertions
195        .iter()
196        .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
197    {
198        return Err(RustInstrumenterError::InvalidRange);
199    }
200    insertions.sort_by(|left, right| {
201        left.offset.cmp(&right.offset).then_with(|| {
202            let kind_order = |kind: InsertionKind| match kind {
203                InsertionKind::End => 0,
204                InsertionKind::Direct => 1,
205                InsertionKind::Start => 2,
206            };
207            kind_order(left.kind)
208                .cmp(&kind_order(right.kind))
209                .then_with(|| match left.kind {
210                    InsertionKind::End => left
211                        .scope_len
212                        .cmp(&right.scope_len)
213                        .then_with(|| right.rank.cmp(&left.rank)),
214                    InsertionKind::Direct => std::cmp::Ordering::Equal,
215                    InsertionKind::Start => right
216                        .scope_len
217                        .cmp(&left.scope_len)
218                        .then_with(|| left.rank.cmp(&right.rank)),
219                })
220        })
221    });
222
223    let mut output = source.to_owned();
224    let mut index = insertions.len();
225    while index > 0 {
226        let offset = insertions[index - 1].offset;
227        let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
228        let text = insertions[start..index]
229            .iter()
230            .map(|insertion| insertion.text.as_str())
231            .collect::<String>();
232        output.insert_str(offset, &text);
233        index = start;
234    }
235    Ok(output)
236}
237
238fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
239    if manifest
240        .limitations
241        .iter()
242        .any(|limitation| limitation.get("id").and_then(|value| value.as_str()) == Some(id))
243    {
244        return;
245    }
246    manifest.limitations.push(json!({
247        "id": id,
248        "kind": "rust-frontend-readiness",
249        "file": file,
250        "line": 1,
251        "column": 0,
252        "source": "",
253        "reason": reason
254    }));
255}
256
257fn allocate_frame_name(
258    file: &str,
259    condition: &ast::Expr,
260    kind: &str,
261    identifiers: &mut BTreeSet<String>,
262) -> String {
263    let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
264    let suffix = id.rsplit(':').next().unwrap_or("decision");
265    let base = format!("__supercov_decision_{suffix}");
266    let mut candidate = base.clone();
267    let mut attempt = 0_usize;
268    while !identifiers.insert(candidate.clone()) {
269        attempt += 1;
270        candidate = format!("{base}_{attempt}");
271    }
272    candidate
273}
274
275/// The `const` naming a match's alternative IDs. Upper case, so it raises no
276/// naming lint in a crate that denies warnings.
277fn allocate_table_name(
278    file: &str,
279    expression: &ast::MatchExpr,
280    identifiers: &mut BTreeSet<String>,
281) -> String {
282    let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
283    let suffix = id
284        .rsplit(':')
285        .next()
286        .unwrap_or("match")
287        .to_ascii_uppercase();
288    let base = format!("__SUPERCOV_ARMS_{suffix}");
289    let mut candidate = base.clone();
290    let mut attempt = 0_usize;
291    while !identifiers.insert(candidate.clone()) {
292        attempt += 1;
293        candidate = format!("{base}_{attempt}");
294    }
295    candidate
296}
297
298/// The local that remembers whether a `while` loop has run its body.
299fn allocate_flag_name(
300    file: &str,
301    expression: &ast::WhileExpr,
302    identifiers: &mut BTreeSet<String>,
303) -> String {
304    let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
305    let suffix = id.rsplit(':').next().unwrap_or("loop");
306    let base = format!("__supercov_loop_{suffix}");
307    let mut candidate = base.clone();
308    let mut attempt = 0_usize;
309    while !identifiers.insert(candidate.clone()) {
310        attempt += 1;
311        candidate = format!("{base}_{attempt}");
312    }
313    candidate
314}
315
316impl std::error::Error for RustInstrumenterError {}
317
318struct SourceLocations<'a> {
319    source: &'a str,
320    line_starts: Vec<usize>,
321}
322
323impl<'a> SourceLocations<'a> {
324    fn new(source: &'a str) -> Self {
325        let mut line_starts = vec![0];
326        line_starts.extend(
327            source
328                .bytes()
329                .enumerate()
330                .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
331        );
332        Self {
333            source,
334            line_starts,
335        }
336    }
337
338    fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
339        let start = usize::from(range.start());
340        let end = usize::from(range.end());
341        if start > end
342            || end > self.source.len()
343            || !self.source.is_char_boundary(start)
344            || !self.source.is_char_boundary(end)
345        {
346            return Err(RustInstrumenterError::InvalidRange);
347        }
348        Ok((start, end))
349    }
350
351    fn line_column(&self, offset: usize) -> (usize, usize) {
352        let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
353        (line_index + 1, offset - self.line_starts[line_index])
354    }
355
356    fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
357        let (start, end) = self.range(range)?;
358        Ok(self.source[start..end].trim().to_owned())
359    }
360}
361
362fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
363    let mut hash = Sha256::new();
364    let start = usize::from(range.start()).to_string();
365    let end = usize::from(range.end()).to_string();
366    for value in [file, kind, &start, &end, suffix] {
367        hash.update(value.as_bytes());
368        hash.update([0]);
369    }
370    let digest = hash.finalize();
371    let mut encoded = String::with_capacity(24);
372    for byte in &digest[..12] {
373        use std::fmt::Write as _;
374        write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
375    }
376    format!("rs:{kind}:{encoded}")
377}
378
379struct RustObligationCollector<'a> {
380    file: &'a str,
381    locations: SourceLocations<'a>,
382    manifest: CoverageManifest,
383    point_ids: BTreeSet<String>,
384    decision_ids: BTreeSet<String>,
385    branch_ids: BTreeSet<String>,
386    limitation_ids: BTreeSet<&'static str>,
387    error: Option<RustInstrumenterError>,
388}
389
390impl<'a> RustObligationCollector<'a> {
391    fn new(file: &'a str, source: &'a str) -> Self {
392        Self {
393            file,
394            locations: SourceLocations::new(source),
395            manifest: CoverageManifest {
396                unmeasured: Vec::new(),
397                decisions: Vec::new(),
398                points: Vec::new(),
399                branches: Vec::new(),
400                limitations: Vec::new(),
401                scope: None,
402            },
403            point_ids: BTreeSet::new(),
404            decision_ids: BTreeSet::new(),
405            branch_ids: BTreeSet::new(),
406            limitation_ids: BTreeSet::new(),
407            error: None,
408        }
409    }
410
411    fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
412        let result = self.locations.range(range).map(|(start, _)| {
413            let (line, column) = self.locations.line_column(start);
414            (line, column, self.locations.text(range))
415        });
416        match result {
417            Ok((line, column, Ok(source))) => Some((line, column, source)),
418            Ok((_, _, Err(error))) | Err(error) => {
419                self.error.get_or_insert(error);
420                None
421            }
422        }
423    }
424
425    fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
426        let kind_name = match kind {
427            PointKind::Statement => "statement",
428            PointKind::Function => "function",
429        };
430        let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
431        if !self.point_ids.insert(id.clone()) {
432            return;
433        }
434        let Some((line, column, source)) = self.location_source(range) else {
435            return;
436        };
437        self.manifest.points.push(PointMeta {
438            id,
439            kind,
440            file: self.file.into(),
441            line,
442            column,
443            source,
444            label,
445        });
446    }
447
448    fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
449        match expression {
450            ast::Expr::ParenExpr(paren) => {
451                if let Some(inner) = paren.expr() {
452                    Self::atomic_condition_ranges(&inner, ranges);
453                } else {
454                    ranges.push(expression.syntax().text_range());
455                }
456            }
457            ast::Expr::BinExpr(binary)
458                if matches!(
459                    binary.op_kind(),
460                    Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
461                ) =>
462            {
463                if let Some(left) = binary.lhs() {
464                    Self::atomic_condition_ranges(&left, ranges);
465                }
466                if let Some(right) = binary.rhs() {
467                    Self::atomic_condition_ranges(&right, ranges);
468                }
469            }
470            _ => ranges.push(expression.syntax().text_range()),
471        }
472    }
473
474    fn decision(&mut self, test: &ast::Expr, kind: &str) {
475        let range = test.syntax().text_range();
476        let id = stable_id(self.file, "decision", range, kind);
477        if !self.decision_ids.insert(id.clone()) {
478            return;
479        }
480        let Some((line, column, source)) = self.location_source(range) else {
481            return;
482        };
483        let mut condition_ranges = Vec::new();
484        Self::atomic_condition_ranges(test, &mut condition_ranges);
485        let mut conditions = Vec::with_capacity(condition_ranges.len());
486        for condition in condition_ranges {
487            match self.locations.text(condition) {
488                Ok(source) => conditions.push(source),
489                Err(error) => {
490                    self.error.get_or_insert(error);
491                    return;
492                }
493            }
494        }
495        self.manifest.decisions.push(DecisionMeta {
496            id: id.clone(),
497            file: self.file.into(),
498            line,
499            column,
500            source: source.clone(),
501            conditions,
502            kind: kind.into(),
503        });
504        self.branch_with_id(
505            format!("{id}:outcome"),
506            range,
507            kind,
508            source,
509            [("true", "true"), ("false", "false")],
510        );
511    }
512
513    fn branch<const N: usize>(
514        &mut self,
515        range: TextRange,
516        kind: &str,
517        alternatives: [(&str, &str); N],
518    ) {
519        let id = stable_id(self.file, "branch", range, kind);
520        let Some((_, _, source)) = self.location_source(range) else {
521            return;
522        };
523        self.branch_with_id(id, range, kind, source, alternatives);
524    }
525
526    fn branch_with_id<const N: usize>(
527        &mut self,
528        id: String,
529        range: TextRange,
530        kind: &str,
531        source: String,
532        alternatives: [(&str, &str); N],
533    ) {
534        if !self.branch_ids.insert(id.clone()) {
535            return;
536        }
537        let Some((line, column, _)) = self.location_source(range) else {
538            return;
539        };
540        self.manifest.branches.push(BranchMeta {
541            id: id.clone(),
542            kind: kind.into(),
543            file: self.file.into(),
544            line,
545            column,
546            source,
547            alternatives: alternatives
548                .into_iter()
549                .map(|(suffix, label)| BranchAlternativeMeta {
550                    id: format!("{id}:{suffix}"),
551                    label: label.into(),
552                })
553                .collect(),
554        });
555    }
556
557    fn limitation(&mut self, id: &'static str, reason: &'static str) {
558        if !self.limitation_ids.insert(id) {
559            return;
560        }
561        self.manifest.limitations.push(json!({
562            "id": id,
563            "kind": "rust-frontend-readiness",
564            "file": self.file,
565            "line": 1,
566            "column": 0,
567            "source": "",
568            "reason": reason
569        }));
570    }
571
572    fn collect(
573        mut self,
574        file: &SourceFile,
575        assertions: &[TextRange],
576        matches: &[TextRange],
577    ) -> Result<CoverageManifest, RustInstrumenterError> {
578        let root = file.syntax();
579
580        for list in root.descendants().filter_map(ast::StmtList::cast) {
581            for statement in list.statements() {
582                match statement {
583                    ast::Stmt::ExprStmt(statement) => {
584                        self.point(statement.syntax().text_range(), PointKind::Statement, None);
585                    }
586                    ast::Stmt::LetStmt(statement) => {
587                        self.point(statement.syntax().text_range(), PointKind::Statement, None);
588                    }
589                    ast::Stmt::Item(_) => {}
590                }
591            }
592            if let Some(tail) = list.tail_expr() {
593                self.point(tail.syntax().text_range(), PointKind::Statement, None);
594            }
595        }
596
597        for function in root.descendants().filter_map(ast::Fn::cast) {
598            if function.body().is_none() {
599                continue;
600            }
601            if function.const_token().is_some() {
602                self.limitation(
603                    "rust-const-context-not-instrumented",
604                    "Runtime probes cannot execute in const fn or compile-time evaluation",
605                );
606                continue;
607            }
608            let label = function.name().map(|name| name.text().to_string());
609            self.point(function.syntax().text_range(), PointKind::Function, label);
610        }
611
612        for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
613            self.point(
614                closure.syntax().text_range(),
615                PointKind::Function,
616                Some("<closure>".into()),
617            );
618        }
619
620        for expression in root.descendants().filter_map(ast::IfExpr::cast) {
621            if let Some(condition) = expression.condition() {
622                self.decision(&condition, "if");
623            }
624        }
625        for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
626            if let Some(condition) = expression.condition() {
627                self.decision(&condition, "while");
628            }
629            self.branch(
630                expression.syntax().text_range(),
631                "while-loop",
632                [("zero", "zero iterations"), ("entered", "entered")],
633            );
634        }
635        for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
636            if let Some(condition) = guard.condition() {
637                self.decision(&condition, "match-guard");
638            }
639        }
640        for arguments in assertions {
641            if let Some(condition) = assertion_condition(root, *arguments) {
642                self.decision(&condition, "assert");
643            }
644        }
645        for expression in standalone_matches(root, matches, assertions) {
646            self.decision(&expression, "matches");
647        }
648
649        for binary in root.descendants().filter_map(ast::BinExpr::cast) {
650            let kind = match binary.op_kind() {
651                Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
652                Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
653                _ => continue,
654            };
655            let range = binary.rhs().map_or_else(
656                || binary.syntax().text_range(),
657                |right| right.syntax().text_range(),
658            );
659            self.branch(
660                range,
661                kind,
662                [
663                    ("short-circuit", "short-circuited"),
664                    ("evaluated", "right operand evaluated"),
665                ],
666            );
667        }
668
669        for expression in root.descendants().filter_map(ast::ForExpr::cast) {
670            self.branch(
671                expression.syntax().text_range(),
672                "for-loop",
673                [("zero", "zero iterations"), ("entered", "entered")],
674            );
675        }
676        for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
677            let Some(list) = expression.match_arm_list() else {
678                continue;
679            };
680            let arms = list.arms().collect::<Vec<_>>();
681            let last = arms.len().saturating_sub(1);
682            for (index, arm) in arms.iter().enumerate() {
683                let range = arm.syntax().text_range();
684                if index == last {
685                    // A match is exhaustive, so once every earlier arm has
686                    // been passed over the last one is selected: it can be
687                    // reached but never skipped.
688                    self.branch(range, "match-arm", [("selected", "selected")]);
689                } else {
690                    self.branch(
691                        range,
692                        "match-arm",
693                        [("missed", "not selected"), ("selected", "selected")],
694                    );
695                }
696            }
697        }
698        for expression in root.descendants().filter_map(ast::TryExpr::cast) {
699            self.branch(
700                expression.syntax().text_range(),
701                "try-operator",
702                [("continued", "continued"), ("returned", "early return")],
703            );
704        }
705
706        if root.descendants().any(|node| {
707            ast::MacroCall::can_cast(node.kind()) || ast::MacroExpr::can_cast(node.kind())
708        }) {
709            // Only macros the view could not open remain: anything but the
710            // std expression macros.
711            self.limitation(
712                "rust-macro-expansion-not-instrumented",
713                "Arguments of macros other than the std expression macros (assert!, println!, vec!, ...) and all macro expansions are not part of the owned source denominator",
714            );
715        }
716
717        // An obligation the probes cannot reach stays in the denominator, but the
718        // gap has to be declared rather than left to read as merely uncovered.
719        // Only a context that actually holds an obligation counts:
720        // `const MAX: usize = 10;` costs nothing and must not raise a limitation.
721        let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
722            ast::StmtList::cast(node.clone()).is_some_and(|list| {
723                list.statements().next().is_some() || list.tail_expr().is_some()
724            }) || ast::IfExpr::can_cast(node.kind())
725                || ast::WhileExpr::can_cast(node.kind())
726                || ast::MatchGuard::can_cast(node.kind())
727                || ast::ForExpr::can_cast(node.kind())
728                || ast::MatchArm::can_cast(node.kind())
729                || ast::TryExpr::can_cast(node.kind())
730                || ast::ClosureExpr::can_cast(node.kind())
731                || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
732                    matches!(
733                        binary.op_kind(),
734                        Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
735                    )
736                })
737        };
738        if root
739            .descendants()
740            .any(|node| bears_obligation(&node) && in_const_context(&node))
741        {
742            self.limitation(
743                "rust-const-context-not-instrumented",
744                "Runtime probes cannot execute in const fn or compile-time evaluation",
745            );
746        }
747        if root
748            .descendants()
749            .any(|node| bears_obligation(&node) && in_global_allocator(&node))
750        {
751            self.limitation(
752                "rust-global-allocator-not-instrumented",
753                "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates",
754            );
755        }
756
757        if let Some(error) = self.error {
758            return Err(error);
759        }
760        self.manifest
761            .decisions
762            .sort_by(|left, right| left.id.cmp(&right.id));
763        self.manifest
764            .points
765            .sort_by(|left, right| left.id.cmp(&right.id));
766        self.manifest
767            .branches
768            .sort_by(|left, right| left.id.cmp(&right.id));
769        self.manifest.limitations.sort_by(|left, right| {
770            left.get("id")
771                .and_then(|value| value.as_str())
772                .cmp(&right.get("id").and_then(|value| value.as_str()))
773        });
774        Ok(self.manifest)
775    }
776}
777
778/// Set to a directory to receive the transformed text of any file whose
779/// instrumentation no longer parses, named after the file.
780pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
781
782/// The std macros whose arguments are ordinary expressions. With the `!`
783/// turned into `_` (and `vec!`'s brackets into parentheses), `name!(args)`
784/// reads as the call `name_(args)` at the same byte offsets -- the same
785/// statement start, the arguments an argument list. Probes then land inside
786/// the arguments, and the macro receives instrumented expressions. `vec![x;
787/// n]` reads as the array `[x; n]` instead, with `vec!` blanked. `matches!`
788/// reads as a call on its scrutinee alone, its pattern blanked, and the whole
789/// `matches!` is a decision of its own.
790const EXPRESSION_MACROS: &[&str] = &[
791    "assert",
792    "debug_assert",
793    "assert_eq",
794    "assert_ne",
795    "debug_assert_eq",
796    "debug_assert_ne",
797    "println",
798    "print",
799    "eprintln",
800    "eprint",
801    "format",
802    "format_args",
803    "write",
804    "writeln",
805    "panic",
806    "unreachable",
807    "todo",
808    "unimplemented",
809    "vec",
810    "dbg",
811    "matches",
812];
813
814/// Macros whose first argument decides whether the program goes on.
815const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
816
817/// The source as the instrumenter reads it: every expression macro rewritten
818/// so its arguments parse as expressions, offsets intact. `assertions` holds
819/// the argument-list ranges of `assert!`-like calls.
820struct ExpressionView {
821    text: String,
822    assertions: Vec<TextRange>,
823    /// The ranges of whole `matches!(...)` calls: each is a boolean decision.
824    matches: Vec<TextRange>,
825}
826
827/// The editions tried when parsing, newest first: a file that parses under
828/// the newest is the common case, and an older one accepts words the newest
829/// reserves -- `gen` is an identifier before 2024, and crates still call
830/// `rng.gen()`. The edition that parses the original also parses its view and
831/// its transformed text.
832const EDITIONS: [Edition; 4] = [
833    Edition::Edition2024,
834    Edition::Edition2021,
835    Edition::Edition2018,
836    Edition::Edition2015,
837];
838
839fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
840    let mut newest_errors = None;
841    for edition in EDITIONS {
842        let parsed = SourceFile::parse(source, edition);
843        let errors = parsed.errors();
844        if errors.is_empty() {
845            return Ok((parsed.tree(), edition));
846        }
847        newest_errors.get_or_insert_with(|| {
848            errors
849                .into_iter()
850                .map(|error| error.to_string())
851                .collect::<Vec<_>>()
852        });
853    }
854    Err(newest_errors.unwrap_or_default())
855}
856
857fn expression_view(source: &str, edition: Edition) -> ExpressionView {
858    let mut text = source.to_owned();
859    let mut assertions = Vec::new();
860    let mut matches = Vec::new();
861    // A macro inside another macro's arguments is tokens until the outer one
862    // reads as a call, so rewrite, re-parse, and repeat until nothing changes.
863    for _ in 0..16 {
864        let tree = SourceFile::parse(&text, edition).tree();
865        let Some(next) =
866            rewrite_expression_macros(&text, &tree, edition, &mut assertions, &mut matches)
867        else {
868            break;
869        };
870        text = next;
871    }
872    ExpressionView {
873        text,
874        assertions,
875        matches,
876    }
877}
878
879/// Whether a macro call is a statement of its own or a block's tail: there
880/// its start is the statement's start, which a blanked prefix would move.
881fn is_statement_macro(call: &ast::MacroCall) -> bool {
882    call.syntax().parent().is_some_and(|parent| {
883        ast::MacroExpr::can_cast(parent.kind())
884            && parent.parent().is_some_and(|grandparent| {
885                ast::ExprStmt::can_cast(grandparent.kind())
886                    || ast::StmtList::can_cast(grandparent.kind())
887            })
888    })
889}
890
891/// The offset just after the first top-level comma of a token tree, if any.
892fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
893    arguments
894        .syntax()
895        .children_with_tokens()
896        .filter_map(|element| element.into_token())
897        .find(|token| token.kind() == SyntaxKind::COMMA)
898        .map(|token| usize::from(token.text_range().end()))
899}
900
901/// One pass over the known macros of `tree`; the rewritten text, or None when
902/// no macro was left to rewrite.
903fn rewrite_expression_macros(
904    source: &str,
905    tree: &SourceFile,
906    edition: Edition,
907    assertions: &mut Vec<TextRange>,
908    matches: &mut Vec<TextRange>,
909) -> Option<String> {
910    let mut text = source.as_bytes().to_vec();
911    let mut changed = false;
912    for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
913        let Some(name) = call
914            .path()
915            .and_then(|path| path.segment())
916            .and_then(|segment| segment.name_ref())
917            .map(|name| name.text().to_string())
918        else {
919            continue;
920        };
921        if !EXPRESSION_MACROS.contains(&name.as_str()) {
922            continue;
923        }
924        let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
925            continue;
926        };
927        let parenthesised = arguments.l_paren_token().is_some();
928        if !parenthesised && arguments.l_brack_token().is_none() {
929            continue;
930        }
931        let range = arguments.syntax().text_range();
932        let (start, end) = (usize::from(range.start()), usize::from(range.end()));
933        let mut rewritten = source.as_bytes()[start..end].to_vec();
934        if !parenthesised {
935            rewritten[0] = b'(';
936            *rewritten
937                .last_mut()
938                .expect("a token tree has a closing delimiter") = b')';
939        }
940        if name == "matches" {
941            // `matches!(e, pat)`: the scrutinee is an expression, the pattern
942            // is not. Blank the pattern so the call reads as `matches_(e, )`.
943            let Some(comma_end) = first_top_level_comma_end(&arguments) else {
944                continue;
945            };
946            for byte in &mut rewritten[comma_end - start..end - start - 1] {
947                if *byte != b'\n' {
948                    *byte = b' ';
949                }
950            }
951        }
952        // The arguments must read as a call's argument list.
953        let probe = format!(
954            "fn __supercov() {{ let _ = __f{}; }}",
955            String::from_utf8_lossy(&rewritten)
956        );
957        if !SourceFile::parse(&probe, edition).errors().is_empty() {
958            // `vec![x; n]` is no argument list; as the array `[x; n]` it still
959            // holds expressions. Blanking `vec!` moves the start of a
960            // statement the macro forms on its own, so that shape stays.
961            if !parenthesised && !is_statement_macro(&call) {
962                let array = source.as_bytes()[start..end].to_vec();
963                let array_probe = format!(
964                    "fn __supercov() {{ let _ = {}; }}",
965                    String::from_utf8_lossy(&array)
966                );
967                if SourceFile::parse(&array_probe, edition).errors().is_empty() {
968                    let prefix_start = usize::from(call.syntax().text_range().start());
969                    let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
970                        usize::from(attribute.syntax().text_range().end())
971                    });
972                    for byte in &mut text[prefix_start..start] {
973                        if *byte != b'\n' {
974                            *byte = b' ';
975                        }
976                    }
977                    changed = true;
978                }
979            }
980            continue;
981        }
982        text[usize::from(bang.text_range().start())] = b'_';
983        text[start..end].copy_from_slice(&rewritten);
984        changed = true;
985        if ASSERTION_MACROS.contains(&name.as_str()) {
986            assertions.push(range);
987        }
988        if name == "matches" {
989            matches.push(call.syntax().text_range());
990        }
991    }
992    changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
993}
994
995/// How many arguments an `assert!`-like call has: one means the macro would
996/// build the panic message from the condition's own text.
997fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
998    root.descendants()
999        .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1000        .and_then(ast::ArgList::cast)
1001        .map_or(0, |list| list.args().count())
1002}
1003
1004/// The condition of an `assert!`-like call, found in the view by the range of
1005/// the call's argument list: its first argument.
1006fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1007    root.descendants()
1008        .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1009        .and_then(ast::ArgList::cast)?
1010        .args()
1011        .next()
1012}
1013
1014/// Parse the source, then parse its expression view; the view is what the
1015/// collector and the instrumenter walk. Should the view not parse -- an
1016/// argument list that stands alone but not in place -- the original tree is
1017/// used and every macro stays declared.
1018struct ParsedSource {
1019    tree: SourceFile,
1020    assertions: Vec<TextRange>,
1021    matches: Vec<TextRange>,
1022    edition: Edition,
1023}
1024
1025fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1026    if source.len() > u32::MAX as usize {
1027        return Err(RustInstrumenterError::SourceTooLarge);
1028    }
1029    let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1030    let view = expression_view(source, edition);
1031    let parsed_view = SourceFile::parse(&view.text, edition);
1032    if parsed_view.errors().is_empty() {
1033        Ok(ParsedSource {
1034            tree: parsed_view.tree(),
1035            assertions: view.assertions,
1036            matches: view.matches,
1037            edition,
1038        })
1039    } else {
1040        Ok(ParsedSource {
1041            tree,
1042            assertions: Vec::new(),
1043            matches: Vec::new(),
1044            edition,
1045        })
1046    }
1047}
1048
1049/// The `matches!` calls that stand as decisions of their own: those not
1050/// already serving as an atomic condition of an `if`, `while`, guard or
1051/// assertion, whose decision records them.
1052fn standalone_matches(
1053    root: &ra_ap_syntax::SyntaxNode,
1054    matches: &[TextRange],
1055    assertions: &[TextRange],
1056) -> Vec<ast::Expr> {
1057    let mut atoms = Vec::new();
1058    let mut conditions = Vec::new();
1059    for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1060        conditions.extend(expression.condition());
1061    }
1062    for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1063        conditions.extend(expression.condition());
1064    }
1065    for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1066        conditions.extend(guard.condition());
1067    }
1068    for arguments in assertions {
1069        conditions.extend(assertion_condition(root, *arguments));
1070    }
1071    for condition in &conditions {
1072        RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1073    }
1074    matches
1075        .iter()
1076        .filter(|range| !atoms.contains(range))
1077        .filter_map(|range| {
1078            root.descendants()
1079                .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1080                .and_then(ast::Expr::cast)
1081        })
1082        .collect()
1083}
1084
1085pub fn build_rust_manifest(
1086    file: &str,
1087    source: &str,
1088) -> Result<CoverageManifest, RustInstrumenterError> {
1089    let parsed = parse_for_instrumentation(source)?;
1090    RustObligationCollector::new(file, source).collect(
1091        &parsed.tree,
1092        &parsed.assertions,
1093        &parsed.matches,
1094    )
1095}
1096
1097fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1098    let list = block.stmt_list()?;
1099    list.attrs()
1100        .last()
1101        .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1102        .or_else(|| {
1103            list.l_curly_token()
1104                .map(|token| usize::from(token.text_range().end()))
1105        })
1106}
1107
1108/// A node's range without its outer attributes: a wrapper placed here stays
1109/// under the attributes, so `#[cfg]` governs the wrapper and the node alike.
1110fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1111    let range = node.syntax().text_range();
1112    node.attrs().last().map_or(range, |attribute| {
1113        TextRange::new(attribute.syntax().text_range().end(), range.end())
1114    })
1115}
1116
1117fn has_let(expression: &ast::Expr) -> bool {
1118    expression
1119        .syntax()
1120        .descendants()
1121        .any(|node| ast::LetExpr::can_cast(node.kind()))
1122}
1123
1124/// The expression whose condition is a let chain.
1125enum ChainHost<'a> {
1126    If(&'a ast::IfExpr),
1127    While(&'a ast::WhileExpr),
1128}
1129
1130/// The `const` naming a let chain's `&&` operators and their alternative IDs.
1131fn allocate_chain_table_name(
1132    file: &str,
1133    condition: &ast::Expr,
1134    identifiers: &mut BTreeSet<String>,
1135) -> String {
1136    let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1137    let suffix = id
1138        .rsplit(':')
1139        .next()
1140        .unwrap_or("chain")
1141        .to_ascii_uppercase();
1142    let base = format!("__SUPERCOV_CHAIN_{suffix}");
1143    let mut candidate = base.clone();
1144    let mut attempt = 0_usize;
1145    while !identifiers.insert(candidate.clone()) {
1146        attempt += 1;
1147        candidate = format!("{base}_{attempt}");
1148    }
1149    candidate
1150}
1151
1152/// A fresh identifier for generated code, from the obligation it serves.
1153fn allocate_identifier(
1154    file: &str,
1155    range: TextRange,
1156    kind: &str,
1157    identifiers: &mut BTreeSet<String>,
1158) -> String {
1159    let id = stable_id(file, kind, range, "");
1160    let suffix = id.rsplit(':').next().unwrap_or(kind);
1161    let base = format!("__supercov_{kind}_{suffix}");
1162    let mut candidate = base.clone();
1163    let mut attempt = 0_usize;
1164    while !identifiers.insert(candidate.clone()) {
1165        attempt += 1;
1166        candidate = format!("{base}_{attempt}");
1167    }
1168    candidate
1169}
1170
1171/// The `break`s in `body` that leave the loop it belongs to: unlabeled ones
1172/// with no other loop between them and the body, and labeled ones naming the
1173/// loop's own label.
1174fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1175    let own_label = label
1176        .and_then(|label| label.lifetime())
1177        .map(|lifetime| lifetime.text().to_string());
1178    body.syntax()
1179        .descendants()
1180        .filter_map(ast::BreakExpr::cast)
1181        .filter(|expression| match expression.lifetime() {
1182            Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1183            None => !expression
1184                .syntax()
1185                .ancestors()
1186                .skip(1)
1187                .take_while(|ancestor| ancestor != body.syntax())
1188                .any(|ancestor| {
1189                    ast::LoopExpr::can_cast(ancestor.kind())
1190                        || ast::WhileExpr::can_cast(ancestor.kind())
1191                        || ast::ForExpr::can_cast(ancestor.kind())
1192                        || ast::ClosureExpr::can_cast(ancestor.kind())
1193                }),
1194        })
1195        .map(|expression| expression.syntax().text_range())
1196        .collect()
1197}
1198
1199/// A decision whose condition holds a `let`. A `let` cannot pass through a
1200/// call and the condition cannot be wrapped as a whole, so the frame lives in
1201/// a block around the `if` or `while`, ordinary conditions take `condition`
1202/// wrappers, each later `let` of a chain is preceded by a `reached` marker,
1203/// and the outcome is recorded where it becomes known: at the entry of the
1204/// then branch or loop body (taken) and at the else branch or after the loop
1205/// (not taken). From those, the runtime derives every pattern's outcome
1206/// exactly: a chain tries its conditions in order and stops at the first that
1207/// fails. The chain's `&&` operators are recorded from the same frame, through
1208/// a table of the operators whose left side holds a `let`.
1209///
1210/// A lone `let` is not a chain, and an `&&` marker would make it one -- which
1211/// editions before 2024 reject. Its evaluation is marked by a statement
1212/// instead: once before an `if`, and for a `while` at each body entry and
1213/// after the loop, where a `break` has to be told from the condition failing.
1214fn instrument_let_chain(
1215    insertions: &mut Vec<Insertion>,
1216    runtime_path: &str,
1217    file: &str,
1218    condition: &ast::Expr,
1219    host: ChainHost<'_>,
1220    identifiers: &mut BTreeSet<String>,
1221) {
1222    // The block goes after any outer attributes, so `#[cfg]` keeps governing
1223    // the frame together with the expression it belongs to.
1224    let (kind, host_range, body, label) = match &host {
1225        ChainHost::If(expression) => (
1226            "if",
1227            range_after_attributes(*expression),
1228            expression.then_branch(),
1229            None,
1230        ),
1231        ChainHost::While(expression) => (
1232            "while",
1233            range_after_attributes(*expression),
1234            expression.loop_body(),
1235            expression.label(),
1236        ),
1237    };
1238    let Some(body) = body else {
1239        return;
1240    };
1241    let Some(body_offset) = block_entry_offset(&body) else {
1242        return;
1243    };
1244    let range = condition.syntax().text_range();
1245    let id = stable_id(file, "decision", range, kind);
1246    let mut atoms = Vec::new();
1247    RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1248    let lets = condition
1249        .syntax()
1250        .descendants()
1251        .filter_map(ast::LetExpr::cast)
1252        .map(|expression| expression.syntax().text_range())
1253        .collect::<Vec<_>>();
1254    let frame = allocate_frame_name(file, condition, kind, identifiers);
1255    let table = allocate_chain_table_name(file, condition, identifiers);
1256    let single_let = atoms.len() == 1;
1257    let broke = match &host {
1258        ChainHost::While(_) if single_let => {
1259            Some(allocate_identifier(file, range, "broke", identifiers))
1260        }
1261        _ => None,
1262    };
1263
1264    let mut operators = Vec::new();
1265    for binary in condition
1266        .syntax()
1267        .descendants()
1268        .filter_map(ast::BinExpr::cast)
1269    {
1270        // Let chains are `&&`-only at the top level; an operator whose left
1271        // side holds a `let` is one the logical wrapper could not touch.
1272        if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1273            continue;
1274        }
1275        let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1276            continue;
1277        };
1278        if !has_let(&left) {
1279            continue;
1280        }
1281        let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1282        let right_range = right.syntax().text_range();
1283        let Some(first) = atoms
1284            .iter()
1285            .position(|atom| right_range.contains_range(*atom))
1286        else {
1287            continue;
1288        };
1289        operators.push(format!(
1290            "({first}, {:?}, {:?})",
1291            format!("{branch}:short-circuit"),
1292            format!("{branch}:evaluated")
1293        ));
1294    }
1295
1296    let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1297    let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1298    let mut prefix = format!(
1299        "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1300        operators.join(", "),
1301        atoms.len()
1302    );
1303    if single_let {
1304        prefix.push_str(&mark);
1305        prefix.push(' ');
1306    }
1307    if let Some(broke) = &broke {
1308        prefix.push_str(&format!("let mut {broke} = false; "));
1309    }
1310    let suffix = match &host {
1311        ChainHost::If(expression) => match expression.else_branch() {
1312            Some(_) => " }".to_owned(),
1313            None => format!(" else {{ {record_false} }} }}"),
1314        },
1315        ChainHost::While(_) => match &broke {
1316            Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1317            None => format!(" {record_false} }}"),
1318        },
1319    };
1320    push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1321    if !single_let {
1322        push_direct(
1323            insertions,
1324            usize::from(range.start()),
1325            format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1326        );
1327    }
1328    for (index, atom) in atoms.iter().enumerate() {
1329        if lets.contains(atom) {
1330            if index > 0 {
1331                push_direct(
1332                    insertions,
1333                    usize::from(atom.start()),
1334                    format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1335                );
1336            }
1337        } else {
1338            push_wrapper(
1339                insertions,
1340                *atom,
1341                *atom,
1342                1,
1343                format!("{runtime_path}::condition(("),
1344                format!("), &mut {frame}, {index})"),
1345            );
1346        }
1347    }
1348    let mut entry = String::new();
1349    if broke.is_some() {
1350        entry.push_str(&format!("\n{mark}"));
1351    }
1352    entry.push_str(&format!(
1353        "\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"
1354    ));
1355    push_direct(insertions, body_offset, entry);
1356    if let Some(broke) = &broke {
1357        for break_range in own_breaks(&body, label) {
1358            push_wrapper(
1359                insertions,
1360                break_range,
1361                break_range,
1362                0,
1363                format!("{{ {broke} = true; "),
1364                " }".into(),
1365            );
1366        }
1367    }
1368    if let ChainHost::If(expression) = &host {
1369        match expression.else_branch() {
1370            Some(ast::ElseBranch::Block(block)) => {
1371                if let Some(offset) = block_entry_offset(&block) {
1372                    push_direct(insertions, offset, format!("\n{record_false}"));
1373                }
1374            }
1375            Some(ast::ElseBranch::IfExpr(nested)) => {
1376                let nested_range = nested.syntax().text_range();
1377                push_wrapper(
1378                    insertions,
1379                    nested_range,
1380                    nested_range,
1381                    0,
1382                    format!("{{ {record_false} "),
1383                    " }".into(),
1384                );
1385            }
1386            None => {}
1387        }
1388    }
1389}
1390
1391/// Where an item for `node` can go: the entry of the nearest enclosing block.
1392/// An item declared there is visible to the whole block, so nothing about the
1393/// expression itself -- its value, its temporaries -- changes.
1394fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1395    node.ancestors()
1396        .skip(1)
1397        .find_map(ast::BlockExpr::cast)
1398        .and_then(|block| block_entry_offset(&block))
1399}
1400
1401/// A block written as a bare `{ ... }`: a probe placed just inside its brace
1402/// runs when the block is entered. Labeled, `unsafe`, `async` and `const`
1403/// blocks are wrapped instead, so an `async` body does not defer the probe.
1404fn plain_block(block: &ast::BlockExpr) -> bool {
1405    block
1406        .syntax()
1407        .first_token()
1408        .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1409}
1410
1411fn instrument_decision(
1412    insertions: &mut Vec<Insertion>,
1413    runtime_path: &str,
1414    file: &str,
1415    condition: &ast::Expr,
1416    kind: &str,
1417    frame_name: &str,
1418) -> bool {
1419    if cannot_carry_probe(condition.syntax())
1420        || condition
1421            .syntax()
1422            .descendants()
1423            .any(|node| ast::LetExpr::can_cast(node.kind()))
1424    {
1425        return false;
1426    }
1427    let range = condition.syntax().text_range();
1428    let id = stable_id(file, "decision", range, kind);
1429    let mut condition_ranges = Vec::new();
1430    RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1431    push_wrapper(
1432        insertions,
1433        range,
1434        range,
1435        0,
1436        format!(
1437            "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1438            condition_ranges.len()
1439        ),
1440        format!("), &mut {frame_name}) }})"),
1441    );
1442    // Each condition's scope is the condition itself, so a wrapper that
1443    // started earlier and ends where this condition ends -- the left operand
1444    // of a logical operator -- closes after it, not before.
1445    for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1446        push_wrapper(
1447            insertions,
1448            atomic_range,
1449            atomic_range,
1450            1,
1451            format!("{runtime_path}::condition(("),
1452            format!("), &mut {frame_name}, {index})"),
1453        );
1454    }
1455    true
1456}
1457
1458/// Produce a private Rust candidate using only Supercov-owned probe calls.
1459///
1460/// The caller supplies a collision-free generated crate-local runtime path.
1461/// Every obligation the manifest declares -- statements, functions, decisions
1462/// with their conditions -- let chains included -- match arms, logical
1463/// operators, loops and the try operator -- takes an owned probe; what a
1464/// probe cannot reach (const contexts, macro expansions, an attributed `let`
1465/// with no initializer) stays in the denominator behind an explicit
1466/// limitation.
1467pub fn instrument_rust_source(
1468    file: &str,
1469    source: &str,
1470    runtime_path: &str,
1471) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1472    if !valid_runtime_path(runtime_path) {
1473        return Err(RustInstrumenterError::InvalidRuntimePath);
1474    }
1475    let mut manifest = build_rust_manifest(file, source)?;
1476    let ParsedSource {
1477        tree,
1478        assertions,
1479        matches,
1480        edition,
1481    } = parse_for_instrumentation(source)?;
1482    let root = tree.syntax();
1483    let mut insertions = Vec::new();
1484    let mut identifiers = root
1485        .descendants_with_tokens()
1486        .filter_map(|element| element.into_token())
1487        .filter(|token| token.kind() == SyntaxKind::IDENT)
1488        .map(|token| token.text().to_string())
1489        .collect::<BTreeSet<_>>();
1490
1491    let mut skipped_attributed_statement = false;
1492    // A probe must never be PREPENDED to a statement that carries outer
1493    // attributes. `#[cfg]` selects among adjacent statements, and a bare
1494    // `hit(...)` inserted between them survives the strip and changes which
1495    // expression is the block's tail: memchr's `is_available` returns bool
1496    // from one of two cfg-gated blocks, and the stray probe turned the kept
1497    // block into a statement and the probe itself into a `()` tail -- 32
1498    // E0308s across the crate. An attributed BLOCK takes the probe inside its
1499    // braces, where the same cfg governs both; any other attributed
1500    // expression is wrapped in such a block. Only a `let` with no initializer
1501    // has nowhere to put a probe, and is declared.
1502    let attributed_probe = |insertions: &mut Vec<Insertion>,
1503                            skipped: &mut bool,
1504                            expression: Option<ast::Expr>,
1505                            has_attrs: bool,
1506                            trailing: bool,
1507                            range: TextRange,
1508                            id: String| {
1509        if !has_attrs {
1510            push_direct(
1511                insertions,
1512                usize::from(range.start()),
1513                format!("{runtime_path}::hit({id:?});"),
1514            );
1515            return;
1516        }
1517        let Some(expression) = expression else {
1518            *skipped = true;
1519            return;
1520        };
1521        if let ast::Expr::BlockExpr(block) = &expression
1522            && let Some(offset) = block_entry_offset(block)
1523        {
1524            push_direct(
1525                insertions,
1526                offset,
1527                format!("\n{runtime_path}::hit({id:?});"),
1528            );
1529            return;
1530        }
1531        // A brace-delimited macro call closing a block without a semicolon
1532        // is a STATEMENT to rustc even though it supplies the block's value
1533        // -- `fn f() -> T { #[rustfmt::skip] m! { .. } }` compiles -- and
1534        // wrapping it as `#[attr] { hit; (m! { .. }) }` would make it an
1535        // attributed tail expression, which is unstable (E0658; tokio's
1536        // `#[rustfmt::skip] tokio::select! { .. }`). The probe goes before
1537        // the attributes instead, in a block wrapping attributes and macro
1538        // together, where the macro is again a trailing statement. A `cfg`
1539        // attribute would then fire the probe for a statement cfg strips, so
1540        // that shape is declared.
1541        if trailing && let ast::Expr::MacroExpr(_) = &expression {
1542            if expression.attrs().any(|attribute| {
1543                // This grammar parses `cfg(..)` as a keyword and predicate
1544                // rather than a path, so the meta's leading word is read.
1545                attribute.meta().is_some_and(|meta| {
1546                    let text = meta.syntax().text().to_string();
1547                    let name = text
1548                        .chars()
1549                        .take_while(|character| character.is_alphanumeric() || *character == '_')
1550                        .collect::<String>();
1551                    matches!(name.as_str(), "cfg" | "cfg_attr")
1552                })
1553            }) {
1554                *skipped = true;
1555                return;
1556            }
1557            let range = expression.syntax().text_range();
1558            push_wrapper(
1559                insertions,
1560                range,
1561                range,
1562                0,
1563                format!("{{ {runtime_path}::hit({id:?}); "),
1564                " }".into(),
1565            );
1566            return;
1567        }
1568        // Any other attributed expression -- or the initializer of an
1569        // attributed `let` -- moves into a block that carries the probe: the
1570        // attributes now govern probe and expression together, the block has
1571        // the expression's value where the expression was, and a block's tail
1572        // still extends the temporaries a `let` would have extended.
1573        let start = expression.attrs().last().map_or_else(
1574            || expression.syntax().text_range().start(),
1575            |attribute| attribute.syntax().text_range().end(),
1576        );
1577        let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1578        push_wrapper(
1579            insertions,
1580            wrapped,
1581            wrapped,
1582            0,
1583            format!(" {{ {runtime_path}::hit({id:?}); ("),
1584            ") }".into(),
1585        );
1586    };
1587    for list in root.descendants().filter_map(ast::StmtList::cast) {
1588        let last_statement = list.statements().last();
1589        for statement in list.statements() {
1590            let (range, expression, has_attrs, trailing) = match &statement {
1591                ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1592                    let expression = statement.expr();
1593                    // Outer attributes on an expression statement attach to
1594                    // the inner expression in this grammar.
1595                    let has_attrs = expression
1596                        .as_ref()
1597                        .is_some_and(|expression| expression.attrs().next().is_some());
1598                    // The block's last statement, without a semicolon and
1599                    // with no tail expression after it, closes the block.
1600                    let trailing = statement.semicolon_token().is_none()
1601                        && list.tail_expr().is_none()
1602                        && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
1603                    (
1604                        statement.syntax().text_range(),
1605                        expression,
1606                        has_attrs,
1607                        trailing,
1608                    )
1609                }
1610                ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1611                    let has_attrs = statement.attrs().next().is_some();
1612                    // Only an attributed `let` needs its initializer; a plain
1613                    // one takes the probe before the statement.
1614                    let initializer = has_attrs.then(|| statement.initializer()).flatten();
1615                    (
1616                        statement.syntax().text_range(),
1617                        initializer,
1618                        has_attrs,
1619                        false,
1620                    )
1621                }
1622                _ => continue,
1623            };
1624            let id = stable_id(file, "statement", range, "");
1625            attributed_probe(
1626                &mut insertions,
1627                &mut skipped_attributed_statement,
1628                expression,
1629                has_attrs,
1630                trailing,
1631                range,
1632                id,
1633            );
1634        }
1635        if let Some(tail) = list
1636            .tail_expr()
1637            .filter(|tail| !cannot_carry_probe(tail.syntax()))
1638        {
1639            let range = tail.syntax().text_range();
1640            let id = stable_id(file, "statement", range, "");
1641            let has_attrs = tail.attrs().next().is_some();
1642            attributed_probe(
1643                &mut insertions,
1644                &mut skipped_attributed_statement,
1645                Some(tail),
1646                has_attrs,
1647                true,
1648                range,
1649                id,
1650            );
1651        }
1652    }
1653
1654    for function in root.descendants().filter_map(ast::Fn::cast) {
1655        // `cannot_carry_probe` covers `const fn` itself, since a node's own
1656        // ancestors include the node.
1657        if cannot_carry_probe(function.syntax()) {
1658            continue;
1659        }
1660        let Some(body) = function.body() else {
1661            continue;
1662        };
1663        let label = function.name().map(|name| name.text().to_string());
1664        let id = stable_id(
1665            file,
1666            "function",
1667            function.syntax().text_range(),
1668            label.as_deref().unwrap_or(""),
1669        );
1670        if let Some(offset) = block_entry_offset(&body) {
1671            push_direct(
1672                &mut insertions,
1673                offset,
1674                format!("\n{runtime_path}::hit({id:?});"),
1675            );
1676        }
1677    }
1678
1679    for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
1680        let Some(body) = closure.body() else {
1681            continue;
1682        };
1683        if cannot_carry_probe(body.syntax()) {
1684            continue;
1685        }
1686        let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
1687        if let ast::Expr::BlockExpr(block) = &body {
1688            if let Some(offset) = block_entry_offset(block) {
1689                push_direct(
1690                    &mut insertions,
1691                    offset,
1692                    format!("\n{runtime_path}::hit({id:?});"),
1693                );
1694            }
1695        } else {
1696            let range = body.syntax().text_range();
1697            push_wrapper(
1698                &mut insertions,
1699                range,
1700                closure.syntax().text_range(),
1701                0,
1702                format!("{{ {runtime_path}::hit({id:?}); ("),
1703                ") }".into(),
1704            );
1705        }
1706    }
1707
1708    // Match arms. One `const` per match, at the entry of the enclosing block,
1709    // names every arm's `not selected` and `selected` IDs in source order;
1710    // each arm then records itself as selected and every arm before it as
1711    // passed over, since a match tries its arms in order and stops at the
1712    // first that fits. The runtime dedupes by ID, so a hot match costs one
1713    // record per alternative.
1714    for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1715        if cannot_carry_probe(expression.syntax()) {
1716            continue;
1717        }
1718        let Some(list) = expression.match_arm_list() else {
1719            continue;
1720        };
1721        let arms = list.arms().collect::<Vec<_>>();
1722        if arms.is_empty() {
1723            continue;
1724        }
1725        let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
1726            continue;
1727        };
1728        let table = allocate_table_name(file, &expression, &mut identifiers);
1729        let entries = arms
1730            .iter()
1731            .map(|arm| {
1732                let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
1733                format!(
1734                    "{:?}, {:?}",
1735                    format!("{id}:missed"),
1736                    format!("{id}:selected")
1737                )
1738            })
1739            .collect::<Vec<_>>()
1740            .join(", ");
1741        push_direct(
1742            &mut insertions,
1743            table_offset,
1744            format!("\nconst {table}: &[&str] = &[{entries}];"),
1745        );
1746        for (index, arm) in arms.iter().enumerate() {
1747            let Some(body) = arm.expr() else {
1748                continue;
1749            };
1750            let call = format!("{runtime_path}::arms({table}, {index});");
1751            match &body {
1752                ast::Expr::BlockExpr(block) if plain_block(block) => {
1753                    if let Some(offset) = block_entry_offset(block) {
1754                        push_direct(&mut insertions, offset, format!("\n{call}"));
1755                    }
1756                }
1757                _ => push_wrapper(
1758                    &mut insertions,
1759                    body.syntax().text_range(),
1760                    arm.syntax().text_range(),
1761                    0,
1762                    format!("{{ {call} ("),
1763                    ") }".into(),
1764                ),
1765            }
1766        }
1767    }
1768
1769    // Logical operators: the left operand alone decides whether the right one
1770    // runs, so wrapping it records the outcome without touching evaluation
1771    // order. `&&` short-circuits on false, `||` on true.
1772    for binary in root.descendants().filter_map(ast::BinExpr::cast) {
1773        let short_circuits_when = match binary.op_kind() {
1774            Some(BinaryOp::LogicOp(LogicOp::And)) => false,
1775            Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
1776            _ => continue,
1777        };
1778        if cannot_carry_probe(binary.syntax()) {
1779            continue;
1780        }
1781        let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1782            continue;
1783        };
1784        // In a let chain the left operand is (or holds) a `let`, whose
1785        // bindings must stay in scope for the right operand; it cannot pass
1786        // through a call. The chain's own probes record that operator.
1787        if has_let(&left) {
1788            continue;
1789        }
1790        let kind = if short_circuits_when {
1791            "logical-or"
1792        } else {
1793            "logical-and"
1794        };
1795        let id = stable_id(file, "branch", right.syntax().text_range(), kind);
1796        push_wrapper(
1797            &mut insertions,
1798            left.syntax().text_range(),
1799            binary.syntax().text_range(),
1800            2,
1801            format!("{runtime_path}::logical(("),
1802            format!(
1803                "), {short_circuits_when}, {:?}, {:?})",
1804                format!("{id}:short-circuit"),
1805                format!("{id}:evaluated")
1806            ),
1807        );
1808    }
1809
1810    // `for` loops: the iterable passes through an adapter that records, on
1811    // the first `next`, whether the body ran at all. `into_iter` is called
1812    // where the loop would have called it, on the same expression.
1813    for expression in root.descendants().filter_map(ast::ForExpr::cast) {
1814        if cannot_carry_probe(expression.syntax()) {
1815            continue;
1816        }
1817        let Some(iterable) = expression.iterable() else {
1818            continue;
1819        };
1820        let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
1821        // Scope is the wrapped range itself: any wrapper that also starts
1822        // here and reaches further -- a decision, a match arm -- must stay
1823        // outside this one.
1824        push_wrapper(
1825            &mut insertions,
1826            iterable.syntax().text_range(),
1827            iterable.syntax().text_range(),
1828            0,
1829            format!("{runtime_path}::for_loop(("),
1830            format!(
1831                "), {:?}, {:?})",
1832                format!("{id}:zero"),
1833                format!("{id}:entered")
1834            ),
1835        );
1836    }
1837
1838    // `while` loops: a flag beside the loop, cleared by the first body entry
1839    // and read once the loop is over. The condition stays as written, so
1840    // `while let` is covered too.
1841    for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1842        if cannot_carry_probe(expression.syntax()) {
1843            continue;
1844        }
1845        let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
1846            continue;
1847        };
1848        let id = stable_id(
1849            file,
1850            "branch",
1851            expression.syntax().text_range(),
1852            "while-loop",
1853        );
1854        let flag = allocate_flag_name(file, &expression, &mut identifiers);
1855        let range = range_after_attributes(&expression);
1856        push_wrapper(
1857            &mut insertions,
1858            range,
1859            range,
1860            0,
1861            format!("{{ let mut {flag} = true; "),
1862            format!(
1863                " {runtime_path}::zero_iterations({flag}, {:?}) }}",
1864                format!("{id}:zero")
1865            ),
1866        );
1867        push_direct(
1868            &mut insertions,
1869            offset,
1870            format!(
1871                "\n{runtime_path}::entered(&mut {flag}, {:?});",
1872                format!("{id}:entered")
1873            ),
1874        );
1875    }
1876
1877    // The try operator: the operand passes through a probe that reads which
1878    // way `?` will go. Every stable `Try` type is covered by the runtime's
1879    // `TryProbe` implementations.
1880    for expression in root.descendants().filter_map(ast::TryExpr::cast) {
1881        if cannot_carry_probe(expression.syntax()) {
1882            continue;
1883        }
1884        let Some(operand) = expression.expr() else {
1885            continue;
1886        };
1887        let id = stable_id(
1888            file,
1889            "branch",
1890            expression.syntax().text_range(),
1891            "try-operator",
1892        );
1893        // Scope is the operand alone: a decision wrapping `expr?` as its
1894        // condition starts at the same offset and must close after this.
1895        push_wrapper(
1896            &mut insertions,
1897            operand.syntax().text_range(),
1898            operand.syntax().text_range(),
1899            0,
1900            format!("{runtime_path}::TryProbe::probe(("),
1901            format!(
1902                "), {:?}, {:?})",
1903                format!("{id}:continued"),
1904                format!("{id}:returned")
1905            ),
1906        );
1907    }
1908
1909    for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1910        let Some(condition) = expression.condition() else {
1911            continue;
1912        };
1913        if has_let(&condition) {
1914            if !cannot_carry_probe(condition.syntax()) {
1915                instrument_let_chain(
1916                    &mut insertions,
1917                    runtime_path,
1918                    file,
1919                    &condition,
1920                    ChainHost::If(&expression),
1921                    &mut identifiers,
1922                );
1923            }
1924            continue;
1925        }
1926        let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
1927        instrument_decision(
1928            &mut insertions,
1929            runtime_path,
1930            file,
1931            &condition,
1932            "if",
1933            &frame_name,
1934        );
1935    }
1936    for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1937        let Some(condition) = expression.condition() else {
1938            continue;
1939        };
1940        if has_let(&condition) {
1941            if !cannot_carry_probe(condition.syntax()) {
1942                instrument_let_chain(
1943                    &mut insertions,
1944                    runtime_path,
1945                    file,
1946                    &condition,
1947                    ChainHost::While(&expression),
1948                    &mut identifiers,
1949                );
1950            }
1951            continue;
1952        }
1953        let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
1954        instrument_decision(
1955            &mut insertions,
1956            runtime_path,
1957            file,
1958            &condition,
1959            "while",
1960            &frame_name,
1961        );
1962    }
1963    for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1964        if let Some(condition) = guard.condition() {
1965            let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
1966            instrument_decision(
1967                &mut insertions,
1968                runtime_path,
1969                file,
1970                &condition,
1971                "match-guard",
1972                &frame_name,
1973            );
1974        }
1975    }
1976    // `assert!(cond, ...)`: the condition decides whether the program goes
1977    // on, and the macro takes an instrumented expression like any other.
1978    // Without a message of its own, `assert!` stringifies the condition into
1979    // the panic message -- which `#[should_panic(expected = "...")]` tests
1980    // read -- so the original text is supplied as the message, exactly as
1981    // the macro would have built it.
1982    for arguments in &assertions {
1983        let Some(condition) = assertion_condition(root, *arguments) else {
1984            continue;
1985        };
1986        let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
1987        if !instrument_decision(
1988            &mut insertions,
1989            runtime_path,
1990            file,
1991            &condition,
1992            "assert",
1993            &frame_name,
1994        ) {
1995            continue;
1996        }
1997        if assertion_argument_count(root, *arguments) == 1 {
1998            let range = condition.syntax().text_range();
1999            let original = &source[usize::from(range.start())..usize::from(range.end())];
2000            push_direct(
2001                &mut insertions,
2002                usize::from(range.end()),
2003                format!(", \"assertion failed: {{}}\", stringify!({original})"),
2004            );
2005        }
2006    }
2007    // `matches!(e, pat)` is a boolean decision wherever it stands; as a
2008    // condition of an `if` it is already one of that decision's atoms.
2009    for expression in standalone_matches(root, &matches, &assertions) {
2010        let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2011        instrument_decision(
2012            &mut insertions,
2013            runtime_path,
2014            file,
2015            &expression,
2016            "matches",
2017            &frame_name,
2018        );
2019    }
2020
2021    if skipped_attributed_statement {
2022        add_manifest_limitation(
2023            &mut manifest,
2024            file,
2025            "rust-attributed-statement-probes-not-injected",
2026            "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2027        );
2028    }
2029    manifest.limitations.sort_by(|left, right| {
2030        left.get("id")
2031            .and_then(|value| value.as_str())
2032            .cmp(&right.get("id").and_then(|value| value.as_str()))
2033    });
2034
2035    let code = apply_insertions(source, insertions)?;
2036    let transformed = SourceFile::parse(&code, edition);
2037    let errors = transformed
2038        .errors()
2039        .into_iter()
2040        .map(|error| error.to_string())
2041        .collect::<Vec<_>>();
2042    if !errors.is_empty() {
2043        // The transformed text is what a diagnosis needs; the parse errors
2044        // alone do not say where. Written only when asked, since it is the
2045        // size of the source.
2046        if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2047            let name = file.replace(['/', '\\'], "__");
2048            let _ = std::fs::create_dir_all(&directory);
2049            let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2050        }
2051        return Err(RustInstrumenterError::Parse(errors));
2052    }
2053    Ok(RustInstrumentedSource { code, manifest })
2054}
2055
2056#[cfg(test)]
2057mod tests {
2058    use std::{
2059        fs,
2060        process::Command,
2061        time::{SystemTime, UNIX_EPOCH},
2062    };
2063
2064    use super::*;
2065
2066    const NOOP_RUNTIME: &str = r#"
2067#[doc(hidden)]
2068mod __supercov_runtime_v1 {
2069    pub struct DecisionFrame;
2070    impl DecisionFrame {
2071        pub fn new(_: &'static str, _: usize) -> Self { Self }
2072    }
2073    pub fn hit(_: &'static str) {}
2074    pub fn arms(_: &[&'static str], _: usize) {}
2075    pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2076    pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2077        iterable.into_iter()
2078    }
2079    pub fn entered(_: &mut bool, _: &'static str) {}
2080    pub fn zero_iterations(_: bool, _: &'static str) {}
2081    pub trait TryProbe: Sized {
2082        fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2083    }
2084    impl<T> TryProbe for T {}
2085    pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2086    pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2087    pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2088    pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2089}
2090"#;
2091
2092    fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2093        compile_and_run_edition(source, name, "2024")
2094    }
2095
2096    fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2097        let nonce = SystemTime::now()
2098            .duration_since(UNIX_EPOCH)
2099            .unwrap()
2100            .as_nanos();
2101        let directory = std::env::temp_dir().join(format!(
2102            "supercov-rust-transform-{}-{nonce}-{name}",
2103            std::process::id()
2104        ));
2105        fs::create_dir(&directory).unwrap();
2106        let input = directory.join("main.rs");
2107        let binary = directory.join("program");
2108        fs::write(&input, source).unwrap();
2109        let compile = Command::new("rustc")
2110            .arg(format!("--edition={edition}"))
2111            .arg(&input)
2112            .arg("-o")
2113            .arg(&binary)
2114            .output()
2115            .unwrap();
2116        assert!(
2117            compile.status.success(),
2118            "rustc failed:\n{}\nsource:\n{source}",
2119            String::from_utf8_lossy(&compile.stderr)
2120        );
2121        let output = Command::new(&binary).output().unwrap();
2122        fs::remove_dir_all(directory).unwrap();
2123        output
2124    }
2125
2126    #[test]
2127    fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2128        let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2129    let picked = if first && (second || third) {
2130        values.first()?
2131    } else {
2132        None
2133    };
2134    for value in values {
2135        if first || second {
2136            return Some(value);
2137        }
2138    }
2139    match picked {
2140        Some(value) if second && third => Some(value),
2141        _ => None,
2142    }
2143}
2144
2145fn closure(value: i32) -> bool {
2146    (|candidate| candidate > 0)(value)
2147}
2148"#;
2149        let first = build_rust_manifest("src/lib.rs", source).unwrap();
2150        let second = build_rust_manifest("src/lib.rs", source).unwrap();
2151        assert_eq!(first, second);
2152        assert!(first.points.iter().any(|point| {
2153            point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2154        }));
2155        assert!(first.points.iter().any(|point| {
2156            point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2157        }));
2158        let first_if = first
2159            .decisions
2160            .iter()
2161            .find(|decision| decision.line == 2)
2162            .unwrap();
2163        assert_eq!(first_if.conditions, ["first", "second", "third"]);
2164        assert_eq!(first_if.column, 20);
2165        assert!(
2166            first
2167                .branches
2168                .iter()
2169                .any(|branch| branch.kind == "for-loop")
2170        );
2171        let mut arms = first
2172            .branches
2173            .iter()
2174            .filter(|branch| branch.kind == "match-arm")
2175            .collect::<Vec<_>>();
2176        arms.sort_by_key(|branch| branch.line);
2177        assert_eq!(arms.len(), 2);
2178        assert_eq!(
2179            arms[0]
2180                .alternatives
2181                .iter()
2182                .map(|alternative| alternative.label.as_str())
2183                .collect::<Vec<_>>(),
2184            ["not selected", "selected"]
2185        );
2186        // The last arm of an exhaustive match is reached or not; it is never
2187        // considered and passed over.
2188        assert_eq!(
2189            arms[1]
2190                .alternatives
2191                .iter()
2192                .map(|alternative| alternative.label.as_str())
2193                .collect::<Vec<_>>(),
2194            ["selected"]
2195        );
2196        assert!(
2197            first
2198                .branches
2199                .iter()
2200                .any(|branch| branch.kind == "try-operator")
2201        );
2202        assert!(first.decisions.iter().all(|decision| {
2203            decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
2204        }));
2205        assert!(first.limitations.is_empty());
2206    }
2207
2208    #[test]
2209    fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
2210        let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
2211
2212macro_rules! noop {
2213    () => {};
2214}
2215
2216fn checked(value: bool) -> bool {
2217    assert!(value);
2218    noop!();
2219    const { doubled(2) == 4 }
2220}
2221"#;
2222        let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
2223        // The assertion is a decision of its own; the crate's own macro keeps
2224        // the macro limitation.
2225        assert!(manifest.decisions.iter().any(|decision| {
2226            decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
2227        }));
2228        let ids = manifest
2229            .limitations
2230            .iter()
2231            .filter_map(|limitation| limitation.get("id")?.as_str())
2232            .collect::<BTreeSet<_>>();
2233        assert_eq!(
2234            ids,
2235            BTreeSet::from([
2236                "rust-const-context-not-instrumented",
2237                "rust-macro-expansion-not-instrumented"
2238            ])
2239        );
2240        assert!(!manifest.points.iter().any(|point| {
2241            point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
2242        }));
2243    }
2244
2245    #[test]
2246    fn transforms_points_and_nested_decisions_without_changing_behavior() {
2247        let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
2248
2249static CALLS: AtomicUsize = AtomicUsize::new(0);
2250
2251fn observed(name: &str, value: bool) -> bool {
2252    let order = CALLS.fetch_add(1, Ordering::SeqCst);
2253    println!("{order}:{name}:{value}");
2254    value
2255}
2256
2257fn classify(first: bool, second: bool, third: bool) -> i32 {
2258    if observed("a", first) && (observed("b", second) || observed("c", third)) {
2259        7
2260    } else {
2261        3
2262    }
2263}
2264
2265fn main() {
2266    let closure = |value: i32| value + 1;
2267    println!("result={}", closure(classify(true, false, true)));
2268}
2269"#;
2270        let transformed =
2271            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2272        assert!(transformed.code.contains("::condition("));
2273        assert!(transformed.code.contains("::decision("));
2274        assert!(transformed.code.contains("::hit("));
2275        let original = compile_and_run(source, "original");
2276        let instrumented = compile_and_run(
2277            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2278            "instrumented",
2279        );
2280        assert_eq!(instrumented.status, original.status);
2281        assert_eq!(instrumented.stdout, original.stdout);
2282        assert_eq!(instrumented.stderr, original.stderr);
2283    }
2284
2285    #[test]
2286    fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
2287        let source = r#"const fn enabled(value: bool) -> bool {
2288    if value { true } else { false }
2289}
2290
2291fn classify(value: Option<bool>, fallback: bool) -> bool {
2292    if let Some(inner) = value && inner && fallback { true } else { false }
2293}
2294"#;
2295        let transformed =
2296            instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2297        let ids = transformed
2298            .manifest
2299            .limitations
2300            .iter()
2301            .filter_map(|limitation| limitation.get("id")?.as_str())
2302            .collect::<BTreeSet<_>>();
2303        assert!(ids.contains("rust-const-context-not-instrumented"));
2304        assert!(!ids.contains("rust-let-chain-probes-not-injected"));
2305        // The chain: a marker at the front, ordinary conditions wrapped, the
2306        // outcome recorded in both branches; the `let` itself untouched.
2307        assert!(
2308            transformed
2309                .code
2310                .contains("::reached(&mut __supercov_decision_")
2311        );
2312        assert!(
2313            transformed
2314                .code
2315                .contains("::condition((inner), &mut __supercov_decision_")
2316        );
2317        assert!(
2318            transformed
2319                .code
2320                .contains("::decision_chain(&mut __supercov_decision_")
2321        );
2322        assert!(transformed.code.contains("&& let Some(inner) = value &&"));
2323        assert!(!transformed.code.contains("condition((let"));
2324    }
2325
2326    #[test]
2327    fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
2328        let source = r#"use std::fmt::Write as _;
2329
2330fn classify(values: &[i32], strict: bool) -> String {
2331    let mut out = String::new();
2332    assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
2333    debug_assert!(values.iter().all(|v| *v > -100));
2334    let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
2335    let repeated = vec![if strict { 1 } else { 0 }; values.len()];
2336    let small = matches!(values.first(), Some(v) if *v < 3);
2337    if matches!(values.len(), 1 | 2) && small {
2338        println!("small");
2339    }
2340    println!("{}", repeated.len() + small as usize);
2341    write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
2342    println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
2343    assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
2344    out
2345}
2346
2347fn main() {
2348    println!("{}", classify(&[1, 2], true));
2349    println!("{}", classify(&[3], false));
2350    println!("{}", classify(&[], true));
2351    let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
2352    println!("{total}");
2353}
2354"#;
2355        let transformed =
2356            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2357        // `debug_assert!(cond)` had no message: the condition's own text is
2358        // supplied so the panic message is the one the macro would build.
2359        assert!(transformed.code.contains(
2360            r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
2361        ));
2362        // `assert!(cond, "msg", args)` keeps its message untouched.
2363        assert!(
2364            transformed
2365                .code
2366                .contains(r#"), "bad input {:?}", values);"#)
2367        );
2368        // The assertion's condition is a two-condition decision, the `if`
2369        // inside `vec!` and `println!` are decisions, the logical operators
2370        // inside the macros are branches, and no macro is left declared.
2371        let assertion = transformed
2372            .manifest
2373            .decisions
2374            .iter()
2375            .find(|decision| decision.line == 5)
2376            .expect("assert! decision");
2377        assert_eq!(
2378            assertion.conditions,
2379            ["values.len() < 10", "strict", "!values.is_empty()"]
2380        );
2381        assert!(
2382            transformed
2383                .manifest
2384                .decisions
2385                .iter()
2386                .any(|decision| decision.line == 7)
2387        );
2388        assert!(
2389            transformed
2390                .manifest
2391                .decisions
2392                .iter()
2393                .any(|decision| decision.line == 9)
2394        );
2395        assert!(
2396            transformed
2397                .code
2398                .contains("assert!(({ let mut __supercov_decision_")
2399        );
2400        assert!(
2401            transformed
2402                .code
2403                .contains(", if ({ let mut __supercov_decision_")
2404        );
2405        // `vec![x; n]`: the element takes probes as an array element would.
2406        assert!(
2407            transformed
2408                .code
2409                .contains("vec![if ({ let mut __supercov_decision_")
2410        );
2411        // A standalone `matches!` is a decision; one that is already an `if`
2412        // condition's atom is not doubled.
2413        assert!(
2414            transformed
2415                .code
2416                .contains("let small = ({ let mut __supercov_decision_")
2417        );
2418        assert_eq!(
2419            transformed
2420                .manifest
2421                .decisions
2422                .iter()
2423                .filter(|decision| decision.line == 10)
2424                .count(),
2425            1
2426        );
2427        assert_eq!(
2428            transformed
2429                .manifest
2430                .decisions
2431                .iter()
2432                .filter(|decision| decision.line == 9)
2433                .count(),
2434            1
2435        );
2436        assert!(
2437            transformed
2438                .code
2439                .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
2440        );
2441        assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2442            limitation.get("id").and_then(|id| id.as_str())
2443                == Some("rust-macro-expansion-not-instrumented")
2444        }));
2445        let original = compile_and_run(source, "original-macros");
2446        let instrumented = compile_and_run(
2447            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2448            "instrumented-macros",
2449        );
2450        assert_eq!(instrumented.status, original.status);
2451        assert_eq!(instrumented.stdout, original.stdout);
2452        // `dbg!` prints its own file:line:column, which the probes move;
2453        // compare what follows the location.
2454        let after_location = |stderr: &[u8]| {
2455            String::from_utf8_lossy(stderr)
2456                .lines()
2457                .map(|line| {
2458                    line.split_once("] ")
2459                        .map_or(line, |(_, rest)| rest)
2460                        .to_owned()
2461                })
2462                .collect::<Vec<_>>()
2463        };
2464        assert_eq!(
2465            after_location(&instrumented.stderr),
2466            after_location(&original.stderr)
2467        );
2468    }
2469
2470    #[test]
2471    fn assertion_panic_messages_survive_instrumentation() {
2472        // smallvec's `#[should_panic(expected = "new_capacity >= len")]` reads
2473        // the message `assert!` builds from its condition's text.
2474        let source = r#"fn grow(len: usize, new_capacity: usize) {
2475    assert!(new_capacity >= len);
2476}
2477
2478fn check(value: i32) {
2479    assert!(value > 0 && value < 10, "value {value} out of range");
2480}
2481
2482// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
2483fn all_seen(seen: &[bool]) {
2484    for was_seen in seen {
2485        assert!(was_seen);
2486        debug_assert!(was_seen, "seen");
2487    }
2488}
2489
2490fn main() {
2491    std::panic::set_hook(Box::new(|_| {}));
2492    all_seen(&[true, true]);
2493    match std::panic::catch_unwind(|| all_seen(&[true, false])) {
2494        Ok(()) => println!("ok"),
2495        Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2496    }
2497    for (len, capacity) in [(3, 5), (8, 5)] {
2498        match std::panic::catch_unwind(|| grow(len, capacity)) {
2499            Ok(()) => println!("ok"),
2500            Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2501        }
2502    }
2503    match std::panic::catch_unwind(|| check(12)) {
2504        Ok(()) => println!("ok"),
2505        Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
2506    }
2507}
2508"#;
2509        let transformed =
2510            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2511        let original = compile_and_run(source, "original-assert-message");
2512        let instrumented = compile_and_run(
2513            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2514            "instrumented-assert-message",
2515        );
2516        assert_eq!(instrumented.status, original.status);
2517        assert_eq!(instrumented.stdout, original.stdout);
2518        assert!(
2519            String::from_utf8_lossy(&instrumented.stdout)
2520                .contains("assertion failed: new_capacity >= len")
2521        );
2522        assert!(
2523            String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
2524        );
2525    }
2526
2527    #[test]
2528    fn files_that_predate_a_reserved_word_still_instrument() {
2529        // itertools' tests call `rng.gen()`; `gen` is a keyword in 2024 only.
2530        let source = r#"struct Rng(u64);
2531impl Rng {
2532    fn gen(&mut self) -> u64 {
2533        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
2534        self.0 >> 33
2535    }
2536}
2537
2538fn main() {
2539    let mut rng = Rng(7);
2540    let mut odd = 0;
2541    for _ in 0..10 {
2542        if rng.gen() % 2 == 1 {
2543            odd += 1;
2544        }
2545    }
2546    println!("{odd}");
2547}
2548"#;
2549        let transformed =
2550            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2551        assert!(
2552            transformed
2553                .manifest
2554                .decisions
2555                .iter()
2556                .any(|decision| decision.line == 13)
2557        );
2558        let original = compile_and_run_edition(source, "original-gen", "2021");
2559        let instrumented = compile_and_run_edition(
2560            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2561            "instrumented-gen",
2562            "2021",
2563        );
2564        assert_eq!(instrumented.status, original.status);
2565        assert_eq!(instrumented.stdout, original.stdout);
2566    }
2567
2568    #[test]
2569    fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
2570        // bytes and memchr are edition 2018/2021: a plain `if let` there must
2571        // not become a let chain. `while let` needs its `break`s told apart
2572        // from the condition failing, including labeled ones from inner loops.
2573        let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
2574    let mut it = values.iter();
2575    'scan: while let Some(value) = it.next() {
2576        if *value < 0 {
2577            break;
2578        }
2579        for _ in 0..1 {
2580            if *value == 99 {
2581                break 'scan;
2582            }
2583            if *value == 98 {
2584                break;
2585            }
2586        }
2587        if *value % 2 == 0 {
2588            return Some(*value);
2589        }
2590    }
2591    None
2592}
2593
2594fn describe(value: Option<i32>) -> &'static str {
2595    if let Some(inner) = value {
2596        if inner > 0 { "positive" } else { "non-positive" }
2597    } else if let None = value {
2598        "none"
2599    } else {
2600        "unreachable"
2601    }
2602}
2603
2604fn count(values: &[Option<i32>]) -> usize {
2605    let mut total = 0;
2606    for value in values {
2607        if let Some(_) = value {
2608            total += 1;
2609        }
2610    }
2611    total
2612}
2613
2614fn main() {
2615    println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
2616    println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
2617    println!("{}", count(&[Some(1), None, Some(3)]));
2618}
2619"#;
2620        let transformed =
2621            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2622        assert!(!transformed.code.contains("&& let"));
2623        assert!(transformed.code.contains("__supercov_broke_"));
2624        assert_eq!(transformed.code.matches("= true; break").count(), 2);
2625        for edition in ["2021", "2024"] {
2626            let original =
2627                compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
2628            let instrumented = compile_and_run_edition(
2629                &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2630                &format!("instrumented-lone-let-{edition}"),
2631                edition,
2632            );
2633            assert_eq!(instrumented.status, original.status);
2634            assert_eq!(instrumented.stdout, original.stdout);
2635            assert_eq!(instrumented.stderr, original.stderr);
2636        }
2637    }
2638
2639    #[test]
2640    fn let_chains_keep_their_behavior() {
2641        let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
2642    if let Some(inner) = value && inner > 0 && flag {
2643        "positive"
2644    } else if let Some(inner) = value && (inner < 0 || flag) {
2645        "negative-or-flagged"
2646    } else {
2647        "other"
2648    }
2649}
2650
2651fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
2652    let mut total = 0;
2653    let mut it = values.iter();
2654    while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
2655        total += inner * second;
2656        if total > 100 {
2657            break;
2658        }
2659    }
2660    total
2661}
2662
2663fn tail(value: Option<&str>) -> usize {
2664    let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
2665    if let Some(text) = value && text.starts_with('x') {
2666        println!("x-prefixed");
2667    }
2668    pick(value)
2669}
2670
2671fn main() {
2672    for value in [Some(3), Some(-3), Some(0), None] {
2673        for flag in [true, false] {
2674            println!("{value:?} {flag} {}", describe(value, flag));
2675        }
2676    }
2677    println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
2678    println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
2679    println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
2680}
2681"#;
2682        let transformed =
2683            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2684        assert_eq!(
2685            transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
2686            5
2687        );
2688        assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2689            limitation.get("id").and_then(|id| id.as_str())
2690                == Some("rust-let-chain-probes-not-injected")
2691        }));
2692        let original = compile_and_run(source, "original-chains");
2693        let instrumented = compile_and_run(
2694            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2695            "instrumented-chains",
2696        );
2697        assert_eq!(instrumented.status, original.status);
2698        assert_eq!(instrumented.stdout, original.stdout);
2699        assert_eq!(instrumented.stderr, original.stderr);
2700    }
2701
2702    #[test]
2703    fn instrumented_const_and_static_initialisers_still_compile() {
2704        // Every one of these positions is const-evaluated, so none of them can
2705        // hold a call to the runtime -- `condition`, `decision` and `hit` are
2706        // not `const fn`. Found on bytes-1.12.1, whose test target has
2707        // `const ITERS: usize = if cfg!(miri) { 100 } else { 1_000 };` and
2708        // failed to build with E0015.
2709        let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
2710static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
2711
2712enum Mode {
2713    Narrow = if cfg!(unix) { 1 } else { 2 },
2714}
2715
2716struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
2717
2718impl Buffer {
2719    const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
2720}
2721
2722fn scaled(flag: bool) -> usize {
2723    const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
2724    if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
2725}
2726
2727fn main() {
2728    let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
2729    println!(
2730        "{} {} {} {}",
2731        scaled(true),
2732        scaled(false),
2733        Mode::Narrow as usize,
2734        buffer.0.len()
2735    );
2736}
2737"#;
2738        let transformed =
2739            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2740        // The runtime `if` in `scaled` is still instrumented -- declining a
2741        // const initialiser must not decline the whole file.
2742        assert!(transformed.code.contains("::decision("));
2743        let ids = transformed
2744            .manifest
2745            .limitations
2746            .iter()
2747            .filter_map(|limitation| limitation.get("id")?.as_str())
2748            .collect::<BTreeSet<_>>();
2749        assert!(ids.contains("rust-const-context-not-instrumented"));
2750
2751        let original = compile_and_run(source, "const-original");
2752        let instrumented = compile_and_run(
2753            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2754            "const-instrumented",
2755        );
2756        assert_eq!(instrumented.status, original.status);
2757        assert_eq!(instrumented.stdout, original.stdout);
2758        assert_eq!(instrumented.stderr, original.stderr);
2759    }
2760
2761    #[test]
2762    fn a_probed_global_allocator_would_recurse_into_itself() {
2763        // The runtime allocates, so a probe inside `alloc` re-enters `alloc` and
2764        // recurses until the stack is gone. bytes-1.12.1's
2765        // tests/test_bytes_odd_alloc.rs installs one of these, and the
2766        // instrumented binary died with SIGSEGV before libtest could list a
2767        // single test, while the uninstrumented binary listed them fine.
2768        let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
2769
2770struct Odd;
2771
2772unsafe impl GlobalAlloc for Odd {
2773    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
2774        if layout.align() == 1 && layout.size() > 0 {
2775            System.alloc(layout)
2776        } else {
2777            System.alloc(layout)
2778        }
2779    }
2780
2781    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
2782        System.dealloc(pointer, layout);
2783    }
2784}
2785
2786#[global_allocator]
2787static ODD: Odd = Odd;
2788
2789fn classify(flag: bool) -> usize {
2790    if flag { 1 } else { 2 }
2791}
2792
2793fn main() {
2794    let held = std::vec![7u8; 32];
2795    println!("{} {}", classify(!held.is_empty()), held.len());
2796}
2797"#;
2798        let transformed =
2799            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2800        // Nothing inside the allocator may carry a probe...
2801        let allocator = transformed
2802            .code
2803            .split("unsafe impl GlobalAlloc for Odd")
2804            .nth(1)
2805            .and_then(|rest| rest.split("#[global_allocator]").next())
2806            .expect("the instrumented source still contains the allocator impl");
2807        assert!(
2808            !allocator.contains("__supercov_runtime_v1"),
2809            "probe injected into a GlobalAlloc impl:\n{allocator}"
2810        );
2811        // ...while `classify`, right next to it, is still measured.
2812        assert!(transformed.code.contains("::decision("));
2813        let ids = transformed
2814            .manifest
2815            .limitations
2816            .iter()
2817            .filter_map(|limitation| limitation.get("id")?.as_str())
2818            .collect::<BTreeSet<_>>();
2819        assert!(ids.contains("rust-global-allocator-not-instrumented"));
2820
2821        let original = compile_and_run(source, "alloc-original");
2822        let instrumented = compile_and_run(
2823            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2824            "alloc-instrumented",
2825        );
2826        assert_eq!(instrumented.status, original.status);
2827        assert_eq!(instrumented.stdout, original.stdout);
2828        assert_eq!(instrumented.stderr, original.stderr);
2829    }
2830
2831    #[test]
2832    fn match_arms_record_selection_without_changing_behavior() {
2833        let source = r#"#[derive(Debug)]
2834enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
2835
2836fn area(shape: &Shape) -> i32 {
2837    match shape {
2838        Shape::Dot => 0,
2839        Shape::Line(length) if *length < 0 => -length,
2840        Shape::Line(length) => *length,
2841        Shape::Box { w, h } => {
2842            let area = w * h;
2843            area
2844        }
2845    }
2846}
2847
2848fn describe(value: i32) -> &'static str {
2849    let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
2850    match value {
2851        0 => inner(value),
2852        n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
2853        n => match n % 2 {
2854            0 => "even",
2855            _ => inner(n),
2856        },
2857    }
2858}
2859
2860fn main() {
2861    for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
2862        println!("{shape:?}={}", area(&shape));
2863    }
2864    for value in [0, 1, 3, 8] {
2865        println!("{value}:{}", describe(value));
2866    }
2867}
2868"#;
2869        let transformed =
2870            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2871        assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
2872        assert_eq!(
2873            transformed.code.matches("const __SUPERCOV_ARMS_").count(),
2874            4
2875        );
2876        let arms = transformed
2877            .manifest
2878            .branches
2879            .iter()
2880            .filter(|branch| branch.kind == "match-arm")
2881            .count();
2882        assert_eq!(arms, 4 + 3 + 3 + 2);
2883        // Every arm's alternatives appear in a table, and the source keeps
2884        // its meaning.
2885        for branch in transformed
2886            .manifest
2887            .branches
2888            .iter()
2889            .filter(|branch| branch.kind == "match-arm")
2890        {
2891            for alternative in &branch.alternatives {
2892                assert!(
2893                    transformed.code.contains(&format!("{:?}", alternative.id)),
2894                    "{} is not in any table",
2895                    alternative.id
2896                );
2897            }
2898        }
2899        let original = compile_and_run(source, "original-arms");
2900        let instrumented = compile_and_run(
2901            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2902            "instrumented-arms",
2903        );
2904        assert_eq!(instrumented.status, original.status);
2905        assert_eq!(instrumented.stdout, original.stdout);
2906        assert_eq!(instrumented.stderr, original.stderr);
2907    }
2908
2909    #[test]
2910    fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
2911        let source = r#"use std::ops::ControlFlow;
2912
2913fn total(values: &[i32]) -> i32 {
2914    let mut sum = 0;
2915    for value in values {
2916        sum += value;
2917    }
2918    'outer: for row in 0..3 {
2919        for column in 0..3 {
2920            if column > row {
2921                continue 'outer;
2922            }
2923            sum += row * column;
2924        }
2925    }
2926    sum
2927}
2928
2929fn first_even(values: &[i32]) -> Option<i32> {
2930    let mut index = 0;
2931    'scan: while index < values.len() {
2932        if values[index] % 2 == 0 {
2933            break 'scan;
2934        }
2935        index += 1;
2936    }
2937    let mut it = values.iter().skip(index);
2938    while let Some(value) = it.next() {
2939        return Some(*value);
2940    }
2941    None
2942}
2943
2944fn parse_twice(text: &str) -> Result<i32, String> {
2945    let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
2946    let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
2947    Ok(doubled)
2948}
2949
2950fn halve(value: i32) -> Option<i32> {
2951    let even = (value % 2 == 0).then_some(value)?;
2952    Some(even / 2)
2953}
2954
2955fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
2956    let mut sum = 0;
2957    for value in values {
2958        let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
2959        sum += step?;
2960    }
2961    ControlFlow::Continue(sum)
2962}
2963
2964fn gate(a: bool, b: bool, c: bool) -> bool {
2965    let both = a && b;
2966    let either = a || b || c;
2967    both || (either && !c) || (c && a && (b || !b))
2968}
2969
2970fn main() {
2971    println!("{} {}", total(&[]), total(&[1, 2, 3]));
2972    println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
2973    println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
2974    println!("{:?} {:?}", halve(8), halve(7));
2975    println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
2976    for a in [false, true] {
2977        for b in [false, true] {
2978            for c in [false, true] {
2979                print!("{}", gate(a, b, c) as u8);
2980            }
2981        }
2982    }
2983    println!();
2984}
2985"#;
2986        let transformed =
2987            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2988        for marker in [
2989            "::logical((",
2990            "::for_loop((",
2991            "::entered(&mut __supercov_loop_",
2992            "::zero_iterations(__supercov_loop_",
2993            "::TryProbe::probe((",
2994        ] {
2995            assert!(transformed.code.contains(marker), "{marker} missing");
2996        }
2997        let kinds = |kind: &str| {
2998            transformed
2999                .manifest
3000                .branches
3001                .iter()
3002                .filter(|branch| branch.kind == kind)
3003                .count()
3004        };
3005        assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3006        assert_eq!(kinds("while-loop"), 2);
3007        assert_eq!(kinds("try-operator"), 4);
3008        assert_eq!(kinds("logical-and"), 4);
3009        assert_eq!(kinds("logical-or"), 5);
3010        assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3011            limitation.get("id").and_then(|id| id.as_str())
3012                == Some("rust-structural-branch-probes-not-yet-injected")
3013        }));
3014        let original = compile_and_run(source, "original-structural");
3015        let instrumented = compile_and_run(
3016            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3017            "instrumented-structural",
3018        );
3019        assert_eq!(instrumented.status, original.status);
3020        assert_eq!(instrumented.stdout, original.stdout);
3021        assert_eq!(instrumented.stderr, original.stderr);
3022    }
3023
3024    #[test]
3025    fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3026        // memchr's is_available returns bool from one of two cfg-gated blocks.
3027        // A probe PREPENDED to the second block sits between the siblings,
3028        // survives the cfg strip, and becomes the new `()` tail -- 32 E0308s
3029        // across the crate. Attributed blocks take the probe inside their
3030        // braces instead, where the same cfg governs both.
3031        let source = r#"pub fn is_available() -> bool {
3032    #[cfg(target_endian = "little")]
3033    {
3034        true
3035    }
3036    #[cfg(not(target_endian = "little"))]
3037    {
3038        false
3039    }
3040}
3041
3042fn main() {
3043    println!("{}", is_available());
3044}
3045"#;
3046        let transformed =
3047            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3048        let original = compile_and_run(source, "cfg-original");
3049        let instrumented = compile_and_run(
3050            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3051            "cfg-instrumented",
3052        );
3053        assert_eq!(instrumented.status, original.status);
3054        assert_eq!(instrumented.stdout, original.stdout);
3055        // The kept block is still probed -- inside its braces.
3056        assert!(
3057            transformed
3058                .code
3059                .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
3060                || transformed
3061                    .code
3062                    .contains("{\ncrate::__supercov_runtime_v1::hit(")
3063        );
3064
3065        // An attributed `let` takes the probe inside its initializer, an
3066        // attributed expression statement inside a block; a `let` without an
3067        // initializer is the one shape left declared.
3068        let attributed_let = r#"fn main() {
3069    #[cfg(target_endian = "little")]
3070    let value = 1;
3071    #[cfg(not(target_endian = "little"))]
3072    let value = 2;
3073    #[cfg(target_endian = "little")]
3074    let borrowed: &String = &String::from("little");
3075    #[cfg(not(target_endian = "little"))]
3076    let borrowed: &String = &String::from("big");
3077    #[cfg(target_endian = "little")]
3078    print!("le ");
3079    #[cfg(not(target_endian = "little"))]
3080    print!("be ");
3081    #[allow(unused_assignments)]
3082    let mut later;
3083    later = value + 1;
3084    println!("{value} {borrowed} {later}");
3085}
3086"#;
3087        let transformed = instrument_rust_source(
3088            "src/main.rs",
3089            attributed_let,
3090            "crate::__supercov_runtime_v1",
3091        )
3092        .unwrap();
3093        let ids = transformed
3094            .manifest
3095            .limitations
3096            .iter()
3097            .filter_map(|limitation| limitation.get("id")?.as_str())
3098            .collect::<BTreeSet<_>>();
3099        // Declared only for `let mut later;`.
3100        assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3101        assert!(
3102            transformed
3103                .code
3104                .contains("let value =  { crate::__supercov_runtime_v1::hit(")
3105        );
3106        assert!(
3107            transformed
3108                .code
3109                .contains("let borrowed: &String =  { crate::__supercov_runtime_v1::hit(")
3110        );
3111        assert!(
3112            transformed
3113                .code
3114                .contains("] { crate::__supercov_runtime_v1::hit(")
3115        );
3116        let original = compile_and_run(attributed_let, "cfg-let-original");
3117        let instrumented = compile_and_run(
3118            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3119            "cfg-let-instrumented",
3120        );
3121        assert_eq!(instrumented.status, original.status);
3122        assert_eq!(instrumented.stdout, original.stdout);
3123        assert_eq!(instrumented.stderr, original.stderr);
3124
3125        // A brace macro closing a block is a statement to rustc, so its
3126        // attributes are legal where an attributed tail expression's are not
3127        // (tokio: `#[rustfmt::skip] tokio::select! { .. }`). The probe goes
3128        // before the attributes; a `cfg` there stays declared.
3129        let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3130fn value() -> i32 {
3131    let base = 20;
3132    #[rustfmt::skip]
3133    pick! { base + 1 }
3134}
3135fn effect() {
3136    #[rustfmt::skip]
3137    println! { "effect" }
3138}
3139fn gated() {
3140    #[cfg(target_endian = "little")]
3141    println! { "little" }
3142}
3143fn main() {
3144    effect();
3145    gated();
3146    println!("{}", value());
3147}
3148"#;
3149        let transformed = instrument_rust_source(
3150            "src/main.rs",
3151            trailing_macro,
3152            "crate::__supercov_runtime_v1",
3153        )
3154        .unwrap();
3155        assert!(
3156            transformed
3157                .code
3158                .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3159        );
3160        assert!(
3161            transformed
3162                .code
3163                .contains("); #[rustfmt::skip]\n    pick! { base + 1 } }")
3164        );
3165        assert!(
3166            transformed
3167                .code
3168                .contains("); #[rustfmt::skip]\n    println! { \"effect\" } }")
3169        );
3170        assert!(
3171            transformed.code.contains(
3172                "\n    #[cfg(target_endian = \"little\")]\n    println! { \"little\" }\n"
3173            )
3174        );
3175        let ids = transformed
3176            .manifest
3177            .limitations
3178            .iter()
3179            .filter_map(|limitation| limitation.get("id")?.as_str())
3180            .collect::<BTreeSet<_>>();
3181        assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3182        let original = compile_and_run(trailing_macro, "trailing-macro-original");
3183        let instrumented = compile_and_run(
3184            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3185            "trailing-macro-instrumented",
3186        );
3187        assert_eq!(
3188            instrumented.status,
3189            original.status,
3190            "{}",
3191            String::from_utf8_lossy(&instrumented.stderr)
3192        );
3193        assert_eq!(instrumented.stdout, original.stdout);
3194        assert_eq!(instrumented.stderr, original.stderr);
3195    }
3196
3197    #[test]
3198    fn rejects_non_crate_local_runtime_paths() {
3199        assert_eq!(
3200            instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
3201            Err(RustInstrumenterError::InvalidRuntimePath)
3202        );
3203    }
3204
3205    #[test]
3206    fn rejects_invalid_rust_without_partial_obligations() {
3207        assert!(matches!(
3208            build_rust_manifest("src/lib.rs", "fn broken( {\n"),
3209            Err(RustInstrumenterError::Parse(_))
3210        ));
3211    }
3212}