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