Skip to main content

supercov_engine/
rust_instrumenter.rs

1//! Supercov-owned Rust parsing and obligation discovery.
2//!
3//! The first private frontend deliberately starts from a lossless concrete
4//! syntax tree. LLVM/rustc coverage remains a development oracle; it is not a
5//! product input. Probe insertion and Cargo execution build on this exact
6//! source denominator.
7
8use std::collections::BTreeSet;
9
10use ra_ap_syntax::{
11    AstNode, Edition, SourceFile, SyntaxKind, TextRange,
12    ast::{self, BinaryOp, HasAttrs, HasLoopBody, HasName, LogicOp},
13};
14use serde_json::json;
15use sha2::{Digest, Sha256};
16
17use crate::{
18    coverage_analysis::PointKind,
19    coverage_report::{
20        BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
21    },
22};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum RustInstrumenterError {
26    SourceTooLarge,
27    Parse(Vec<String>),
28    InvalidRange,
29    InvalidRuntimePath,
30}
31
32impl std::fmt::Display for RustInstrumenterError {
33    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::SourceTooLarge => write!(formatter, "Rust source exceeds the parser range"),
36            Self::Parse(errors) => write!(formatter, "Rust parse failed: {}", errors.join("; ")),
37            Self::InvalidRange => write!(formatter, "Rust parser returned an invalid range"),
38            Self::InvalidRuntimePath => write!(formatter, "invalid generated Rust runtime path"),
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct RustInstrumentedSource {
45    pub code: String,
46    pub manifest: CoverageManifest,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum InsertionKind {
51    End,
52    Direct,
53    Start,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57struct Insertion {
58    offset: usize,
59    kind: InsertionKind,
60    scope_len: usize,
61    rank: usize,
62    text: String,
63}
64
65fn valid_runtime_path(path: &str) -> bool {
66    let mut parts = path.split("::");
67    if !matches!(parts.next(), Some("crate")) {
68        return false;
69    }
70    let parts = parts.collect::<Vec<_>>();
71    !parts.is_empty()
72        && parts.into_iter().all(|part| {
73            !part.is_empty()
74                && part.bytes().enumerate().all(|(index, byte)| {
75                    byte == b'_'
76                        || byte.is_ascii_alphabetic()
77                        || (index > 0 && byte.is_ascii_digit())
78                })
79        })
80}
81
82/// Report whether rustc will evaluate this node at compile time.
83///
84/// Runtime probes cannot appear anywhere this is true: `condition`, `decision`
85/// and `hit` are not `const fn`, so emitting a call here is not a bad
86/// measurement but a build failure (E0015). bytes-1.12.1 hit exactly that with
87/// `const ITERS: usize = if cfg!(miri) { 100 } else { 1_000 };`.
88///
89/// `ConstArg` is the shared node for enum discriminants, array lengths, const
90/// generic arguments and const parameter defaults, so matching it covers all
91/// four. The remaining case is an array repeat expression, `[value; count]`,
92/// where only the count after the semicolon is const-evaluated.
93/// Report whether the source's doc comments contain a fenced code block.
94///
95/// rustdoc turns fenced blocks in `///`, `//!` and `#[doc]` text into doctest
96/// crates. The scan is line-based and deliberately coarse: a fence inside a
97/// doc comment declares the limitation even when the fence is `ignore`d, which
98/// over-declares the unmeasured surface rather than ever under-declaring it.
99fn in_const_context(node: &ra_ap_syntax::SyntaxNode) -> bool {
100    let start = node.text_range().start();
101    node.ancestors().any(|ancestor| {
102        ast::Fn::cast(ancestor.clone()).is_some_and(|function| function.const_token().is_some())
103            || ast::BlockExpr::cast(ancestor.clone())
104                .is_some_and(|block| block.const_token().is_some())
105            || ast::Const::can_cast(ancestor.kind())
106            || ast::Static::can_cast(ancestor.kind())
107            || ast::ConstArg::can_cast(ancestor.kind())
108            || ast::ArrayExpr::cast(ancestor).is_some_and(|array| {
109                array
110                    .semicolon_token()
111                    .is_some_and(|semicolon| start >= semicolon.text_range().end())
112            })
113    })
114}
115
116/// Report whether this node sits inside a `GlobalAlloc` implementation.
117///
118/// The probe runtime allocates, so a probe inside `alloc` calls back into
119/// `alloc`, which probes again, until the stack is gone. bytes-1.12.1's
120/// tests/test_bytes_odd_alloc.rs installs a `#[global_allocator]`, and the
121/// instrumented binary died with SIGSEGV before libtest could even list its
122/// tests -- while the uninstrumented one listed them fine.
123///
124/// The general rule this enforces is that nothing the runtime itself calls can
125/// carry a probe, and `#[global_allocator]` is the one way a user crate gets
126/// onto that path. A `GlobalAlloc` impl is skipped whether or not it is the
127/// registered allocator, because the registering `static` may live in another
128/// file: declining a handful of allocator bodies costs almost no exactness,
129/// while instrumenting the live one costs the whole run.
130fn in_global_allocator(node: &ra_ap_syntax::SyntaxNode) -> bool {
131    node.ancestors().any(|ancestor| {
132        ast::Impl::cast(ancestor).is_some_and(|block| {
133            block.trait_().is_some_and(|implemented| {
134                implemented
135                    .syntax()
136                    .descendants_with_tokens()
137                    .filter_map(|element| element.into_token())
138                    .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
139            })
140        })
141    })
142}
143
144/// Report whether a probe placed at this node could not run correctly.
145fn cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
146    in_const_context(node) || in_global_allocator(node)
147}
148
149fn range_offsets(range: TextRange) -> (usize, usize) {
150    (usize::from(range.start()), usize::from(range.end()))
151}
152
153fn push_wrapper(
154    insertions: &mut Vec<Insertion>,
155    range: TextRange,
156    scope: TextRange,
157    rank: usize,
158    prefix: String,
159    suffix: String,
160) {
161    let (start, end) = range_offsets(range);
162    let (scope_start, scope_end) = range_offsets(scope);
163    let scope_len = scope_end - scope_start;
164    insertions.push(Insertion {
165        offset: start,
166        kind: InsertionKind::Start,
167        scope_len,
168        rank,
169        text: prefix,
170    });
171    insertions.push(Insertion {
172        offset: end,
173        kind: InsertionKind::End,
174        scope_len,
175        rank,
176        text: suffix,
177    });
178}
179
180fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
181    insertions.push(Insertion {
182        offset,
183        kind: InsertionKind::Direct,
184        scope_len: 0,
185        rank: 0,
186        text,
187    });
188}
189
190fn apply_insertions(
191    source: &str,
192    mut insertions: Vec<Insertion>,
193) -> Result<String, RustInstrumenterError> {
194    if insertions
195        .iter()
196        .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
197    {
198        return Err(RustInstrumenterError::InvalidRange);
199    }
200    insertions.sort_by(|left, right| {
201        left.offset.cmp(&right.offset).then_with(|| {
202            let kind_order = |kind: InsertionKind| match kind {
203                InsertionKind::End => 0,
204                InsertionKind::Direct => 1,
205                InsertionKind::Start => 2,
206            };
207            kind_order(left.kind)
208                .cmp(&kind_order(right.kind))
209                .then_with(|| match left.kind {
210                    InsertionKind::End => left
211                        .scope_len
212                        .cmp(&right.scope_len)
213                        .then_with(|| right.rank.cmp(&left.rank)),
214                    InsertionKind::Direct => std::cmp::Ordering::Equal,
215                    InsertionKind::Start => right
216                        .scope_len
217                        .cmp(&left.scope_len)
218                        .then_with(|| left.rank.cmp(&right.rank)),
219                })
220        })
221    });
222
223    let mut output = source.to_owned();
224    let mut index = insertions.len();
225    while index > 0 {
226        let offset = insertions[index - 1].offset;
227        let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
228        let text = insertions[start..index]
229            .iter()
230            .map(|insertion| insertion.text.as_str())
231            .collect::<String>();
232        output.insert_str(offset, &text);
233        index = start;
234    }
235    Ok(output)
236}
237
238fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
239    if manifest
240        .limitations
241        .iter()
242        .any(|limitation| limitation.get("id").and_then(|value| value.as_str()) == Some(id))
243    {
244        return;
245    }
246    manifest.limitations.push(json!({
247        "id": id,
248        "kind": "rust-frontend-readiness",
249        "file": file,
250        "line": 1,
251        "column": 0,
252        "source": "",
253        "reason": reason
254    }));
255}
256
257fn allocate_frame_name(
258    file: &str,
259    condition: &ast::Expr,
260    kind: &str,
261    identifiers: &mut BTreeSet<String>,
262) -> String {
263    let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
264    let suffix = id.rsplit(':').next().unwrap_or("decision");
265    let base = format!("__supercov_decision_{suffix}");
266    let mut candidate = base.clone();
267    let mut attempt = 0_usize;
268    while !identifiers.insert(candidate.clone()) {
269        attempt += 1;
270        candidate = format!("{base}_{attempt}");
271    }
272    candidate
273}
274
275/// The `const` naming a match's alternative IDs. Upper case, so it raises no
276/// naming lint in a crate that denies warnings.
277fn allocate_table_name(
278    file: &str,
279    expression: &ast::MatchExpr,
280    identifiers: &mut BTreeSet<String>,
281) -> String {
282    let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
283    let suffix = id
284        .rsplit(':')
285        .next()
286        .unwrap_or("match")
287        .to_ascii_uppercase();
288    let base = format!("__SUPERCOV_ARMS_{suffix}");
289    let mut candidate = base.clone();
290    let mut attempt = 0_usize;
291    while !identifiers.insert(candidate.clone()) {
292        attempt += 1;
293        candidate = format!("{base}_{attempt}");
294    }
295    candidate
296}
297
298/// The local that remembers whether a `while` loop has run its body.
299fn allocate_flag_name(
300    file: &str,
301    expression: &ast::WhileExpr,
302    identifiers: &mut BTreeSet<String>,
303) -> String {
304    let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
305    let suffix = id.rsplit(':').next().unwrap_or("loop");
306    let base = format!("__supercov_loop_{suffix}");
307    let mut candidate = base.clone();
308    let mut attempt = 0_usize;
309    while !identifiers.insert(candidate.clone()) {
310        attempt += 1;
311        candidate = format!("{base}_{attempt}");
312    }
313    candidate
314}
315
316impl std::error::Error for RustInstrumenterError {}
317
318struct SourceLocations<'a> {
319    source: &'a str,
320    line_starts: Vec<usize>,
321}
322
323impl<'a> SourceLocations<'a> {
324    fn new(source: &'a str) -> Self {
325        let mut line_starts = vec![0];
326        line_starts.extend(
327            source
328                .bytes()
329                .enumerate()
330                .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
331        );
332        Self {
333            source,
334            line_starts,
335        }
336    }
337
338    fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
339        let start = usize::from(range.start());
340        let end = usize::from(range.end());
341        if start > end
342            || end > self.source.len()
343            || !self.source.is_char_boundary(start)
344            || !self.source.is_char_boundary(end)
345        {
346            return Err(RustInstrumenterError::InvalidRange);
347        }
348        Ok((start, end))
349    }
350
351    fn line_column(&self, offset: usize) -> (usize, usize) {
352        let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
353        (line_index + 1, offset - self.line_starts[line_index])
354    }
355
356    fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
357        let (start, end) = self.range(range)?;
358        Ok(self.source[start..end].trim().to_owned())
359    }
360}
361
362fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
363    let mut hash = Sha256::new();
364    let start = usize::from(range.start()).to_string();
365    let end = usize::from(range.end()).to_string();
366    for value in [file, kind, &start, &end, suffix] {
367        hash.update(value.as_bytes());
368        hash.update([0]);
369    }
370    let digest = hash.finalize();
371    let mut encoded = String::with_capacity(24);
372    for byte in &digest[..12] {
373        use std::fmt::Write as _;
374        write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
375    }
376    format!("rs:{kind}:{encoded}")
377}
378
379struct RustObligationCollector<'a> {
380    file: &'a str,
381    locations: SourceLocations<'a>,
382    manifest: CoverageManifest,
383    point_ids: BTreeSet<String>,
384    decision_ids: BTreeSet<String>,
385    branch_ids: BTreeSet<String>,
386    limitation_ids: BTreeSet<&'static str>,
387    error: Option<RustInstrumenterError>,
388}
389
390impl<'a> RustObligationCollector<'a> {
391    fn new(file: &'a str, source: &'a str) -> Self {
392        Self {
393            file,
394            locations: SourceLocations::new(source),
395            manifest: CoverageManifest {
396                unmeasured: Vec::new(),
397                decisions: Vec::new(),
398                points: Vec::new(),
399                branches: Vec::new(),
400                limitations: Vec::new(),
401                scope: None,
402            },
403            point_ids: BTreeSet::new(),
404            decision_ids: BTreeSet::new(),
405            branch_ids: BTreeSet::new(),
406            limitation_ids: BTreeSet::new(),
407            error: None,
408        }
409    }
410
411    fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
412        let result = self.locations.range(range).map(|(start, _)| {
413            let (line, column) = self.locations.line_column(start);
414            (line, column, self.locations.text(range))
415        });
416        match result {
417            Ok((line, column, Ok(source))) => Some((line, column, source)),
418            Ok((_, _, Err(error))) | Err(error) => {
419                self.error.get_or_insert(error);
420                None
421            }
422        }
423    }
424
425    fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
426        let kind_name = match kind {
427            PointKind::Statement => "statement",
428            PointKind::Function => "function",
429        };
430        let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
431        if !self.point_ids.insert(id.clone()) {
432            return;
433        }
434        let Some((line, column, source)) = self.location_source(range) else {
435            return;
436        };
437        self.manifest.points.push(PointMeta {
438            id,
439            kind,
440            file: self.file.into(),
441            line,
442            column,
443            source,
444            label,
445        });
446    }
447
448    fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
449        match expression {
450            ast::Expr::ParenExpr(paren) => {
451                if let Some(inner) = paren.expr() {
452                    Self::atomic_condition_ranges(&inner, ranges);
453                } else {
454                    ranges.push(expression.syntax().text_range());
455                }
456            }
457            ast::Expr::BinExpr(binary)
458                if matches!(
459                    binary.op_kind(),
460                    Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
461                ) =>
462            {
463                if let Some(left) = binary.lhs() {
464                    Self::atomic_condition_ranges(&left, ranges);
465                }
466                if let Some(right) = binary.rhs() {
467                    Self::atomic_condition_ranges(&right, ranges);
468                }
469            }
470            _ => ranges.push(expression.syntax().text_range()),
471        }
472    }
473
474    fn decision(&mut self, test: &ast::Expr, kind: &str) {
475        let range = test.syntax().text_range();
476        let id = stable_id(self.file, "decision", range, kind);
477        if !self.decision_ids.insert(id.clone()) {
478            return;
479        }
480        let Some((line, column, source)) = self.location_source(range) else {
481            return;
482        };
483        let mut condition_ranges = Vec::new();
484        Self::atomic_condition_ranges(test, &mut condition_ranges);
485        let mut conditions = Vec::with_capacity(condition_ranges.len());
486        for condition in condition_ranges {
487            match self.locations.text(condition) {
488                Ok(source) => conditions.push(source),
489                Err(error) => {
490                    self.error.get_or_insert(error);
491                    return;
492                }
493            }
494        }
495        self.manifest.decisions.push(DecisionMeta {
496            id: id.clone(),
497            file: self.file.into(),
498            line,
499            column,
500            source: source.clone(),
501            conditions,
502            kind: kind.into(),
503        });
504        self.branch_with_id(
505            format!("{id}:outcome"),
506            range,
507            kind,
508            source,
509            [("true", "true"), ("false", "false")],
510        );
511    }
512
513    fn branch<const N: usize>(
514        &mut self,
515        range: TextRange,
516        kind: &str,
517        alternatives: [(&str, &str); N],
518    ) {
519        let id = stable_id(self.file, "branch", range, kind);
520        let Some((_, _, source)) = self.location_source(range) else {
521            return;
522        };
523        self.branch_with_id(id, range, kind, source, alternatives);
524    }
525
526    fn branch_with_id<const N: usize>(
527        &mut self,
528        id: String,
529        range: TextRange,
530        kind: &str,
531        source: String,
532        alternatives: [(&str, &str); N],
533    ) {
534        if !self.branch_ids.insert(id.clone()) {
535            return;
536        }
537        let Some((line, column, _)) = self.location_source(range) else {
538            return;
539        };
540        self.manifest.branches.push(BranchMeta {
541            id: id.clone(),
542            kind: kind.into(),
543            file: self.file.into(),
544            line,
545            column,
546            source,
547            alternatives: alternatives
548                .into_iter()
549                .map(|(suffix, label)| BranchAlternativeMeta {
550                    id: format!("{id}:{suffix}"),
551                    label: label.into(),
552                })
553                .collect(),
554        });
555    }
556
557    fn limitation(&mut self, id: &'static str, reason: &'static str) {
558        if !self.limitation_ids.insert(id) {
559            return;
560        }
561        self.manifest.limitations.push(json!({
562            "id": id,
563            "kind": "rust-frontend-readiness",
564            "file": self.file,
565            "line": 1,
566            "column": 0,
567            "source": "",
568            "reason": reason
569        }));
570    }
571
572    fn collect(mut self, file: &SourceFile) -> Result<CoverageManifest, RustInstrumenterError> {
573        let root = file.syntax();
574
575        for list in root.descendants().filter_map(ast::StmtList::cast) {
576            for statement in list.statements() {
577                match statement {
578                    ast::Stmt::ExprStmt(statement) => {
579                        self.point(statement.syntax().text_range(), PointKind::Statement, None);
580                    }
581                    ast::Stmt::LetStmt(statement) => {
582                        self.point(statement.syntax().text_range(), PointKind::Statement, None);
583                    }
584                    ast::Stmt::Item(_) => {}
585                }
586            }
587            if let Some(tail) = list.tail_expr() {
588                self.point(tail.syntax().text_range(), PointKind::Statement, None);
589            }
590        }
591
592        for function in root.descendants().filter_map(ast::Fn::cast) {
593            if function.body().is_none() {
594                continue;
595            }
596            if function.const_token().is_some() {
597                self.limitation(
598                    "rust-const-context-not-instrumented",
599                    "Runtime probes cannot execute in const fn or compile-time evaluation",
600                );
601                continue;
602            }
603            let label = function.name().map(|name| name.text().to_string());
604            self.point(function.syntax().text_range(), PointKind::Function, label);
605        }
606
607        for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
608            self.point(
609                closure.syntax().text_range(),
610                PointKind::Function,
611                Some("<closure>".into()),
612            );
613        }
614
615        for expression in root.descendants().filter_map(ast::IfExpr::cast) {
616            if let Some(condition) = expression.condition() {
617                self.decision(&condition, "if");
618            }
619        }
620        for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
621            if let Some(condition) = expression.condition() {
622                self.decision(&condition, "while");
623            }
624            self.branch(
625                expression.syntax().text_range(),
626                "while-loop",
627                [("zero", "zero iterations"), ("entered", "entered")],
628            );
629        }
630        for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
631            if let Some(condition) = guard.condition() {
632                self.decision(&condition, "match-guard");
633            }
634        }
635
636        for binary in root.descendants().filter_map(ast::BinExpr::cast) {
637            let kind = match binary.op_kind() {
638                Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
639                Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
640                _ => continue,
641            };
642            let range = binary.rhs().map_or_else(
643                || binary.syntax().text_range(),
644                |right| right.syntax().text_range(),
645            );
646            self.branch(
647                range,
648                kind,
649                [
650                    ("short-circuit", "short-circuited"),
651                    ("evaluated", "right operand evaluated"),
652                ],
653            );
654        }
655
656        for expression in root.descendants().filter_map(ast::ForExpr::cast) {
657            self.branch(
658                expression.syntax().text_range(),
659                "for-loop",
660                [("zero", "zero iterations"), ("entered", "entered")],
661            );
662        }
663        for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
664            let Some(list) = expression.match_arm_list() else {
665                continue;
666            };
667            let arms = list.arms().collect::<Vec<_>>();
668            let last = arms.len().saturating_sub(1);
669            for (index, arm) in arms.iter().enumerate() {
670                let range = arm.syntax().text_range();
671                if index == last {
672                    // A match is exhaustive, so once every earlier arm has
673                    // been passed over the last one is selected: it can be
674                    // reached but never skipped.
675                    self.branch(range, "match-arm", [("selected", "selected")]);
676                } else {
677                    self.branch(
678                        range,
679                        "match-arm",
680                        [("missed", "not selected"), ("selected", "selected")],
681                    );
682                }
683            }
684        }
685        for expression in root.descendants().filter_map(ast::TryExpr::cast) {
686            self.branch(
687                expression.syntax().text_range(),
688                "try-operator",
689                [("continued", "continued"), ("returned", "early return")],
690            );
691        }
692
693        if root.descendants().any(|node| {
694            ast::MacroCall::can_cast(node.kind()) || ast::MacroExpr::can_cast(node.kind())
695        }) {
696            self.limitation(
697                "rust-macro-expansion-not-instrumented",
698                "Declarative and procedural macro expansions are not yet part of the owned source denominator",
699            );
700        }
701
702        // An obligation the probes cannot reach stays in the denominator, but the
703        // gap has to be declared rather than left to read as merely uncovered.
704        // Only a context that actually holds an obligation counts:
705        // `const MAX: usize = 10;` costs nothing and must not raise a limitation.
706        let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
707            ast::StmtList::cast(node.clone()).is_some_and(|list| {
708                list.statements().next().is_some() || list.tail_expr().is_some()
709            }) || ast::IfExpr::can_cast(node.kind())
710                || ast::WhileExpr::can_cast(node.kind())
711                || ast::MatchGuard::can_cast(node.kind())
712                || ast::ForExpr::can_cast(node.kind())
713                || ast::MatchArm::can_cast(node.kind())
714                || ast::TryExpr::can_cast(node.kind())
715                || ast::ClosureExpr::can_cast(node.kind())
716                || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
717                    matches!(
718                        binary.op_kind(),
719                        Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
720                    )
721                })
722        };
723        if root
724            .descendants()
725            .any(|node| bears_obligation(&node) && in_const_context(&node))
726        {
727            self.limitation(
728                "rust-const-context-not-instrumented",
729                "Runtime probes cannot execute in const fn or compile-time evaluation",
730            );
731        }
732        if root
733            .descendants()
734            .any(|node| bears_obligation(&node) && in_global_allocator(&node))
735        {
736            self.limitation(
737                "rust-global-allocator-not-instrumented",
738                "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates",
739            );
740        }
741
742        if let Some(error) = self.error {
743            return Err(error);
744        }
745        self.manifest
746            .decisions
747            .sort_by(|left, right| left.id.cmp(&right.id));
748        self.manifest
749            .points
750            .sort_by(|left, right| left.id.cmp(&right.id));
751        self.manifest
752            .branches
753            .sort_by(|left, right| left.id.cmp(&right.id));
754        self.manifest.limitations.sort_by(|left, right| {
755            left.get("id")
756                .and_then(|value| value.as_str())
757                .cmp(&right.get("id").and_then(|value| value.as_str()))
758        });
759        Ok(self.manifest)
760    }
761}
762
763pub fn build_rust_manifest(
764    file: &str,
765    source: &str,
766) -> Result<CoverageManifest, RustInstrumenterError> {
767    if source.len() > u32::MAX as usize {
768        return Err(RustInstrumenterError::SourceTooLarge);
769    }
770    let parsed = SourceFile::parse(source, Edition::CURRENT);
771    let errors = parsed
772        .errors()
773        .into_iter()
774        .map(|error| error.to_string())
775        .collect::<Vec<_>>();
776    if !errors.is_empty() {
777        return Err(RustInstrumenterError::Parse(errors));
778    }
779    RustObligationCollector::new(file, source).collect(&parsed.tree())
780}
781
782fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
783    let list = block.stmt_list()?;
784    list.attrs()
785        .last()
786        .map(|attribute| usize::from(attribute.syntax().text_range().end()))
787        .or_else(|| {
788            list.l_curly_token()
789                .map(|token| usize::from(token.text_range().end()))
790        })
791}
792
793/// A node's range without its outer attributes: a wrapper placed here stays
794/// under the attributes, so `#[cfg]` governs the wrapper and the node alike.
795fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
796    let range = node.syntax().text_range();
797    node.attrs().last().map_or(range, |attribute| {
798        TextRange::new(attribute.syntax().text_range().end(), range.end())
799    })
800}
801
802fn has_let(expression: &ast::Expr) -> bool {
803    expression
804        .syntax()
805        .descendants()
806        .any(|node| ast::LetExpr::can_cast(node.kind()))
807}
808
809/// The expression whose condition is a let chain.
810enum ChainHost<'a> {
811    If(&'a ast::IfExpr),
812    While(&'a ast::WhileExpr),
813}
814
815/// The `const` naming a let chain's `&&` operators and their alternative IDs.
816fn allocate_chain_table_name(
817    file: &str,
818    condition: &ast::Expr,
819    identifiers: &mut BTreeSet<String>,
820) -> String {
821    let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
822    let suffix = id
823        .rsplit(':')
824        .next()
825        .unwrap_or("chain")
826        .to_ascii_uppercase();
827    let base = format!("__SUPERCOV_CHAIN_{suffix}");
828    let mut candidate = base.clone();
829    let mut attempt = 0_usize;
830    while !identifiers.insert(candidate.clone()) {
831        attempt += 1;
832        candidate = format!("{base}_{attempt}");
833    }
834    candidate
835}
836
837/// A decision whose condition is a let chain. A `let` cannot pass through a
838/// call and the chain cannot be wrapped as a whole, so the frame lives in a
839/// block around the `if` or `while`, ordinary conditions take `condition`
840/// wrappers, each later `let` is preceded by a `reached` marker, and the
841/// outcome is recorded where it becomes known: at the entry of the then
842/// branch or loop body (taken) and at the else branch or after the loop (not
843/// taken). From those, the runtime derives every pattern's outcome exactly:
844/// a chain tries its conditions in order and stops at the first that fails.
845/// The chain's `&&` operators are recorded from the same frame, through a
846/// table of the operators whose left side holds a `let`.
847fn instrument_let_chain(
848    insertions: &mut Vec<Insertion>,
849    runtime_path: &str,
850    file: &str,
851    condition: &ast::Expr,
852    host: ChainHost<'_>,
853    identifiers: &mut BTreeSet<String>,
854) {
855    // The block goes after any outer attributes, so `#[cfg]` keeps governing
856    // the frame together with the expression it belongs to.
857    let (kind, host_range, body) = match &host {
858        ChainHost::If(expression) => (
859            "if",
860            range_after_attributes(*expression),
861            expression.then_branch(),
862        ),
863        ChainHost::While(expression) => (
864            "while",
865            range_after_attributes(*expression),
866            expression.loop_body(),
867        ),
868    };
869    let Some(body_offset) = body.as_ref().and_then(block_entry_offset) else {
870        return;
871    };
872    let range = condition.syntax().text_range();
873    let id = stable_id(file, "decision", range, kind);
874    let mut atoms = Vec::new();
875    RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
876    let lets = condition
877        .syntax()
878        .descendants()
879        .filter_map(ast::LetExpr::cast)
880        .map(|expression| expression.syntax().text_range())
881        .collect::<Vec<_>>();
882    let frame = allocate_frame_name(file, condition, kind, identifiers);
883    let table = allocate_chain_table_name(file, condition, identifiers);
884
885    let mut operators = Vec::new();
886    for binary in condition
887        .syntax()
888        .descendants()
889        .filter_map(ast::BinExpr::cast)
890    {
891        // Let chains are `&&`-only at the top level; an operator whose left
892        // side holds a `let` is one the logical wrapper could not touch.
893        if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
894            continue;
895        }
896        let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
897            continue;
898        };
899        if !has_let(&left) {
900            continue;
901        }
902        let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
903        let right_range = right.syntax().text_range();
904        let Some(first) = atoms
905            .iter()
906            .position(|atom| right_range.contains_range(*atom))
907        else {
908            continue;
909        };
910        operators.push(format!(
911            "({first}, {:?}, {:?})",
912            format!("{branch}:short-circuit"),
913            format!("{branch}:evaluated")
914        ));
915    }
916
917    let prefix = format!(
918        "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
919        operators.join(", "),
920        atoms.len()
921    );
922    let suffix = match &host {
923        ChainHost::If(expression) => match expression.else_branch() {
924            Some(_) => " }".to_owned(),
925            None => format!(
926                " else {{ {runtime_path}::decision_chain(&mut {frame}, false, {table}); }} }}"
927            ),
928        },
929        ChainHost::While(_) => {
930            format!(" {runtime_path}::decision_chain(&mut {frame}, false, {table}); }}")
931        }
932    };
933    push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
934    push_direct(
935        insertions,
936        usize::from(range.start()),
937        format!("{runtime_path}::reached(&mut {frame}, 0) && "),
938    );
939    for (index, atom) in atoms.iter().enumerate() {
940        if lets.contains(atom) {
941            if index > 0 {
942                push_direct(
943                    insertions,
944                    usize::from(atom.start()),
945                    format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
946                );
947            }
948        } else {
949            push_wrapper(
950                insertions,
951                *atom,
952                *atom,
953                1,
954                format!("{runtime_path}::condition(("),
955                format!("), &mut {frame}, {index})"),
956            );
957        }
958    }
959    push_direct(
960        insertions,
961        body_offset,
962        format!("\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"),
963    );
964    if let ChainHost::If(expression) = &host {
965        match expression.else_branch() {
966            Some(ast::ElseBranch::Block(block)) => {
967                if let Some(offset) = block_entry_offset(&block) {
968                    push_direct(
969                        insertions,
970                        offset,
971                        format!("\n{runtime_path}::decision_chain(&mut {frame}, false, {table});"),
972                    );
973                }
974            }
975            Some(ast::ElseBranch::IfExpr(nested)) => {
976                let nested_range = nested.syntax().text_range();
977                push_wrapper(
978                    insertions,
979                    nested_range,
980                    nested_range,
981                    0,
982                    format!("{{ {runtime_path}::decision_chain(&mut {frame}, false, {table}); "),
983                    " }".into(),
984                );
985            }
986            None => {}
987        }
988    }
989}
990
991/// Where an item for `node` can go: the entry of the nearest enclosing block.
992/// An item declared there is visible to the whole block, so nothing about the
993/// expression itself -- its value, its temporaries -- changes.
994fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
995    node.ancestors()
996        .skip(1)
997        .find_map(ast::BlockExpr::cast)
998        .and_then(|block| block_entry_offset(&block))
999}
1000
1001/// A block written as a bare `{ ... }`: a probe placed just inside its brace
1002/// runs when the block is entered. Labeled, `unsafe`, `async` and `const`
1003/// blocks are wrapped instead, so an `async` body does not defer the probe.
1004fn plain_block(block: &ast::BlockExpr) -> bool {
1005    block
1006        .syntax()
1007        .first_token()
1008        .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1009}
1010
1011fn instrument_decision(
1012    insertions: &mut Vec<Insertion>,
1013    runtime_path: &str,
1014    file: &str,
1015    condition: &ast::Expr,
1016    kind: &str,
1017    frame_name: &str,
1018) -> bool {
1019    if cannot_carry_probe(condition.syntax())
1020        || condition
1021            .syntax()
1022            .descendants()
1023            .any(|node| ast::LetExpr::can_cast(node.kind()))
1024    {
1025        return false;
1026    }
1027    let range = condition.syntax().text_range();
1028    let id = stable_id(file, "decision", range, kind);
1029    let mut condition_ranges = Vec::new();
1030    RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1031    push_wrapper(
1032        insertions,
1033        range,
1034        range,
1035        0,
1036        format!(
1037            "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1038            condition_ranges.len()
1039        ),
1040        format!("), &mut {frame_name}) }})"),
1041    );
1042    // Each condition's scope is the condition itself, so a wrapper that
1043    // started earlier and ends where this condition ends -- the left operand
1044    // of a logical operator -- closes after it, not before.
1045    for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1046        push_wrapper(
1047            insertions,
1048            atomic_range,
1049            atomic_range,
1050            1,
1051            format!("{runtime_path}::condition(("),
1052            format!("), &mut {frame_name}, {index})"),
1053        );
1054    }
1055    true
1056}
1057
1058/// Produce a private Rust candidate using only Supercov-owned probe calls.
1059///
1060/// The caller supplies a collision-free generated crate-local runtime path.
1061/// Every obligation the manifest declares -- statements, functions, decisions
1062/// with their conditions -- let chains included -- match arms, logical
1063/// operators, loops and the try operator -- takes an owned probe; what a
1064/// probe cannot reach (const contexts, macro expansions, an attributed `let`
1065/// with no initializer) stays in the denominator behind an explicit
1066/// limitation.
1067pub fn instrument_rust_source(
1068    file: &str,
1069    source: &str,
1070    runtime_path: &str,
1071) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1072    if !valid_runtime_path(runtime_path) {
1073        return Err(RustInstrumenterError::InvalidRuntimePath);
1074    }
1075    let mut manifest = build_rust_manifest(file, source)?;
1076    let parsed = SourceFile::parse(source, Edition::CURRENT);
1077    let tree = parsed.tree();
1078    let root = tree.syntax();
1079    let mut insertions = Vec::new();
1080    let mut identifiers = root
1081        .descendants_with_tokens()
1082        .filter_map(|element| element.into_token())
1083        .filter(|token| token.kind() == SyntaxKind::IDENT)
1084        .map(|token| token.text().to_string())
1085        .collect::<BTreeSet<_>>();
1086
1087    let mut skipped_attributed_statement = false;
1088    // A probe must never be PREPENDED to a statement that carries outer
1089    // attributes. `#[cfg]` selects among adjacent statements, and a bare
1090    // `hit(...)` inserted between them survives the strip and changes which
1091    // expression is the block's tail: memchr's `is_available` returns bool
1092    // from one of two cfg-gated blocks, and the stray probe turned the kept
1093    // block into a statement and the probe itself into a `()` tail -- 32
1094    // E0308s across the crate. An attributed BLOCK takes the probe inside its
1095    // braces, where the same cfg governs both; any other attributed
1096    // expression is wrapped in such a block. Only a `let` with no initializer
1097    // has nowhere to put a probe, and is declared.
1098    let attributed_probe = |insertions: &mut Vec<Insertion>,
1099                            skipped: &mut bool,
1100                            expression: Option<ast::Expr>,
1101                            has_attrs: bool,
1102                            range: TextRange,
1103                            id: String| {
1104        if !has_attrs {
1105            push_direct(
1106                insertions,
1107                usize::from(range.start()),
1108                format!("{runtime_path}::hit({id:?});"),
1109            );
1110            return;
1111        }
1112        let Some(expression) = expression else {
1113            *skipped = true;
1114            return;
1115        };
1116        if let ast::Expr::BlockExpr(block) = &expression
1117            && let Some(offset) = block_entry_offset(block)
1118        {
1119            push_direct(
1120                insertions,
1121                offset,
1122                format!("\n{runtime_path}::hit({id:?});"),
1123            );
1124            return;
1125        }
1126        // Any other attributed expression -- or the initializer of an
1127        // attributed `let` -- moves into a block that carries the probe: the
1128        // attributes now govern probe and expression together, the block has
1129        // the expression's value where the expression was, and a block's tail
1130        // still extends the temporaries a `let` would have extended.
1131        let start = expression.attrs().last().map_or_else(
1132            || expression.syntax().text_range().start(),
1133            |attribute| attribute.syntax().text_range().end(),
1134        );
1135        let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1136        push_wrapper(
1137            insertions,
1138            wrapped,
1139            wrapped,
1140            0,
1141            format!(" {{ {runtime_path}::hit({id:?}); ("),
1142            ") }".into(),
1143        );
1144    };
1145    for list in root.descendants().filter_map(ast::StmtList::cast) {
1146        for statement in list.statements() {
1147            let (range, expression, has_attrs) = match statement {
1148                ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1149                    let expression = statement.expr();
1150                    // Outer attributes on an expression statement attach to
1151                    // the inner expression in this grammar.
1152                    let has_attrs = expression
1153                        .as_ref()
1154                        .is_some_and(|expression| expression.attrs().next().is_some());
1155                    (statement.syntax().text_range(), expression, has_attrs)
1156                }
1157                ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1158                    let has_attrs = statement.attrs().next().is_some();
1159                    // Only an attributed `let` needs its initializer; a plain
1160                    // one takes the probe before the statement.
1161                    let initializer = has_attrs.then(|| statement.initializer()).flatten();
1162                    (statement.syntax().text_range(), initializer, has_attrs)
1163                }
1164                _ => continue,
1165            };
1166            let id = stable_id(file, "statement", range, "");
1167            attributed_probe(
1168                &mut insertions,
1169                &mut skipped_attributed_statement,
1170                expression,
1171                has_attrs,
1172                range,
1173                id,
1174            );
1175        }
1176        if let Some(tail) = list
1177            .tail_expr()
1178            .filter(|tail| !cannot_carry_probe(tail.syntax()))
1179        {
1180            let range = tail.syntax().text_range();
1181            let id = stable_id(file, "statement", range, "");
1182            let has_attrs = tail.attrs().next().is_some();
1183            attributed_probe(
1184                &mut insertions,
1185                &mut skipped_attributed_statement,
1186                Some(tail),
1187                has_attrs,
1188                range,
1189                id,
1190            );
1191        }
1192    }
1193
1194    for function in root.descendants().filter_map(ast::Fn::cast) {
1195        // `cannot_carry_probe` covers `const fn` itself, since a node's own
1196        // ancestors include the node.
1197        if cannot_carry_probe(function.syntax()) {
1198            continue;
1199        }
1200        let Some(body) = function.body() else {
1201            continue;
1202        };
1203        let label = function.name().map(|name| name.text().to_string());
1204        let id = stable_id(
1205            file,
1206            "function",
1207            function.syntax().text_range(),
1208            label.as_deref().unwrap_or(""),
1209        );
1210        if let Some(offset) = block_entry_offset(&body) {
1211            push_direct(
1212                &mut insertions,
1213                offset,
1214                format!("\n{runtime_path}::hit({id:?});"),
1215            );
1216        }
1217    }
1218
1219    for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
1220        let Some(body) = closure.body() else {
1221            continue;
1222        };
1223        if cannot_carry_probe(body.syntax()) {
1224            continue;
1225        }
1226        let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
1227        if let ast::Expr::BlockExpr(block) = &body {
1228            if let Some(offset) = block_entry_offset(block) {
1229                push_direct(
1230                    &mut insertions,
1231                    offset,
1232                    format!("\n{runtime_path}::hit({id:?});"),
1233                );
1234            }
1235        } else {
1236            let range = body.syntax().text_range();
1237            push_wrapper(
1238                &mut insertions,
1239                range,
1240                closure.syntax().text_range(),
1241                0,
1242                format!("{{ {runtime_path}::hit({id:?}); ("),
1243                ") }".into(),
1244            );
1245        }
1246    }
1247
1248    // Match arms. One `const` per match, at the entry of the enclosing block,
1249    // names every arm's `not selected` and `selected` IDs in source order;
1250    // each arm then records itself as selected and every arm before it as
1251    // passed over, since a match tries its arms in order and stops at the
1252    // first that fits. The runtime dedupes by ID, so a hot match costs one
1253    // record per alternative.
1254    for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1255        if cannot_carry_probe(expression.syntax()) {
1256            continue;
1257        }
1258        let Some(list) = expression.match_arm_list() else {
1259            continue;
1260        };
1261        let arms = list.arms().collect::<Vec<_>>();
1262        if arms.is_empty() {
1263            continue;
1264        }
1265        let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
1266            continue;
1267        };
1268        let table = allocate_table_name(file, &expression, &mut identifiers);
1269        let entries = arms
1270            .iter()
1271            .map(|arm| {
1272                let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
1273                format!(
1274                    "{:?}, {:?}",
1275                    format!("{id}:missed"),
1276                    format!("{id}:selected")
1277                )
1278            })
1279            .collect::<Vec<_>>()
1280            .join(", ");
1281        push_direct(
1282            &mut insertions,
1283            table_offset,
1284            format!("\nconst {table}: &[&str] = &[{entries}];"),
1285        );
1286        for (index, arm) in arms.iter().enumerate() {
1287            let Some(body) = arm.expr() else {
1288                continue;
1289            };
1290            let call = format!("{runtime_path}::arms({table}, {index});");
1291            match &body {
1292                ast::Expr::BlockExpr(block) if plain_block(block) => {
1293                    if let Some(offset) = block_entry_offset(block) {
1294                        push_direct(&mut insertions, offset, format!("\n{call}"));
1295                    }
1296                }
1297                _ => push_wrapper(
1298                    &mut insertions,
1299                    body.syntax().text_range(),
1300                    arm.syntax().text_range(),
1301                    0,
1302                    format!("{{ {call} ("),
1303                    ") }".into(),
1304                ),
1305            }
1306        }
1307    }
1308
1309    // Logical operators: the left operand alone decides whether the right one
1310    // runs, so wrapping it records the outcome without touching evaluation
1311    // order. `&&` short-circuits on false, `||` on true.
1312    for binary in root.descendants().filter_map(ast::BinExpr::cast) {
1313        let short_circuits_when = match binary.op_kind() {
1314            Some(BinaryOp::LogicOp(LogicOp::And)) => false,
1315            Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
1316            _ => continue,
1317        };
1318        if cannot_carry_probe(binary.syntax()) {
1319            continue;
1320        }
1321        let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1322            continue;
1323        };
1324        // In a let chain the left operand is (or holds) a `let`, whose
1325        // bindings must stay in scope for the right operand; it cannot pass
1326        // through a call. The chain's own probes record that operator.
1327        if has_let(&left) {
1328            continue;
1329        }
1330        let kind = if short_circuits_when {
1331            "logical-or"
1332        } else {
1333            "logical-and"
1334        };
1335        let id = stable_id(file, "branch", right.syntax().text_range(), kind);
1336        push_wrapper(
1337            &mut insertions,
1338            left.syntax().text_range(),
1339            binary.syntax().text_range(),
1340            2,
1341            format!("{runtime_path}::logical(("),
1342            format!(
1343                "), {short_circuits_when}, {:?}, {:?})",
1344                format!("{id}:short-circuit"),
1345                format!("{id}:evaluated")
1346            ),
1347        );
1348    }
1349
1350    // `for` loops: the iterable passes through an adapter that records, on
1351    // the first `next`, whether the body ran at all. `into_iter` is called
1352    // where the loop would have called it, on the same expression.
1353    for expression in root.descendants().filter_map(ast::ForExpr::cast) {
1354        if cannot_carry_probe(expression.syntax()) {
1355            continue;
1356        }
1357        let Some(iterable) = expression.iterable() else {
1358            continue;
1359        };
1360        let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
1361        // Scope is the wrapped range itself: any wrapper that also starts
1362        // here and reaches further -- a decision, a match arm -- must stay
1363        // outside this one.
1364        push_wrapper(
1365            &mut insertions,
1366            iterable.syntax().text_range(),
1367            iterable.syntax().text_range(),
1368            0,
1369            format!("{runtime_path}::for_loop(("),
1370            format!(
1371                "), {:?}, {:?})",
1372                format!("{id}:zero"),
1373                format!("{id}:entered")
1374            ),
1375        );
1376    }
1377
1378    // `while` loops: a flag beside the loop, cleared by the first body entry
1379    // and read once the loop is over. The condition stays as written, so
1380    // `while let` is covered too.
1381    for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1382        if cannot_carry_probe(expression.syntax()) {
1383            continue;
1384        }
1385        let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
1386            continue;
1387        };
1388        let id = stable_id(
1389            file,
1390            "branch",
1391            expression.syntax().text_range(),
1392            "while-loop",
1393        );
1394        let flag = allocate_flag_name(file, &expression, &mut identifiers);
1395        let range = range_after_attributes(&expression);
1396        push_wrapper(
1397            &mut insertions,
1398            range,
1399            range,
1400            0,
1401            format!("{{ let mut {flag} = true; "),
1402            format!(
1403                " {runtime_path}::zero_iterations({flag}, {:?}) }}",
1404                format!("{id}:zero")
1405            ),
1406        );
1407        push_direct(
1408            &mut insertions,
1409            offset,
1410            format!(
1411                "\n{runtime_path}::entered(&mut {flag}, {:?});",
1412                format!("{id}:entered")
1413            ),
1414        );
1415    }
1416
1417    // The try operator: the operand passes through a probe that reads which
1418    // way `?` will go. Every stable `Try` type is covered by the runtime's
1419    // `TryProbe` implementations.
1420    for expression in root.descendants().filter_map(ast::TryExpr::cast) {
1421        if cannot_carry_probe(expression.syntax()) {
1422            continue;
1423        }
1424        let Some(operand) = expression.expr() else {
1425            continue;
1426        };
1427        let id = stable_id(
1428            file,
1429            "branch",
1430            expression.syntax().text_range(),
1431            "try-operator",
1432        );
1433        // Scope is the operand alone: a decision wrapping `expr?` as its
1434        // condition starts at the same offset and must close after this.
1435        push_wrapper(
1436            &mut insertions,
1437            operand.syntax().text_range(),
1438            operand.syntax().text_range(),
1439            0,
1440            format!("{runtime_path}::TryProbe::probe(("),
1441            format!(
1442                "), {:?}, {:?})",
1443                format!("{id}:continued"),
1444                format!("{id}:returned")
1445            ),
1446        );
1447    }
1448
1449    for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1450        let Some(condition) = expression.condition() else {
1451            continue;
1452        };
1453        if has_let(&condition) {
1454            if !cannot_carry_probe(condition.syntax()) {
1455                instrument_let_chain(
1456                    &mut insertions,
1457                    runtime_path,
1458                    file,
1459                    &condition,
1460                    ChainHost::If(&expression),
1461                    &mut identifiers,
1462                );
1463            }
1464            continue;
1465        }
1466        let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
1467        instrument_decision(
1468            &mut insertions,
1469            runtime_path,
1470            file,
1471            &condition,
1472            "if",
1473            &frame_name,
1474        );
1475    }
1476    for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1477        let Some(condition) = expression.condition() else {
1478            continue;
1479        };
1480        if has_let(&condition) {
1481            if !cannot_carry_probe(condition.syntax()) {
1482                instrument_let_chain(
1483                    &mut insertions,
1484                    runtime_path,
1485                    file,
1486                    &condition,
1487                    ChainHost::While(&expression),
1488                    &mut identifiers,
1489                );
1490            }
1491            continue;
1492        }
1493        let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
1494        instrument_decision(
1495            &mut insertions,
1496            runtime_path,
1497            file,
1498            &condition,
1499            "while",
1500            &frame_name,
1501        );
1502    }
1503    for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1504        if let Some(condition) = guard.condition() {
1505            let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
1506            instrument_decision(
1507                &mut insertions,
1508                runtime_path,
1509                file,
1510                &condition,
1511                "match-guard",
1512                &frame_name,
1513            );
1514        }
1515    }
1516
1517    if skipped_attributed_statement {
1518        add_manifest_limitation(
1519            &mut manifest,
1520            file,
1521            "rust-attributed-statement-probes-not-injected",
1522            "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
1523        );
1524    }
1525    manifest.limitations.sort_by(|left, right| {
1526        left.get("id")
1527            .and_then(|value| value.as_str())
1528            .cmp(&right.get("id").and_then(|value| value.as_str()))
1529    });
1530
1531    let code = apply_insertions(source, insertions)?;
1532    let transformed = SourceFile::parse(&code, Edition::CURRENT);
1533    let errors = transformed
1534        .errors()
1535        .into_iter()
1536        .map(|error| error.to_string())
1537        .collect::<Vec<_>>();
1538    if !errors.is_empty() {
1539        return Err(RustInstrumenterError::Parse(errors));
1540    }
1541    Ok(RustInstrumentedSource { code, manifest })
1542}
1543
1544#[cfg(test)]
1545mod tests {
1546    use std::{
1547        fs,
1548        process::Command,
1549        time::{SystemTime, UNIX_EPOCH},
1550    };
1551
1552    use super::*;
1553
1554    const NOOP_RUNTIME: &str = r#"
1555#[doc(hidden)]
1556mod __supercov_runtime_v1 {
1557    pub struct DecisionFrame;
1558    impl DecisionFrame {
1559        pub fn new(_: &'static str, _: usize) -> Self { Self }
1560    }
1561    pub fn hit(_: &'static str) {}
1562    pub fn arms(_: &[&'static str], _: usize) {}
1563    pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
1564    pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
1565        iterable.into_iter()
1566    }
1567    pub fn entered(_: &mut bool, _: &'static str) {}
1568    pub fn zero_iterations(_: bool, _: &'static str) {}
1569    pub trait TryProbe: Sized {
1570        fn probe(self, _: &'static str, _: &'static str) -> Self { self }
1571    }
1572    impl<T> TryProbe for T {}
1573    pub fn condition(value: bool, _: &mut DecisionFrame, _: usize) -> bool { value }
1574    pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
1575    pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
1576    pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
1577}
1578"#;
1579
1580    fn compile_and_run(source: &str, name: &str) -> std::process::Output {
1581        let nonce = SystemTime::now()
1582            .duration_since(UNIX_EPOCH)
1583            .unwrap()
1584            .as_nanos();
1585        let directory = std::env::temp_dir().join(format!(
1586            "supercov-rust-transform-{}-{nonce}-{name}",
1587            std::process::id()
1588        ));
1589        fs::create_dir(&directory).unwrap();
1590        let input = directory.join("main.rs");
1591        let binary = directory.join("program");
1592        fs::write(&input, source).unwrap();
1593        let compile = Command::new("rustc")
1594            .arg("--edition=2024")
1595            .arg(&input)
1596            .arg("-o")
1597            .arg(&binary)
1598            .output()
1599            .unwrap();
1600        assert!(
1601            compile.status.success(),
1602            "rustc failed:\n{}\nsource:\n{source}",
1603            String::from_utf8_lossy(&compile.stderr)
1604        );
1605        let output = Command::new(&binary).output().unwrap();
1606        fs::remove_dir_all(directory).unwrap();
1607        output
1608    }
1609
1610    #[test]
1611    fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
1612        let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
1613    let picked = if first && (second || third) {
1614        values.first()?
1615    } else {
1616        None
1617    };
1618    for value in values {
1619        if first || second {
1620            return Some(value);
1621        }
1622    }
1623    match picked {
1624        Some(value) if second && third => Some(value),
1625        _ => None,
1626    }
1627}
1628
1629fn closure(value: i32) -> bool {
1630    (|candidate| candidate > 0)(value)
1631}
1632"#;
1633        let first = build_rust_manifest("src/lib.rs", source).unwrap();
1634        let second = build_rust_manifest("src/lib.rs", source).unwrap();
1635        assert_eq!(first, second);
1636        assert!(first.points.iter().any(|point| {
1637            point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
1638        }));
1639        assert!(first.points.iter().any(|point| {
1640            point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
1641        }));
1642        let first_if = first
1643            .decisions
1644            .iter()
1645            .find(|decision| decision.line == 2)
1646            .unwrap();
1647        assert_eq!(first_if.conditions, ["first", "second", "third"]);
1648        assert_eq!(first_if.column, 20);
1649        assert!(
1650            first
1651                .branches
1652                .iter()
1653                .any(|branch| branch.kind == "for-loop")
1654        );
1655        let mut arms = first
1656            .branches
1657            .iter()
1658            .filter(|branch| branch.kind == "match-arm")
1659            .collect::<Vec<_>>();
1660        arms.sort_by_key(|branch| branch.line);
1661        assert_eq!(arms.len(), 2);
1662        assert_eq!(
1663            arms[0]
1664                .alternatives
1665                .iter()
1666                .map(|alternative| alternative.label.as_str())
1667                .collect::<Vec<_>>(),
1668            ["not selected", "selected"]
1669        );
1670        // The last arm of an exhaustive match is reached or not; it is never
1671        // considered and passed over.
1672        assert_eq!(
1673            arms[1]
1674                .alternatives
1675                .iter()
1676                .map(|alternative| alternative.label.as_str())
1677                .collect::<Vec<_>>(),
1678            ["selected"]
1679        );
1680        assert!(
1681            first
1682                .branches
1683                .iter()
1684                .any(|branch| branch.kind == "try-operator")
1685        );
1686        assert!(first.decisions.iter().all(|decision| {
1687            decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
1688        }));
1689        assert!(first.limitations.is_empty());
1690    }
1691
1692    #[test]
1693    fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
1694        let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
1695
1696fn checked(value: bool) -> bool {
1697    assert!(value);
1698    const { doubled(2) == 4 }
1699}
1700"#;
1701        let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
1702        let ids = manifest
1703            .limitations
1704            .iter()
1705            .filter_map(|limitation| limitation.get("id")?.as_str())
1706            .collect::<BTreeSet<_>>();
1707        assert_eq!(
1708            ids,
1709            BTreeSet::from([
1710                "rust-const-context-not-instrumented",
1711                "rust-macro-expansion-not-instrumented"
1712            ])
1713        );
1714        assert!(!manifest.points.iter().any(|point| {
1715            point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
1716        }));
1717    }
1718
1719    #[test]
1720    fn transforms_points_and_nested_decisions_without_changing_behavior() {
1721        let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
1722
1723static CALLS: AtomicUsize = AtomicUsize::new(0);
1724
1725fn observed(name: &str, value: bool) -> bool {
1726    let order = CALLS.fetch_add(1, Ordering::SeqCst);
1727    println!("{order}:{name}:{value}");
1728    value
1729}
1730
1731fn classify(first: bool, second: bool, third: bool) -> i32 {
1732    if observed("a", first) && (observed("b", second) || observed("c", third)) {
1733        7
1734    } else {
1735        3
1736    }
1737}
1738
1739fn main() {
1740    let closure = |value: i32| value + 1;
1741    println!("result={}", closure(classify(true, false, true)));
1742}
1743"#;
1744        let transformed =
1745            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1746        assert!(transformed.code.contains("::condition("));
1747        assert!(transformed.code.contains("::decision("));
1748        assert!(transformed.code.contains("::hit("));
1749        let original = compile_and_run(source, "original");
1750        let instrumented = compile_and_run(
1751            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
1752            "instrumented",
1753        );
1754        assert_eq!(instrumented.status, original.status);
1755        assert_eq!(instrumented.stdout, original.stdout);
1756        assert_eq!(instrumented.stderr, original.stderr);
1757    }
1758
1759    #[test]
1760    fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
1761        let source = r#"const fn enabled(value: bool) -> bool {
1762    if value { true } else { false }
1763}
1764
1765fn classify(value: Option<bool>, fallback: bool) -> bool {
1766    if let Some(inner) = value && inner && fallback { true } else { false }
1767}
1768"#;
1769        let transformed =
1770            instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
1771        let ids = transformed
1772            .manifest
1773            .limitations
1774            .iter()
1775            .filter_map(|limitation| limitation.get("id")?.as_str())
1776            .collect::<BTreeSet<_>>();
1777        assert!(ids.contains("rust-const-context-not-instrumented"));
1778        assert!(!ids.contains("rust-let-chain-probes-not-injected"));
1779        // The chain: a marker at the front, ordinary conditions wrapped, the
1780        // outcome recorded in both branches; the `let` itself untouched.
1781        assert!(
1782            transformed
1783                .code
1784                .contains("::reached(&mut __supercov_decision_")
1785        );
1786        assert!(
1787            transformed
1788                .code
1789                .contains("::condition((inner), &mut __supercov_decision_")
1790        );
1791        assert!(
1792            transformed
1793                .code
1794                .contains("::decision_chain(&mut __supercov_decision_")
1795        );
1796        assert!(transformed.code.contains("&& let Some(inner) = value &&"));
1797        assert!(!transformed.code.contains("condition((let"));
1798    }
1799
1800    #[test]
1801    fn let_chains_keep_their_behavior() {
1802        let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
1803    if let Some(inner) = value && inner > 0 && flag {
1804        "positive"
1805    } else if let Some(inner) = value && (inner < 0 || flag) {
1806        "negative-or-flagged"
1807    } else {
1808        "other"
1809    }
1810}
1811
1812fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
1813    let mut total = 0;
1814    let mut it = values.iter();
1815    while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
1816        total += inner * second;
1817        if total > 100 {
1818            break;
1819        }
1820    }
1821    total
1822}
1823
1824fn tail(value: Option<&str>) -> usize {
1825    let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
1826    if let Some(text) = value && text.starts_with('x') {
1827        println!("x-prefixed");
1828    }
1829    pick(value)
1830}
1831
1832fn main() {
1833    for value in [Some(3), Some(-3), Some(0), None] {
1834        for flag in [true, false] {
1835            println!("{value:?} {flag} {}", describe(value, flag));
1836        }
1837    }
1838    println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
1839    println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
1840    println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
1841}
1842"#;
1843        let transformed =
1844            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1845        assert_eq!(
1846            transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
1847            5
1848        );
1849        assert!(!transformed.manifest.limitations.iter().any(|limitation| {
1850            limitation.get("id").and_then(|id| id.as_str())
1851                == Some("rust-let-chain-probes-not-injected")
1852        }));
1853        let original = compile_and_run(source, "original-chains");
1854        let instrumented = compile_and_run(
1855            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
1856            "instrumented-chains",
1857        );
1858        assert_eq!(instrumented.status, original.status);
1859        assert_eq!(instrumented.stdout, original.stdout);
1860        assert_eq!(instrumented.stderr, original.stderr);
1861    }
1862
1863    #[test]
1864    fn instrumented_const_and_static_initialisers_still_compile() {
1865        // Every one of these positions is const-evaluated, so none of them can
1866        // hold a call to the runtime -- `condition`, `decision` and `hit` are
1867        // not `const fn`. Found on bytes-1.12.1, whose test target has
1868        // `const ITERS: usize = if cfg!(miri) { 100 } else { 1_000 };` and
1869        // failed to build with E0015.
1870        let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
1871static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
1872
1873enum Mode {
1874    Narrow = if cfg!(unix) { 1 } else { 2 },
1875}
1876
1877struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
1878
1879impl Buffer {
1880    const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
1881}
1882
1883fn scaled(flag: bool) -> usize {
1884    const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
1885    if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
1886}
1887
1888fn main() {
1889    let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
1890    println!(
1891        "{} {} {} {}",
1892        scaled(true),
1893        scaled(false),
1894        Mode::Narrow as usize,
1895        buffer.0.len()
1896    );
1897}
1898"#;
1899        let transformed =
1900            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1901        // The runtime `if` in `scaled` is still instrumented -- declining a
1902        // const initialiser must not decline the whole file.
1903        assert!(transformed.code.contains("::decision("));
1904        let ids = transformed
1905            .manifest
1906            .limitations
1907            .iter()
1908            .filter_map(|limitation| limitation.get("id")?.as_str())
1909            .collect::<BTreeSet<_>>();
1910        assert!(ids.contains("rust-const-context-not-instrumented"));
1911
1912        let original = compile_and_run(source, "const-original");
1913        let instrumented = compile_and_run(
1914            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
1915            "const-instrumented",
1916        );
1917        assert_eq!(instrumented.status, original.status);
1918        assert_eq!(instrumented.stdout, original.stdout);
1919        assert_eq!(instrumented.stderr, original.stderr);
1920    }
1921
1922    #[test]
1923    fn a_probed_global_allocator_would_recurse_into_itself() {
1924        // The runtime allocates, so a probe inside `alloc` re-enters `alloc` and
1925        // recurses until the stack is gone. bytes-1.12.1's
1926        // tests/test_bytes_odd_alloc.rs installs one of these, and the
1927        // instrumented binary died with SIGSEGV before libtest could list a
1928        // single test, while the uninstrumented binary listed them fine.
1929        let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
1930
1931struct Odd;
1932
1933unsafe impl GlobalAlloc for Odd {
1934    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1935        if layout.align() == 1 && layout.size() > 0 {
1936            System.alloc(layout)
1937        } else {
1938            System.alloc(layout)
1939        }
1940    }
1941
1942    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
1943        System.dealloc(pointer, layout);
1944    }
1945}
1946
1947#[global_allocator]
1948static ODD: Odd = Odd;
1949
1950fn classify(flag: bool) -> usize {
1951    if flag { 1 } else { 2 }
1952}
1953
1954fn main() {
1955    let held = std::vec![7u8; 32];
1956    println!("{} {}", classify(!held.is_empty()), held.len());
1957}
1958"#;
1959        let transformed =
1960            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1961        // Nothing inside the allocator may carry a probe...
1962        let allocator = transformed
1963            .code
1964            .split("unsafe impl GlobalAlloc for Odd")
1965            .nth(1)
1966            .and_then(|rest| rest.split("#[global_allocator]").next())
1967            .expect("the instrumented source still contains the allocator impl");
1968        assert!(
1969            !allocator.contains("__supercov_runtime_v1"),
1970            "probe injected into a GlobalAlloc impl:\n{allocator}"
1971        );
1972        // ...while `classify`, right next to it, is still measured.
1973        assert!(transformed.code.contains("::decision("));
1974        let ids = transformed
1975            .manifest
1976            .limitations
1977            .iter()
1978            .filter_map(|limitation| limitation.get("id")?.as_str())
1979            .collect::<BTreeSet<_>>();
1980        assert!(ids.contains("rust-global-allocator-not-instrumented"));
1981
1982        let original = compile_and_run(source, "alloc-original");
1983        let instrumented = compile_and_run(
1984            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
1985            "alloc-instrumented",
1986        );
1987        assert_eq!(instrumented.status, original.status);
1988        assert_eq!(instrumented.stdout, original.stdout);
1989        assert_eq!(instrumented.stderr, original.stderr);
1990    }
1991
1992    #[test]
1993    fn match_arms_record_selection_without_changing_behavior() {
1994        let source = r#"#[derive(Debug)]
1995enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
1996
1997fn area(shape: &Shape) -> i32 {
1998    match shape {
1999        Shape::Dot => 0,
2000        Shape::Line(length) if *length < 0 => -length,
2001        Shape::Line(length) => *length,
2002        Shape::Box { w, h } => {
2003            let area = w * h;
2004            area
2005        }
2006    }
2007}
2008
2009fn describe(value: i32) -> &'static str {
2010    let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
2011    match value {
2012        0 => inner(value),
2013        n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
2014        n => match n % 2 {
2015            0 => "even",
2016            _ => inner(n),
2017        },
2018    }
2019}
2020
2021fn main() {
2022    for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
2023        println!("{shape:?}={}", area(&shape));
2024    }
2025    for value in [0, 1, 3, 8] {
2026        println!("{value}:{}", describe(value));
2027    }
2028}
2029"#;
2030        let transformed =
2031            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2032        assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
2033        assert_eq!(
2034            transformed.code.matches("const __SUPERCOV_ARMS_").count(),
2035            4
2036        );
2037        let arms = transformed
2038            .manifest
2039            .branches
2040            .iter()
2041            .filter(|branch| branch.kind == "match-arm")
2042            .count();
2043        assert_eq!(arms, 4 + 3 + 3 + 2);
2044        // Every arm's alternatives appear in a table, and the source keeps
2045        // its meaning.
2046        for branch in transformed
2047            .manifest
2048            .branches
2049            .iter()
2050            .filter(|branch| branch.kind == "match-arm")
2051        {
2052            for alternative in &branch.alternatives {
2053                assert!(
2054                    transformed.code.contains(&format!("{:?}", alternative.id)),
2055                    "{} is not in any table",
2056                    alternative.id
2057                );
2058            }
2059        }
2060        let original = compile_and_run(source, "original-arms");
2061        let instrumented = compile_and_run(
2062            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2063            "instrumented-arms",
2064        );
2065        assert_eq!(instrumented.status, original.status);
2066        assert_eq!(instrumented.stdout, original.stdout);
2067        assert_eq!(instrumented.stderr, original.stderr);
2068    }
2069
2070    #[test]
2071    fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
2072        let source = r#"use std::ops::ControlFlow;
2073
2074fn total(values: &[i32]) -> i32 {
2075    let mut sum = 0;
2076    for value in values {
2077        sum += value;
2078    }
2079    'outer: for row in 0..3 {
2080        for column in 0..3 {
2081            if column > row {
2082                continue 'outer;
2083            }
2084            sum += row * column;
2085        }
2086    }
2087    sum
2088}
2089
2090fn first_even(values: &[i32]) -> Option<i32> {
2091    let mut index = 0;
2092    'scan: while index < values.len() {
2093        if values[index] % 2 == 0 {
2094            break 'scan;
2095        }
2096        index += 1;
2097    }
2098    let mut it = values.iter().skip(index);
2099    while let Some(value) = it.next() {
2100        return Some(*value);
2101    }
2102    None
2103}
2104
2105fn parse_twice(text: &str) -> Result<i32, String> {
2106    let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
2107    let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
2108    Ok(doubled)
2109}
2110
2111fn halve(value: i32) -> Option<i32> {
2112    let even = (value % 2 == 0).then_some(value)?;
2113    Some(even / 2)
2114}
2115
2116fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
2117    let mut sum = 0;
2118    for value in values {
2119        let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
2120        sum += step?;
2121    }
2122    ControlFlow::Continue(sum)
2123}
2124
2125fn gate(a: bool, b: bool, c: bool) -> bool {
2126    let both = a && b;
2127    let either = a || b || c;
2128    both || (either && !c) || (c && a && (b || !b))
2129}
2130
2131fn main() {
2132    println!("{} {}", total(&[]), total(&[1, 2, 3]));
2133    println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
2134    println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
2135    println!("{:?} {:?}", halve(8), halve(7));
2136    println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
2137    for a in [false, true] {
2138        for b in [false, true] {
2139            for c in [false, true] {
2140                print!("{}", gate(a, b, c) as u8);
2141            }
2142        }
2143    }
2144    println!();
2145}
2146"#;
2147        let transformed =
2148            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2149        for marker in [
2150            "::logical((",
2151            "::for_loop((",
2152            "::entered(&mut __supercov_loop_",
2153            "::zero_iterations(__supercov_loop_",
2154            "::TryProbe::probe((",
2155        ] {
2156            assert!(transformed.code.contains(marker), "{marker} missing");
2157        }
2158        let kinds = |kind: &str| {
2159            transformed
2160                .manifest
2161                .branches
2162                .iter()
2163                .filter(|branch| branch.kind == kind)
2164                .count()
2165        };
2166        assert_eq!(kinds("for-loop"), 3 + 1 + 3);
2167        assert_eq!(kinds("while-loop"), 2);
2168        assert_eq!(kinds("try-operator"), 4);
2169        assert_eq!(kinds("logical-and"), 4);
2170        assert_eq!(kinds("logical-or"), 5);
2171        assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2172            limitation.get("id").and_then(|id| id.as_str())
2173                == Some("rust-structural-branch-probes-not-yet-injected")
2174        }));
2175        let original = compile_and_run(source, "original-structural");
2176        let instrumented = compile_and_run(
2177            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2178            "instrumented-structural",
2179        );
2180        assert_eq!(instrumented.status, original.status);
2181        assert_eq!(instrumented.stdout, original.stdout);
2182        assert_eq!(instrumented.stderr, original.stderr);
2183    }
2184
2185    #[test]
2186    fn cfg_gated_sibling_blocks_keep_their_tail_position() {
2187        // memchr's is_available returns bool from one of two cfg-gated blocks.
2188        // A probe PREPENDED to the second block sits between the siblings,
2189        // survives the cfg strip, and becomes the new `()` tail -- 32 E0308s
2190        // across the crate. Attributed blocks take the probe inside their
2191        // braces instead, where the same cfg governs both.
2192        let source = r#"pub fn is_available() -> bool {
2193    #[cfg(target_endian = "little")]
2194    {
2195        true
2196    }
2197    #[cfg(not(target_endian = "little"))]
2198    {
2199        false
2200    }
2201}
2202
2203fn main() {
2204    println!("{}", is_available());
2205}
2206"#;
2207        let transformed =
2208            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2209        let original = compile_and_run(source, "cfg-original");
2210        let instrumented = compile_and_run(
2211            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2212            "cfg-instrumented",
2213        );
2214        assert_eq!(instrumented.status, original.status);
2215        assert_eq!(instrumented.stdout, original.stdout);
2216        // The kept block is still probed -- inside its braces.
2217        assert!(
2218            transformed
2219                .code
2220                .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
2221                || transformed
2222                    .code
2223                    .contains("{\ncrate::__supercov_runtime_v1::hit(")
2224        );
2225
2226        // An attributed `let` takes the probe inside its initializer, an
2227        // attributed expression statement inside a block; a `let` without an
2228        // initializer is the one shape left declared.
2229        let attributed_let = r#"fn main() {
2230    #[cfg(target_endian = "little")]
2231    let value = 1;
2232    #[cfg(not(target_endian = "little"))]
2233    let value = 2;
2234    #[cfg(target_endian = "little")]
2235    let borrowed: &String = &String::from("little");
2236    #[cfg(not(target_endian = "little"))]
2237    let borrowed: &String = &String::from("big");
2238    #[cfg(target_endian = "little")]
2239    print!("le ");
2240    #[cfg(not(target_endian = "little"))]
2241    print!("be ");
2242    #[allow(unused_assignments)]
2243    let mut later;
2244    later = value + 1;
2245    println!("{value} {borrowed} {later}");
2246}
2247"#;
2248        let transformed = instrument_rust_source(
2249            "src/main.rs",
2250            attributed_let,
2251            "crate::__supercov_runtime_v1",
2252        )
2253        .unwrap();
2254        let ids = transformed
2255            .manifest
2256            .limitations
2257            .iter()
2258            .filter_map(|limitation| limitation.get("id")?.as_str())
2259            .collect::<BTreeSet<_>>();
2260        // Declared only for `let mut later;`.
2261        assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
2262        assert!(
2263            transformed
2264                .code
2265                .contains("let value =  { crate::__supercov_runtime_v1::hit(")
2266        );
2267        assert!(
2268            transformed
2269                .code
2270                .contains("let borrowed: &String =  { crate::__supercov_runtime_v1::hit(")
2271        );
2272        assert!(
2273            transformed
2274                .code
2275                .contains("] { crate::__supercov_runtime_v1::hit(")
2276        );
2277        let original = compile_and_run(attributed_let, "cfg-let-original");
2278        let instrumented = compile_and_run(
2279            &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2280            "cfg-let-instrumented",
2281        );
2282        assert_eq!(instrumented.status, original.status);
2283        assert_eq!(instrumented.stdout, original.stdout);
2284        assert_eq!(instrumented.stderr, original.stderr);
2285    }
2286
2287    #[test]
2288    fn rejects_non_crate_local_runtime_paths() {
2289        assert_eq!(
2290            instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
2291            Err(RustInstrumenterError::InvalidRuntimePath)
2292        );
2293    }
2294
2295    #[test]
2296    fn rejects_invalid_rust_without_partial_obligations() {
2297        assert!(matches!(
2298            build_rust_manifest("src/lib.rs", "fn broken( {\n"),
2299            Err(RustInstrumenterError::Parse(_))
2300        ));
2301    }
2302}