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