Skip to main content

supercov_engine/
ruby_instrumenter.rs

1//! Supercov-owned Ruby obligation discovery.
2//!
3//! Prism (Ruby's own parser) supplies syntax and exact byte ranges. Supercov
4//! owns the denominator: every statement, method, decision and branch
5//! obligation is decided here, ahead of the run, from source alone.
6//!
7//! Alongside the shared [`CoverageManifest`] this module emits a *probe plan*
8//! for the stdlib-only Ruby runtime. The runtime reads Ruby's `Coverage`
9//! module for lines, and proves everything else two ways at once, so that
10//! either interpreter generation can pick its own:
11//!
12//! - **Ruby 3.4+** reads line events only. Asking `Coverage` for `branches`
13//!   and `methods` too made every sample rebuild both tables for every loaded
14//!   file (7 ms per sample with 550 files, five samples per test), which was
15//!   80% of the runtime's cost. Instead, the statement that starts a branch
16//!   body or a method body proves the branch, the method and the decision
17//!   outcome it witnesses (the `implied` map), and what has no such statement
18//!   — an else-less or modifier `if`, a ternary, `&.`, a `case` without
19//!   `else`, an empty body — gets a probe. Probes also prove what `Coverage`
20//!   never sees: the operands of `&&`/`||` for MC/DC, `||=`, loop entry,
21//!   `rescue` flow and a second statement on a line.
22//! - **Ruby 3.3** cannot apply the insertions (it does not cover code compiled
23//!   by a load hook), so it keeps asking `Coverage` for branches and methods
24//!   and proves those obligations by matching its keys in the untouched
25//!   source; the probe-only remainder is declared unmeasured.
26//!
27//! No insertion contains a newline, so line numbers, backtraces and the
28//! stdlib line table stay exact.
29
30use std::collections::{BTreeMap, BTreeSet};
31
32use ruby_prism::{
33    AndNode, BeginNode, CallNode, CaseMatchNode, CaseNode, DefNode, ForNode, IfNode, Location,
34    Node, OrNode, RescueModifierNode, RescueNode, StatementsNode, UnlessNode, UntilNode, Visit,
35    WhileNode,
36};
37use serde::{Deserialize, Serialize};
38use serde_json::json;
39use sha2::{Digest, Sha256};
40
41use crate::{
42    coverage_analysis::PointKind,
43    coverage_report::{
44        BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
45    },
46};
47
48pub const RUBY_PROBE_PLAN_VERSION: u32 = 1;
49/// Global the runtime binds its probe receiver to. Chosen to be unpronounceable
50/// in application code.
51pub const RUBY_PROBE_RECEIVER: &str = "$__supercov";
52
53/// Block-taking methods whose block runs once per element (or per count):
54/// the idiomatic Ruby loops. Methods that may call the block zero times on a
55/// non-empty receiver (`cycle`, `loop`, `lazy`) are deliberately absent.
56const ITERATORS: &[&[u8]] = &[
57    b"each",
58    b"each_with_index",
59    b"each_with_object",
60    b"each_pair",
61    b"each_key",
62    b"each_value",
63    b"each_char",
64    b"each_byte",
65    b"each_line",
66    b"each_slice",
67    b"each_cons",
68    b"each_entry",
69    b"each_index",
70    b"reverse_each",
71    b"map",
72    b"collect",
73    b"flat_map",
74    b"collect_concat",
75    b"filter_map",
76    b"select",
77    b"filter",
78    b"reject",
79    b"find",
80    b"detect",
81    b"find_index",
82    b"find_all",
83    b"all?",
84    b"any?",
85    b"none?",
86    b"one?",
87    b"count",
88    b"sum",
89    b"min_by",
90    b"max_by",
91    b"sort_by",
92    b"group_by",
93    b"partition",
94    b"inject",
95    b"reduce",
96    b"take_while",
97    b"drop_while",
98    b"times",
99    b"upto",
100    b"downto",
101    b"step",
102];
103pub const BEGIN_BODY_LIMITATION: &str = "ruby-begin-completion-unmeasured";
104/// A `Ractor.new` block cannot call the probes: a non-main Ractor cannot read
105/// the receiver global, so a probe there raises where the untouched program
106/// ran. Nothing is inserted inside one; what only a probe could prove there
107/// is unmeasured and declared at the block.
108pub const RACTOR_BLOCK_LIMITATION: &str = "ruby-ractor-block-unprobed";
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum RubyInstrumenterError {
112    Parse(String),
113    InvalidRange,
114}
115
116impl std::fmt::Display for RubyInstrumenterError {
117    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::Parse(error) => write!(formatter, "Ruby parse failed: {error}"),
120            Self::InvalidRange => write!(formatter, "Ruby parser returned an invalid range"),
121        }
122    }
123}
124
125impl std::error::Error for RubyInstrumenterError {}
126
127/// A source span in one-based lines and zero-based byte columns, the units
128/// Ruby's `Coverage` module reports. Serialized as `[[line, col], [line, col]]`.
129/// What a stdlib key's span names.
130#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "camelCase")]
132pub enum KeyKind {
133    /// A statement list (`then`, `else`, `when`, `in`, a loop body): Ruby
134    /// spans it from its first statement's start to its last statement's
135    /// end, so a probe or wrapper on its first statement becomes part of it.
136    List,
137    /// One expression node: a wrapper around it is a different node.
138    Node,
139    /// A zero-width position at the end of a predicate, which Ruby reports
140    /// for an `if` without a body; it follows anything inserted up to there.
141    Point,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
145#[serde(from = "[[usize; 2]; 2]", into = "[[usize; 2]; 2]")]
146pub struct PlanSpan {
147    pub start: [usize; 2],
148    pub end: [usize; 2],
149}
150
151impl From<[[usize; 2]; 2]> for PlanSpan {
152    fn from(value: [[usize; 2]; 2]) -> Self {
153        Self {
154            start: value[0],
155            end: value[1],
156        }
157    }
158}
159
160impl From<PlanSpan> for [[usize; 2]; 2] {
161    fn from(value: PlanSpan) -> Self {
162        [value.start, value.end]
163    }
164}
165
166/// One text insertion the runtime applies to the original source before
167/// compiling it. Offsets are bytes into the original file; the runtime applies
168/// insertions from the end of the file backwards, so offsets stay valid.
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct Edit {
172    pub offset: usize,
173    pub text: String,
174    /// `clause`, `statement`, `opener` or `closer`: how the insertion sits
175    /// relative to the node at its offset, which decides whether keys
176    /// starting or ending there move (see [`Collector::shifted`]).
177    pub rank: String,
178    /// The other end of the range the insertion belongs to: the end of the
179    /// wrapped node or probed statement for an opener or probe, the opener's
180    /// offset for a closer.
181    pub scope: usize,
182}
183
184/// A `Coverage` branch key: the group type (`if`, `case`, `&.`, `while`), the
185/// branch type (`then`, `else`, `when`, `in`, `body`) and the branch span in
186/// post-insertion coordinates.
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct StdlibKey {
190    pub group: String,
191    pub branch: String,
192    /// What the key's span names, which decides how insertions at its edges
193    /// move it (see [`Collector::shifted`]).
194    pub kind: KeyKind,
195    /// Span after the plan's insertions are applied.
196    pub span: PlanSpan,
197    /// Span in the untouched source, for interpreters that cannot apply the
198    /// insertions (Ruby 3.3 does not cover code compiled by a load hook).
199    pub unshifted: PlanSpan,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase", deny_unknown_fields)]
204pub struct StdlibDecision {
205    pub id: String,
206    pub value: bool,
207    pub outcome: String,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase", deny_unknown_fields)]
212pub struct BranchKeyPlan {
213    pub key: StdlibKey,
214    /// Obligation IDs proven when this branch executed: alternatives and any
215    /// statement whose first line is shared with an earlier statement.
216    pub hits: Vec<String>,
217    /// A single-condition decision whose vector this branch witnesses.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub decision: Option<StdlibDecision>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224pub struct MethodKeyPlan {
225    pub span: PlanSpan,
226    pub unshifted: PlanSpan,
227    pub id: String,
228}
229
230/// What observing one obligation also proves: the branch body it starts, the
231/// method it opens, the single-condition decision outcome it witnesses. Keyed
232/// by the statement (or decision outcome) whose hit implies the rest, so the
233/// runtime pays one hash lookup per first sighting and nothing per execution.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
235#[serde(rename_all = "camelCase", deny_unknown_fields)]
236pub struct ImpliedPlan {
237    #[serde(default, skip_serializing_if = "Vec::is_empty")]
238    pub hits: Vec<String>,
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub decisions: Vec<StdlibDecision>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase", deny_unknown_fields)]
245pub struct CaseClausePlan {
246    pub key: StdlibKey,
247    pub missed: String,
248    pub selected: String,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase", deny_unknown_fields)]
253pub struct CaseNoMatchPlan {
254    pub key: StdlibKey,
255    pub matched: String,
256    pub unmatched: String,
257}
258
259/// `case` clauses are tested in order, so a clause was missed exactly when a
260/// later clause (or the implicit else) was selected. The runtime derives that
261/// per phase from the selected counts.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase", deny_unknown_fields)]
264pub struct CasePlan {
265    pub clauses: Vec<CaseClausePlan>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub no_match: Option<CaseNoMatchPlan>,
268}
269
270/// Short-circuit structure of a decision. Leaves are condition indexes.
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(untagged)]
273pub enum ConditionTree {
274    Leaf(usize),
275    Node {
276        op: String,
277        items: Vec<ConditionTree>,
278        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
279        negate: bool,
280    },
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase", deny_unknown_fields)]
285pub struct DerivedLogical {
286    pub previous_leaves: Vec<usize>,
287    pub operand_leaves: Vec<usize>,
288    pub short_circuit: String,
289    pub evaluated: String,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase", deny_unknown_fields)]
294pub struct LoopTarget {
295    pub id: String,
296    pub zero: String,
297    pub entered: String,
298    /// `until` enters the body when the predicate is falsy.
299    pub until: bool,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304pub struct HandlerTarget {
305    pub id: String,
306    pub missed: String,
307    pub selected: String,
308}
309
310/// What a probe call reports. The runtime looks the key up and records the
311/// obligations named here.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
314pub enum ProbeTarget {
315    /// `s(k)`: a statement sharing a line with an earlier statement ran.
316    #[serde(rename_all = "camelCase")]
317    Statement { id: String },
318    /// `c(k, i, v)` per condition, `d(k, v)` or `w(k, v)` for the outcome.
319    #[serde(rename_all = "camelCase")]
320    Decision {
321        id: String,
322        width: usize,
323        not: Vec<bool>,
324        tree: ConditionTree,
325        outcome_true: String,
326        outcome_false: String,
327        logical: Vec<DerivedLogical>,
328        #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
329        loop_: Option<LoopTarget>,
330    },
331    /// `f(k, collection)` at the loop head and `fb(k)` as the first body statement.
332    #[serde(rename_all = "camelCase")]
333    For {
334        id: String,
335        zero: String,
336        entered: String,
337    },
338    /// `l(k, left)` for value-context `&&`/`||`/`||=`/`&&=`.
339    #[serde(rename_all = "camelCase")]
340    Logical {
341        op: String,
342        short_circuit: String,
343        evaluated: String,
344    },
345    /// `pre(k)` before an operator assignment whose target cannot be re-read
346    /// without side effects, `es(k)` as the first thing its right side does:
347    /// arrivals that never started the right side are short-circuits.
348    #[serde(rename_all = "camelCase")]
349    Arrival {
350        short_circuit: String,
351        evaluated: String,
352    },
353    /// `n(k, receiver)` before `&.`: the receiver was nil, or the method was
354    /// called.
355    #[serde(rename_all = "camelCase")]
356    SafeNavigation { nil: String, called: String },
357    /// `hs(k)` where a branch body or method body would start when it has no
358    /// statement to observe: the alternatives (and method) it proves.
359    #[serde(rename_all = "camelCase")]
360    Hits { ids: Vec<String> },
361    /// `ok(k, v)`/`ok0(k)` completion, `h(k, n)` handler entry, `p(k)`
362    /// propagation, `hm(k, v)` rescue-modifier fallback.
363    #[serde(rename_all = "camelCase")]
364    Try {
365        id: String,
366        success: String,
367        raised: String,
368        handlers: Vec<HandlerTarget>,
369    },
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
373#[serde(rename_all = "camelCase", deny_unknown_fields)]
374pub struct RubyFilePlan {
375    pub edits: Vec<Edit>,
376    /// Line -> statement id for statements that own their first line.
377    pub lines: BTreeMap<usize, String>,
378    /// Statement id -> [start, end] byte offsets for line-owned statements,
379    /// so the runtime can probe one whose first line Ruby turns out not to
380    /// count (`begin`, `case` without subject, multi-line literals).
381    pub statement_offsets: BTreeMap<String, [usize; 2]>,
382    pub branches: Vec<BranchKeyPlan>,
383    pub methods: Vec<MethodKeyPlan>,
384    pub cases: Vec<CasePlan>,
385    /// Obligation id -> what its observation implies (Ruby 3.4+ path).
386    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
387    pub implied: BTreeMap<String, ImpliedPlan>,
388    /// This file's share of [`RubyProbePlan::probe_obligations`], so a file
389    /// the runtime fails to instrument can declare exactly its own.
390    #[serde(default)]
391    pub probe_obligations: Vec<String>,
392    /// Byte ranges of `Ractor.new` blocks, where the runtime must not add
393    /// load-time probes either (see [`RACTOR_BLOCK_LIMITATION`]).
394    #[serde(default, skip_serializing_if = "Vec::is_empty")]
395    pub ractor_blocks: Vec<[usize; 2]>,
396    /// Byte ranges of `refine` blocks. A refinement is active inside its own
397    /// block, so an operator the block refines -- `Hash#[]=` among them --
398    /// is not the one a table probe means; statements there take the
399    /// method-call probe instead.
400    #[serde(default, skip_serializing_if = "Vec::is_empty")]
401    pub refine_blocks: Vec<[usize; 2]>,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "camelCase", deny_unknown_fields)]
406pub struct RubyProbePlan {
407    pub version: u32,
408    pub root: String,
409    pub receiver: String,
410    pub files: BTreeMap<String, RubyFilePlan>,
411    pub probes: BTreeMap<u64, ProbeTarget>,
412    /// See [`RubyProbePlan::probe_obligations`]; stored so the runtime can
413    /// declare them without re-deriving alternative ids.
414    #[serde(default)]
415    pub probe_obligations: Vec<String>,
416}
417
418impl RubyProbePlan {
419    /// Manifest obligations (points, decisions, branches) that only a probe
420    /// can prove: the union of each file's, which already leaves out what
421    /// Ruby 3.3 still proves through Coverage's keys. An interpreter that
422    /// cannot apply the insertions reports them as unmeasured.
423    pub fn probe_obligations(&self) -> Vec<String> {
424        let mut ids = self
425            .files
426            .values()
427            .flat_map(|file| file.probe_obligations.iter().cloned())
428            .collect::<Vec<_>>();
429        ids.sort();
430        ids.dedup();
431        ids
432    }
433}
434
435/// Manifest obligations (points, decisions, branches) that only a probe can
436/// prove.
437fn probe_obligations_of(probes: &BTreeMap<u64, ProbeTarget>) -> Vec<String> {
438    {
439        let mut ids = BTreeSet::new();
440        for target in probes.values() {
441            match target {
442                ProbeTarget::Statement { id } => {
443                    ids.insert(id.clone());
444                }
445                ProbeTarget::Decision {
446                    id,
447                    outcome_true,
448                    logical,
449                    loop_,
450                    ..
451                } => {
452                    ids.insert(id.clone());
453                    ids.insert(branch_of(outcome_true));
454                    for derived in logical {
455                        ids.insert(branch_of(&derived.short_circuit));
456                    }
457                    if let Some(loop_) = loop_ {
458                        ids.insert(loop_.id.clone());
459                    }
460                }
461                ProbeTarget::For { id, .. } => {
462                    ids.insert(id.clone());
463                }
464                ProbeTarget::Logical { short_circuit, .. } => {
465                    ids.insert(branch_of(short_circuit));
466                }
467                ProbeTarget::Arrival { short_circuit, .. } => {
468                    ids.insert(branch_of(short_circuit));
469                }
470                ProbeTarget::SafeNavigation { nil, .. } => {
471                    ids.insert(branch_of(nil));
472                }
473                ProbeTarget::Hits { ids: proven } => {
474                    for id in proven {
475                        ids.insert(obligation_of(id));
476                    }
477                }
478                ProbeTarget::Try { id, handlers, .. } => {
479                    ids.insert(id.clone());
480                    for handler in handlers {
481                        ids.insert(handler.id.clone());
482                    }
483                }
484            }
485        }
486        ids.into_iter().collect()
487    }
488}
489
490/// The probe keys an insertion's text calls: every `<receiver>.<name>(<key>`.
491fn probe_keys_in(text: &str) -> Vec<u64> {
492    let mut keys = Vec::new();
493    let mut rest = text;
494    while let Some(position) = rest.find(RUBY_PROBE_RECEIVER) {
495        rest = &rest[position + RUBY_PROBE_RECEIVER.len()..];
496        let Some(open) = rest.find('(') else { break };
497        let digits: String = rest[open + 1..]
498            .chars()
499            .take_while(char::is_ascii_digit)
500            .collect();
501        if let Ok(key) = digits.parse() {
502            keys.push(key);
503        }
504        rest = &rest[open + 1..];
505    }
506    keys
507}
508
509/// Every id a probe target names, so implications keyed by any of them can
510/// be found when the probe is dropped.
511fn target_ids(target: &ProbeTarget) -> Vec<String> {
512    match target {
513        ProbeTarget::Statement { id } => vec![id.clone()],
514        ProbeTarget::Decision {
515            id,
516            outcome_true,
517            outcome_false,
518            logical,
519            loop_,
520            ..
521        } => {
522            let mut ids = vec![id.clone(), outcome_true.clone(), outcome_false.clone()];
523            for derived in logical {
524                ids.push(derived.short_circuit.clone());
525                ids.push(derived.evaluated.clone());
526            }
527            if let Some(loop_) = loop_ {
528                ids.push(loop_.zero.clone());
529                ids.push(loop_.entered.clone());
530            }
531            ids
532        }
533        ProbeTarget::For { id, zero, entered } => vec![id.clone(), zero.clone(), entered.clone()],
534        ProbeTarget::Logical {
535            short_circuit,
536            evaluated,
537            ..
538        }
539        | ProbeTarget::Arrival {
540            short_circuit,
541            evaluated,
542        } => vec![short_circuit.clone(), evaluated.clone()],
543        ProbeTarget::Try {
544            id,
545            success,
546            raised,
547            handlers,
548        } => {
549            let mut ids = vec![id.clone(), success.clone(), raised.clone()];
550            for handler in handlers {
551                ids.push(handler.id.clone());
552                ids.push(handler.missed.clone());
553                ids.push(handler.selected.clone());
554            }
555            ids
556        }
557        ProbeTarget::SafeNavigation { nil, called } => vec![nil.clone(), called.clone()],
558        ProbeTarget::Hits { ids } => ids.clone(),
559    }
560}
561
562/// `rb:branch:<hash>:alternative` -> `rb:branch:<hash>`; decision outcome
563/// branches are `rb:decision:<hash>:outcome`.
564fn branch_of(alternative: &str) -> String {
565    alternative
566        .rsplit_once(':')
567        .map(|(branch, _)| branch.to_owned())
568        .unwrap_or_else(|| alternative.to_owned())
569}
570
571/// The manifest obligation an id names: a point (`rb:statement:<hash>`,
572/// `rb:function:<hash>`) is its own obligation; an alternative
573/// (`rb:branch:<hash>:selected`, `rb:decision:<hash>:outcome:true`) belongs
574/// to its branch.
575fn obligation_of(id: &str) -> String {
576    if id.matches(':').count() >= 3 {
577        branch_of(id)
578    } else {
579        id.to_owned()
580    }
581}
582
583/// Everything Ruby 3.3 proves by matching `Coverage`'s branch and method
584/// keys. A probe that proves one of these on 3.4+ is not a gap on 3.3.
585fn stdlib_provable(
586    branches: &[BranchKeyPlan],
587    cases: &[CasePlan],
588    methods: &[MethodKeyPlan],
589) -> BTreeSet<String> {
590    let mut ids = BTreeSet::new();
591    for branch in branches {
592        for hit in &branch.hits {
593            ids.insert(obligation_of(hit));
594        }
595        if let Some(decision) = &branch.decision {
596            ids.insert(decision.id.clone());
597            ids.insert(obligation_of(&decision.outcome));
598        }
599    }
600    for case in cases {
601        for clause in &case.clauses {
602            ids.insert(obligation_of(&clause.missed));
603            ids.insert(obligation_of(&clause.selected));
604        }
605        if let Some(no_match) = &case.no_match {
606            ids.insert(obligation_of(&no_match.matched));
607            ids.insert(obligation_of(&no_match.unmatched));
608        }
609    }
610    for method in methods {
611        ids.insert(method.id.clone());
612    }
613    ids
614}
615
616#[derive(Debug, Clone, PartialEq)]
617pub struct RubyFileObligations {
618    pub manifest: CoverageManifest,
619    pub plan: RubyFilePlan,
620    pub probes: BTreeMap<u64, ProbeTarget>,
621}
622
623fn stable_id(file: &str, kind: &str, start: usize, end: usize, suffix: &str) -> String {
624    let mut hash = Sha256::new();
625    for value in [file, kind, &start.to_string(), &end.to_string(), suffix] {
626        hash.update(value.as_bytes());
627        hash.update([0]);
628    }
629    let digest = hash.finalize();
630    let mut encoded = String::with_capacity(24);
631    for byte in &digest[..12] {
632        use std::fmt::Write as _;
633        write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
634    }
635    format!("rb:{kind}:{encoded}")
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
639enum EditRank {
640    Clause,
641    StatementProbe,
642    Opener,
643    Closer,
644}
645
646#[derive(Debug, Clone)]
647struct PendingEdit {
648    offset: usize,
649    rank: EditRank,
650    /// Openers sort by ascending depth (outer first), closers by descending.
651    order: i64,
652    sequence: usize,
653    text: String,
654    scope: usize,
655}
656
657/// How a statement that a stdlib branch key proves on Ruby 3.3 is proven on
658/// 3.4+, where no keys are read.
659#[derive(Debug, Clone)]
660enum KeyProof {
661    /// Like any statement: its own line when it owns one, else a probe.
662    Probe,
663    /// The body of a modifier (`x if c`, `x while c`): it runs exactly when
664    /// this decision outcome or loop alternative is observed, and its line
665    /// belongs to the modifier statement.
666    Implied(String),
667}
668
669#[derive(Debug, Clone)]
670struct KeyStatement {
671    index: usize,
672    proof: KeyProof,
673}
674
675/// The body statements whose execution witnesses each outcome of a
676/// single-condition decision. Both sides present: the decision is derived
677/// from lines; either side missing: the predicate is probed.
678#[derive(Debug, Default)]
679struct DecisionProof {
680    true_statements: Vec<String>,
681    false_statements: Vec<String>,
682}
683
684struct Collector<'a> {
685    file: &'a str,
686    source: &'a [u8],
687    line_starts: Vec<usize>,
688    manifest: CoverageManifest,
689    lines: BTreeMap<usize, String>,
690    statement_offsets: BTreeMap<String, [usize; 2]>,
691    branches: Vec<BranchKeyPlan>,
692    methods: Vec<MethodKeyPlan>,
693    cases: Vec<CasePlan>,
694    implied: BTreeMap<String, ImpliedPlan>,
695    probes: BTreeMap<u64, ProbeTarget>,
696    edits: Vec<PendingEdit>,
697    next_probe: &'a mut u64,
698    point_ids: BTreeSet<String>,
699    decision_ids: BTreeSet<String>,
700    branch_ids: BTreeSet<String>,
701    claimed_lines: BTreeSet<usize>,
702    /// Statement start offsets a stdlib branch key proves on Ruby 3.3 (index
703    /// into `branches`), and how 3.4+ proves them instead.
704    key_statements: BTreeMap<usize, KeyStatement>,
705    /// Start offsets of the body expressions of endless method definitions,
706    /// which take a wrapped probe because `def m = s(k); expr` would end the
707    /// definition at the probe.
708    endless_bodies: std::collections::BTreeSet<usize>,
709    /// Offsets of `&&`/`||` nodes that belong to a decision's tree.
710    tree_logicals: BTreeSet<usize>,
711    /// Statement lists that are expressions in disguise (parentheses, string
712    /// interpolation): their children are not statements in the denominator.
713    expression_lists: BTreeSet<usize>,
714    /// `if`/`unless` nodes that are `case/in` guards, owned by the clause.
715    guard_nodes: BTreeSet<usize>,
716    /// `elsif` nodes already handled through their parent's chain walk.
717    elsif_nodes: BTreeSet<usize>,
718    depth: i64,
719    begin_unmeasured: Vec<(String, usize)>,
720    /// Byte ranges of `Ractor.new` blocks; no insertion may land inside.
721    ractor_blocks: Vec<(usize, usize)>,
722    /// Byte ranges of `refine` blocks (see [`RubyFilePlan::refine_blocks`]).
723    refine_blocks: Vec<[usize; 2]>,
724    error: Option<RubyInstrumenterError>,
725}
726
727impl<'a> Collector<'a> {
728    fn new(file: &'a str, source: &'a [u8], next_probe: &'a mut u64) -> Self {
729        let mut line_starts = vec![0];
730        line_starts.extend(
731            source
732                .iter()
733                .enumerate()
734                .filter_map(|(index, byte)| (*byte == b'\n').then_some(index + 1)),
735        );
736        Self {
737            file,
738            source,
739            line_starts,
740            manifest: CoverageManifest {
741                unmeasured: Vec::new(),
742                decisions: Vec::new(),
743                points: Vec::new(),
744                branches: Vec::new(),
745                limitations: Vec::new(),
746                scope: None,
747                assertion_sites: Vec::new(),
748            },
749            lines: BTreeMap::new(),
750            statement_offsets: BTreeMap::new(),
751            branches: Vec::new(),
752            methods: Vec::new(),
753            cases: Vec::new(),
754            implied: BTreeMap::new(),
755            probes: BTreeMap::new(),
756            edits: Vec::new(),
757            next_probe,
758            point_ids: BTreeSet::new(),
759            decision_ids: BTreeSet::new(),
760            branch_ids: BTreeSet::new(),
761            claimed_lines: BTreeSet::new(),
762            key_statements: BTreeMap::new(),
763            endless_bodies: std::collections::BTreeSet::new(),
764            tree_logicals: BTreeSet::new(),
765            expression_lists: BTreeSet::new(),
766            guard_nodes: BTreeSet::new(),
767            elsif_nodes: BTreeSet::new(),
768            depth: 0,
769            begin_unmeasured: Vec::new(),
770            ractor_blocks: Vec::new(),
771            refine_blocks: Vec::new(),
772            error: None,
773        }
774    }
775
776    // -- positions ----------------------------------------------------------
777
778    fn line_column(&self, offset: usize) -> (usize, usize) {
779        let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
780        (line_index + 1, offset - self.line_starts[line_index])
781    }
782
783    fn span(&self, start: usize, end: usize) -> PlanSpan {
784        let (start_line, start_column) = self.line_column(start);
785        let (end_line, end_column) = self.line_column(end);
786        PlanSpan {
787            start: [start_line, start_column],
788            end: [end_line, end_column],
789        }
790    }
791
792    fn point_span(&self, offset: usize) -> PlanSpan {
793        let (line, column) = self.line_column(offset);
794        PlanSpan {
795            start: [line, column],
796            end: [line, column],
797        }
798    }
799
800    fn location_span(&self, location: &Location<'_>) -> PlanSpan {
801        self.span(location.start_offset(), location.end_offset())
802    }
803
804    fn node_span(&self, node: &Node<'_>) -> PlanSpan {
805        self.location_span(&node.location())
806    }
807
808    fn text(&self, start: usize, end: usize) -> String {
809        String::from_utf8_lossy(&self.source[start.min(end)..end.min(self.source.len())])
810            .trim()
811            .to_owned()
812    }
813
814    fn statements_span(&self, statements: &Option<StatementsNode<'_>>) -> Option<PlanSpan> {
815        statements
816            .as_ref()
817            .map(|statements| self.location_span(&statements.location()))
818    }
819
820    // -- edits --------------------------------------------------------------
821
822    fn edit(&mut self, offset: usize, rank: EditRank, text: String, scope: usize) {
823        debug_assert!(!text.contains('\n'));
824        let order = match rank {
825            EditRank::Opener => self.depth,
826            EditRank::Closer => -self.depth,
827            _ => 0,
828        };
829        let sequence = self.edits.len();
830        self.edits.push(PendingEdit {
831            offset,
832            rank,
833            order,
834            sequence,
835            text,
836            scope,
837        });
838    }
839
840    fn probe_key(&mut self, target: ProbeTarget) -> u64 {
841        let key = *self.next_probe;
842        *self.next_probe += 1;
843        self.probes.insert(key, target);
844        key
845    }
846
847    fn wrap(&mut self, start: usize, end: usize, opener: String) {
848        self.edit(start, EditRank::Opener, opener, end);
849        self.edit(end, EditRank::Closer, "))".into(), start);
850    }
851
852    // -- manifest helpers ---------------------------------------------------
853
854    fn push_point(
855        &mut self,
856        id: &str,
857        start: usize,
858        end: usize,
859        kind: PointKind,
860        label: Option<String>,
861    ) {
862        let (line, column) = self.line_column(start);
863        self.manifest.points.push(PointMeta {
864            id: id.into(),
865            kind,
866            file: self.file.into(),
867            line,
868            column,
869            source: self.text(start, end),
870            label,
871        });
872    }
873
874    fn branch<const N: usize>(
875        &mut self,
876        start: usize,
877        end: usize,
878        kind: &str,
879        alternatives: [(&str, &str); N],
880    ) -> Option<String> {
881        let id = stable_id(self.file, "branch", start, end, kind);
882        let source = self.text(start, end);
883        self.branch_with_id(id, start, end, kind, source, alternatives)
884    }
885
886    fn branch_with_id<const N: usize>(
887        &mut self,
888        id: String,
889        start: usize,
890        _end: usize,
891        kind: &str,
892        source: String,
893        alternatives: [(&str, &str); N],
894    ) -> Option<String> {
895        if !self.branch_ids.insert(id.clone()) {
896            return None;
897        }
898        let (line, column) = self.line_column(start);
899        self.manifest.branches.push(BranchMeta {
900            id: id.clone(),
901            kind: kind.into(),
902            file: self.file.into(),
903            line,
904            column,
905            source,
906            alternatives: alternatives
907                .into_iter()
908                .map(|(suffix, label)| BranchAlternativeMeta {
909                    id: format!("{id}:{suffix}"),
910                    label: label.into(),
911                })
912                .collect(),
913        });
914        Some(id)
915    }
916
917    fn stdlib(
918        &mut self,
919        group: &str,
920        branch: &str,
921        span: PlanSpan,
922        kind: KeyKind,
923        hits: Vec<String>,
924    ) -> usize {
925        self.branches.push(BranchKeyPlan {
926            key: StdlibKey {
927                group: group.into(),
928                branch: branch.into(),
929                kind,
930                span,
931                unshifted: span,
932            },
933            hits,
934            decision: None,
935        });
936        self.branches.len() - 1
937    }
938
939    // -- statements ---------------------------------------------------------
940
941    fn statements(&mut self, statements: &StatementsNode<'_>) {
942        for statement in statements.body().iter() {
943            self.statement(&statement);
944        }
945    }
946
947    fn statement(&mut self, node: &Node<'_>) {
948        let location = node.location();
949        let (start, end) = (location.start_offset(), location.end_offset());
950        let id = stable_id(self.file, "statement", start, end, "");
951        if !self.point_ids.insert(id.clone()) {
952            return;
953        }
954        self.push_point(&id, start, end, PointKind::Statement, None);
955        let (line, _) = self.line_column(start);
956        if let Some(key) = self.key_statements.get(&start).cloned() {
957            // The body of a modifier or one-line branch: on Ruby 3.3 the
958            // stdlib branch key that proves the branch proves this statement.
959            self.branches[key.index].hits.push(id.clone());
960            if let KeyProof::Implied(by) = key.proof {
961                // A modifier body shares its line with the modifier statement,
962                // so the line proves nothing about it; the decision outcome or
963                // loop entry that runs it does.
964                self.claimed_lines.insert(line);
965                self.implied.entry(by).or_default().hits.push(id);
966                return;
967            }
968        }
969        if self.needs_probe(node) {
970            // No instruction carries this statement's first line: `x = begin`
971            // starts executing inside the begin body.
972            self.claimed_lines.insert(line);
973            let key = self.probe_key(ProbeTarget::Statement { id });
974            self.statement_probe(start, end, key);
975        } else if self.claimed_lines.insert(line) {
976            // Ruby's line table counts the statement's first line; the
977            // offsets let the runtime probe it instead where the interpreter
978            // turns out not to count that line.
979            self.lines.insert(line, id.clone());
980            self.statement_offsets.insert(id, [start, end]);
981        } else {
982            let key = self.probe_key(ProbeTarget::Statement { id });
983            self.statement_probe(start, end, key);
984        }
985    }
986
987    /// `s(k); statement`, or `(s(k); expression)` for the body of an endless
988    /// method definition, which admits exactly one expression.
989    fn statement_probe(&mut self, start: usize, end: usize, key: u64) {
990        if self.endless_bodies.contains(&start) {
991            self.depth += 1;
992            self.edit(
993                start,
994                EditRank::Opener,
995                format!("({RUBY_PROBE_RECEIVER}.s({key}); "),
996                end,
997            );
998            self.edit(end, EditRank::Closer, ")".into(), start);
999            self.depth -= 1;
1000        } else {
1001            self.edit(
1002                start,
1003                EditRank::StatementProbe,
1004                format!("{RUBY_PROBE_RECEIVER}.s({key}); "),
1005                end,
1006            );
1007        }
1008    }
1009
1010    fn needs_probe(&self, node: &Node<'_>) -> bool {
1011        let value = if let Some(write) = node.as_local_variable_write_node() {
1012            Some(write.value())
1013        } else if let Some(write) = node.as_instance_variable_write_node() {
1014            Some(write.value())
1015        } else if let Some(write) = node.as_class_variable_write_node() {
1016            Some(write.value())
1017        } else if let Some(write) = node.as_global_variable_write_node() {
1018            Some(write.value())
1019        } else if let Some(write) = node.as_constant_write_node() {
1020            Some(write.value())
1021        } else {
1022            node.as_multi_write_node().map(|write| write.value())
1023        };
1024        // `x = begin ... end` and `x = (\n ... )` start executing inside the
1025        // value on a later line; Ruby records nothing for the assignment line.
1026        value.is_some_and(|value| {
1027            value
1028                .as_begin_node()
1029                .is_some_and(|begin| begin.begin_keyword_loc().is_some())
1030                || value.as_parentheses_node().is_some()
1031        })
1032    }
1033
1034    /// Register the first statement of a body as proven by a stdlib key.
1035    fn key_body(&mut self, statements: Option<StatementsNode<'_>>, index: usize, proof: KeyProof) {
1036        if let Some(first) = statements.and_then(|statements| statements.body().iter().next()) {
1037            self.key_statements.insert(
1038                first.location().start_offset(),
1039                KeyStatement { index, proof },
1040            );
1041        }
1042    }
1043
1044    /// A clause header (`when 1 then`, `elsif b`, `else`) runs its test on
1045    /// its own line whether or not its body follows, so a body statement
1046    /// starting on that line proves nothing by the line and takes a probe.
1047    fn claim_line_at(&mut self, offset: usize) {
1048        let (line, _) = self.line_column(offset);
1049        self.claimed_lines.insert(line);
1050    }
1051
1052    /// The obligation id of a body's first statement, whose observation
1053    /// (its own line, or a probe when it shares one) witnesses that the body
1054    /// ran. `None` for an empty body.
1055    fn first_statement_id(&self, statements: &Option<StatementsNode<'_>>) -> Option<String> {
1056        let first = statements.as_ref()?.body().iter().next()?;
1057        let location = first.location();
1058        Some(stable_id(
1059            self.file,
1060            "statement",
1061            location.start_offset(),
1062            location.end_offset(),
1063            "",
1064        ))
1065    }
1066
1067    /// Attach what running a body proves to its first statement, or, for a
1068    /// body with no statement, to a `hs(k)` probe inserted where the body
1069    /// would start (`before` and `after` supply the punctuation).
1070    fn body_proves(
1071        &mut self,
1072        statements: &Option<StatementsNode<'_>>,
1073        ids: Vec<String>,
1074        offset: usize,
1075        before: &str,
1076        after: &str,
1077    ) {
1078        match self.first_statement_id(statements) {
1079            Some(first) => self.implied.entry(first).or_default().hits.extend(ids),
1080            None => {
1081                let key = self.probe_key(ProbeTarget::Hits { ids });
1082                // Closer rank: a guard's `d(...)` wrapper closes at the very
1083                // offset an empty `in` body starts, and the probe must follow
1084                // that `))`, not split it.
1085                self.edit(
1086                    offset,
1087                    EditRank::Closer,
1088                    format!("{before}{RUBY_PROBE_RECEIVER}.hs({key}){after}"),
1089                    offset,
1090                );
1091            }
1092        }
1093    }
1094
1095    // -- decisions ----------------------------------------------------------
1096
1097    /// Strip parentheses and `!`/`not`, counting the negations.
1098    fn strip<'n>(&self, node: Node<'n>) -> (Node<'n>, usize) {
1099        let mut current = node;
1100        let mut not = 0;
1101        loop {
1102            if let Some(parens) = current.as_parentheses_node() {
1103                if !parens.is_multiple_statements()
1104                    && let Some(body) = parens.body()
1105                    && let Some(statements) = body.as_statements_node()
1106                    && statements.body().iter().count() == 1
1107                    && let Some(inner) = statements.body().iter().next()
1108                {
1109                    current = inner;
1110                    continue;
1111                }
1112                break;
1113            }
1114            if let Some(call) = current.as_call_node()
1115                && call.name().as_slice() == b"!"
1116                && call.arguments().is_none()
1117                && call.block().is_none()
1118                && let Some(receiver) = call.receiver()
1119            {
1120                not += 1;
1121                current = receiver;
1122                continue;
1123            }
1124            break;
1125        }
1126        (current, not)
1127    }
1128
1129    fn tree(
1130        &mut self,
1131        node: Node<'_>,
1132        leaves: &mut Vec<(usize, usize, usize)>,
1133        logicals: &mut Vec<(String, Vec<Vec<usize>>)>,
1134    ) -> ConditionTree {
1135        let (operand, not) = self.strip(node);
1136        let logical: Option<(&str, Node<'_>, Node<'_>, usize)> =
1137            if let Some(and) = operand.as_and_node() {
1138                Some((
1139                    "and",
1140                    and.left(),
1141                    and.right(),
1142                    operand.location().start_offset(),
1143                ))
1144            } else if let Some(or) = operand.as_or_node() {
1145                Some((
1146                    "or",
1147                    or.left(),
1148                    or.right(),
1149                    operand.location().start_offset(),
1150                ))
1151            } else {
1152                None
1153            };
1154        if let Some((op, left, right, offset)) = logical {
1155            self.tree_logicals.insert(offset);
1156            let first = leaves.len();
1157            let left_tree = self.tree(left, leaves, logicals);
1158            let middle = leaves.len();
1159            let right_tree = self.tree(right, leaves, logicals);
1160            logicals.push((
1161                op.into(),
1162                vec![(first..middle).collect(), (middle..leaves.len()).collect()],
1163            ));
1164            return ConditionTree::Node {
1165                op: op.into(),
1166                items: vec![left_tree, right_tree],
1167                negate: not % 2 == 1,
1168            };
1169        }
1170        let location = operand.location();
1171        leaves.push((location.start_offset(), location.end_offset(), not));
1172        ConditionTree::Leaf(leaves.len() - 1)
1173    }
1174
1175    /// A decision proven by probes: multi-condition predicates, loop
1176    /// predicates and pattern guards. Returns the probe key the outcome
1177    /// wrapper must use.
1178    fn probe_decision(
1179        &mut self,
1180        predicate: Node<'_>,
1181        kind: &str,
1182        loop_: Option<LoopTarget>,
1183        wrapper: &str,
1184    ) -> Option<u64> {
1185        let location = predicate.location();
1186        let (start, end) = (location.start_offset(), location.end_offset());
1187        let id = stable_id(self.file, "decision", start, end, kind);
1188        if !self.decision_ids.insert(id.clone()) {
1189            return None;
1190        }
1191        let mut leaves = Vec::new();
1192        let mut logicals = Vec::new();
1193        let tree = self.tree(predicate, &mut leaves, &mut logicals);
1194        let conditions = leaves
1195            .iter()
1196            .map(|(leaf_start, leaf_end, not)| {
1197                let text = self.text(*leaf_start, *leaf_end);
1198                if not % 2 == 1 {
1199                    format!("!{text}")
1200                } else {
1201                    text
1202                }
1203            })
1204            .collect::<Vec<_>>();
1205        let (line, column) = self.line_column(start);
1206        let source = self.text(start, end);
1207        self.manifest.decisions.push(DecisionMeta {
1208            id: id.clone(),
1209            file: self.file.into(),
1210            line,
1211            column,
1212            source: source.clone(),
1213            conditions,
1214            kind: kind.into(),
1215        });
1216        let outcome_id = format!("{id}:outcome");
1217        self.branch_with_id(
1218            outcome_id.clone(),
1219            start,
1220            end,
1221            kind,
1222            source,
1223            [("true", "true"), ("false", "false")],
1224        );
1225        let mut derived = Vec::new();
1226        for (op, groups) in logicals {
1227            // The logical branch lives on the right operand's range.
1228            let right_leaf = groups[1].first().copied().unwrap_or(0);
1229            let (right_start, right_end, _) = leaves[right_leaf];
1230            if let Some(branch_id) = self.branch(
1231                right_start,
1232                right_end,
1233                &format!("logical-{op}"),
1234                [
1235                    ("short-circuit", "short-circuited"),
1236                    ("evaluated", "right operand evaluated"),
1237                ],
1238            ) {
1239                derived.push(DerivedLogical {
1240                    previous_leaves: groups[0].clone(),
1241                    operand_leaves: groups[1].clone(),
1242                    short_circuit: format!("{branch_id}:short-circuit"),
1243                    evaluated: format!("{branch_id}:evaluated"),
1244                });
1245            }
1246        }
1247        let key = self.probe_key(ProbeTarget::Decision {
1248            id,
1249            width: leaves.len(),
1250            not: leaves.iter().map(|(_, _, not)| not % 2 == 1).collect(),
1251            tree,
1252            outcome_true: format!("{outcome_id}:true"),
1253            outcome_false: format!("{outcome_id}:false"),
1254            logical: derived,
1255            loop_,
1256        });
1257        self.depth += 1;
1258        self.wrap(
1259            start,
1260            end,
1261            format!("{RUBY_PROBE_RECEIVER}.{wrapper}({key}, ("),
1262        );
1263        self.depth += 1;
1264        // A lone condition's value is the outcome; the runtime reads it from
1265        // the outcome probe, so it needs no wrapper of its own.
1266        if leaves.len() > 1 {
1267            for (index, (leaf_start, leaf_end, _)) in leaves.iter().enumerate() {
1268                self.wrap(
1269                    *leaf_start,
1270                    *leaf_end,
1271                    format!("{RUBY_PROBE_RECEIVER}.c({key}, {index}, ("),
1272                );
1273            }
1274        }
1275        self.depth -= 2;
1276        Some(key)
1277    }
1278
1279    /// A single-condition `if`/`unless`/ternary decision: Ruby's `then`/`else`
1280    /// counts already witness both outcomes, so no probe is inserted.
1281    fn stdlib_decision(
1282        &mut self,
1283        predicate: Node<'_>,
1284        kind: &str,
1285        then_index: usize,
1286        else_index: usize,
1287        then_is_true: bool,
1288        proof: DecisionProof,
1289    ) {
1290        let location = predicate.location();
1291        let (start, end) = (location.start_offset(), location.end_offset());
1292        let id = stable_id(self.file, "decision", start, end, kind);
1293        if !self.decision_ids.insert(id.clone()) {
1294            return;
1295        }
1296        let (operand, not) = self.strip(predicate);
1297        let operand_location = operand.location();
1298        let mut condition = self.text(
1299            operand_location.start_offset(),
1300            operand_location.end_offset(),
1301        );
1302        if not % 2 == 1 {
1303            condition = format!("!{condition}");
1304        }
1305        let (line, column) = self.line_column(start);
1306        let source = self.text(start, end);
1307        self.manifest.decisions.push(DecisionMeta {
1308            id: id.clone(),
1309            file: self.file.into(),
1310            line,
1311            column,
1312            source: source.clone(),
1313            conditions: vec![condition],
1314            kind: kind.into(),
1315        });
1316        let outcome_id = format!("{id}:outcome");
1317        self.branch_with_id(
1318            outcome_id.clone(),
1319            start,
1320            end,
1321            kind,
1322            source,
1323            [("true", "true"), ("false", "false")],
1324        );
1325        let (true_index, false_index) = if then_is_true {
1326            (then_index, else_index)
1327        } else {
1328            (else_index, then_index)
1329        };
1330        self.branches[true_index].decision = Some(StdlibDecision {
1331            id: id.clone(),
1332            value: true,
1333            outcome: format!("{outcome_id}:true"),
1334        });
1335        self.branches[false_index].decision = Some(StdlibDecision {
1336            id: id.clone(),
1337            value: false,
1338            outcome: format!("{outcome_id}:false"),
1339        });
1340        // Ruby 3.4+ reads no branch keys. Both arms have a statement: each
1341        // arm's first statement witnesses its outcome. Otherwise the
1342        // predicate is probed -- one call per evaluation, no condition
1343        // wrapper, because a lone condition's value is the outcome.
1344        if !proof.true_statements.is_empty() && !proof.false_statements.is_empty() {
1345            for (statements, value) in [
1346                (&proof.true_statements, true),
1347                (&proof.false_statements, false),
1348            ] {
1349                for statement in statements {
1350                    self.implied
1351                        .entry(statement.clone())
1352                        .or_default()
1353                        .decisions
1354                        .push(StdlibDecision {
1355                            id: id.clone(),
1356                            value,
1357                            outcome: format!("{outcome_id}:{value}"),
1358                        });
1359                }
1360            }
1361        } else {
1362            let key = self.probe_key(ProbeTarget::Decision {
1363                id,
1364                width: 1,
1365                not: vec![not % 2 == 1],
1366                tree: ConditionTree::Leaf(0),
1367                outcome_true: format!("{outcome_id}:true"),
1368                outcome_false: format!("{outcome_id}:false"),
1369                logical: Vec::new(),
1370                loop_: None,
1371            });
1372            self.depth += 1;
1373            self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.d({key}, ("));
1374            self.depth -= 1;
1375        }
1376    }
1377
1378    /// Outcome ids of a decision before it is registered, so a modifier body
1379    /// can be tied to the outcome that runs it.
1380    fn decision_outcomes(&self, predicate: &Node<'_>, kind: &str) -> (String, String) {
1381        let location = predicate.location();
1382        let id = stable_id(
1383            self.file,
1384            "decision",
1385            location.start_offset(),
1386            location.end_offset(),
1387            kind,
1388        );
1389        (format!("{id}:outcome:true"), format!("{id}:outcome:false"))
1390    }
1391
1392    /// The first statements of every arm after `subsequent` in an `if`
1393    /// chain, which together witness the false outcome of the arm before
1394    /// them; `None` when an arm is empty or the chain has no final `else`.
1395    fn chain_false_statements(&self, subsequent: Option<Node<'_>>) -> Option<Vec<String>> {
1396        let mut ids = Vec::new();
1397        let mut current = subsequent;
1398        loop {
1399            let node = current?;
1400            if let Some(elsif) = node.as_if_node() {
1401                ids.push(self.first_statement_id(&elsif.statements())?);
1402                current = elsif.subsequent();
1403            } else if let Some(else_node) = node.as_else_node() {
1404                ids.push(self.first_statement_id(&else_node.statements())?);
1405                return Some(ids);
1406            } else {
1407                return None;
1408            }
1409        }
1410    }
1411
1412    /// `Some(truthiness)` for a predicate Ruby folds at compile time: a
1413    /// `true`, `false` or `nil` literal, or a numeric, string or symbol
1414    /// literal, possibly parenthesised. Ruby emits neither a branch nor the
1415    /// dead arm for these.
1416    fn literal_truth(&self, predicate: Node<'_>) -> Option<bool> {
1417        let mut node = predicate;
1418        loop {
1419            let inner = node.as_parentheses_node().and_then(|parens| {
1420                let body = parens.body()?;
1421                let statements = body.as_statements_node()?;
1422                let mut iter = statements.body().iter();
1423                let only = iter.next()?;
1424                iter.next().is_none().then_some(only)
1425            });
1426            match inner {
1427                Some(inner) => node = inner,
1428                None => break,
1429            }
1430        }
1431        if node.as_true_node().is_some()
1432            || node.as_integer_node().is_some()
1433            || node.as_float_node().is_some()
1434            || node.as_rational_node().is_some()
1435            || node.as_imaginary_node().is_some()
1436            || node.as_string_node().is_some()
1437            || node.as_symbol_node().is_some()
1438        {
1439            Some(true)
1440        } else if node.as_false_node().is_some() || node.as_nil_node().is_some() {
1441            Some(false)
1442        } else if let Some(and) = node.as_and_node() {
1443            // Ruby folds `x and false` the same way: the then arm is never
1444            // compiled and reports no branch, while `x` still runs.
1445            match (
1446                self.literal_truth(and.left()),
1447                self.literal_truth(and.right()),
1448            ) {
1449                (Some(false), _) | (_, Some(false)) => Some(false),
1450                (Some(true), Some(true)) => Some(true),
1451                _ => None,
1452            }
1453        } else if let Some(or) = node.as_or_node() {
1454            match (
1455                self.literal_truth(or.left()),
1456                self.literal_truth(or.right()),
1457            ) {
1458                (Some(true), _) | (_, Some(true)) => Some(true),
1459                (Some(false), Some(false)) => Some(false),
1460                _ => None,
1461            }
1462        } else {
1463            None
1464        }
1465    }
1466
1467    fn is_compound(&self, predicate: Node<'_>) -> bool {
1468        let (operand, _) = self.strip(predicate);
1469        operand.as_and_node().is_some() || operand.as_or_node().is_some()
1470    }
1471
1472    /// `predicate` is called twice because Prism nodes are handles that
1473    /// cannot be copied; every call returns the same node.
1474    fn predicate_decision<'n>(
1475        &mut self,
1476        predicate: impl Fn() -> Node<'n>,
1477        kind: &str,
1478        then_index: usize,
1479        else_index: usize,
1480        then_is_true: bool,
1481        proof: DecisionProof,
1482    ) {
1483        if self.is_compound(predicate()) {
1484            self.probe_decision(predicate(), kind, None, "d");
1485        } else {
1486            self.stdlib_decision(
1487                predicate(),
1488                kind,
1489                then_index,
1490                else_index,
1491                then_is_true,
1492                proof,
1493            );
1494        }
1495    }
1496
1497    // -- constructs ---------------------------------------------------------
1498
1499    fn if_node(&mut self, node: &IfNode<'_>, kind: &str) {
1500        let location = node.location();
1501        let node_span = self.location_span(&location);
1502        let then_statements = self.statements_span(&node.statements());
1503        // An `if` without a body gets a zero-width key at its predicate's end.
1504        let (then_span, then_kind) = match then_statements {
1505            Some(span) => (span, KeyKind::List),
1506            None => (
1507                self.point_span(node.predicate().location().end_offset()),
1508                KeyKind::Point,
1509            ),
1510        };
1511        let (else_span, else_kind) = match node.subsequent() {
1512            Some(subsequent) => match subsequent.as_else_node() {
1513                Some(else_node) => match self.statements_span(&else_node.statements()) {
1514                    Some(span) => (span, KeyKind::List),
1515                    None => (self.node_span(&subsequent), KeyKind::Node),
1516                },
1517                None => (self.node_span(&subsequent), KeyKind::Node),
1518            },
1519            None => (node_span, KeyKind::Node),
1520        };
1521        let then_index = self.stdlib("if", "then", then_span, then_kind, Vec::new());
1522        let else_index = self.stdlib("if", "else", else_span, else_kind, Vec::new());
1523        if kind == "ternary" {
1524            // A ternary's arms are expressions, not statements.
1525            if let Some(statements) = node.statements() {
1526                self.expression_lists
1527                    .insert(statements.location().start_offset());
1528            }
1529            if let Some(subsequent) = node.subsequent()
1530                && let Some(else_node) = subsequent.as_else_node()
1531                && let Some(statements) = else_node.statements()
1532            {
1533                self.expression_lists
1534                    .insert(statements.location().start_offset());
1535            }
1536        } else {
1537            // `x if c`: the body precedes the keyword and shares its line
1538            // with the modifier statement, so the true outcome proves it.
1539            let modifier = node.statements().is_some_and(|statements| {
1540                statements.location().start_offset() < node.predicate().location().start_offset()
1541            });
1542            let then_proof = if modifier {
1543                KeyProof::Implied(self.decision_outcomes(&node.predicate(), kind).0)
1544            } else {
1545                KeyProof::Probe
1546            };
1547            self.claim_line_at(node.predicate().location().end_offset());
1548            self.key_body(node.statements(), then_index, then_proof);
1549            if let Some(subsequent) = node.subsequent()
1550                && let Some(else_node) = subsequent.as_else_node()
1551            {
1552                self.claim_line_at(else_node.else_keyword_loc().end_offset());
1553                self.key_body(else_node.statements(), else_index, KeyProof::Probe);
1554            }
1555        }
1556        let proof = if kind == "ternary" {
1557            // Arms are expressions, not statements: nothing to derive from.
1558            DecisionProof::default()
1559        } else {
1560            DecisionProof {
1561                true_statements: self
1562                    .first_statement_id(&node.statements())
1563                    .into_iter()
1564                    .collect(),
1565                false_statements: self
1566                    .chain_false_statements(node.subsequent())
1567                    .unwrap_or_default(),
1568            }
1569        };
1570        self.predicate_decision(
1571            || node.predicate(),
1572            kind,
1573            then_index,
1574            else_index,
1575            true,
1576            proof,
1577        );
1578    }
1579
1580    fn unless_node(&mut self, node: &UnlessNode<'_>) {
1581        let node_span = self.location_span(&node.location());
1582        let then_statements = self.statements_span(&node.statements());
1583        let (then_span, then_kind) = match then_statements {
1584            Some(span) => (span, KeyKind::List),
1585            None => (
1586                self.point_span(node.predicate().location().end_offset()),
1587                KeyKind::Point,
1588            ),
1589        };
1590        let (else_span, else_kind) = match node.else_clause() {
1591            Some(else_node) => match self.statements_span(&else_node.statements()) {
1592                Some(span) => (span, KeyKind::List),
1593                None => (self.location_span(&else_node.location()), KeyKind::Node),
1594            },
1595            None => (node_span, KeyKind::Node),
1596        };
1597        let then_index = self.stdlib("unless", "then", then_span, then_kind, Vec::new());
1598        let else_index = self.stdlib("unless", "else", else_span, else_kind, Vec::new());
1599        let modifier = node.statements().is_some_and(|statements| {
1600            statements.location().start_offset() < node.predicate().location().start_offset()
1601        });
1602        let then_proof = if modifier {
1603            KeyProof::Implied(self.decision_outcomes(&node.predicate(), "unless").1)
1604        } else {
1605            KeyProof::Probe
1606        };
1607        self.claim_line_at(node.predicate().location().end_offset());
1608        self.key_body(node.statements(), then_index, then_proof);
1609        if let Some(else_node) = node.else_clause() {
1610            self.claim_line_at(else_node.else_keyword_loc().end_offset());
1611            self.key_body(else_node.statements(), else_index, KeyProof::Probe);
1612        }
1613        // `unless` runs `then` when the predicate is falsy.
1614        let proof = DecisionProof {
1615            true_statements: node
1616                .else_clause()
1617                .and_then(|else_node| self.first_statement_id(&else_node.statements()))
1618                .into_iter()
1619                .collect(),
1620            false_statements: self
1621                .first_statement_id(&node.statements())
1622                .into_iter()
1623                .collect(),
1624        };
1625        self.predicate_decision(
1626            || node.predicate(),
1627            "unless",
1628            then_index,
1629            else_index,
1630            false,
1631            proof,
1632        );
1633    }
1634
1635    fn loop_node(
1636        &mut self,
1637        location: &Location<'_>,
1638        predicate: Node<'_>,
1639        statements: Option<StatementsNode<'_>>,
1640        begin_modifier: bool,
1641        until: bool,
1642    ) {
1643        let kind = if until { "until" } else { "while" };
1644        let (start, end) = (location.start_offset(), location.end_offset());
1645        let loop_target = if begin_modifier {
1646            // `begin ... end while` always enters the body once.
1647            None
1648        } else {
1649            self.branch(
1650                start,
1651                end,
1652                kind,
1653                [("zero", "zero iterations"), ("entered", "entered")],
1654            )
1655            .map(|id| LoopTarget {
1656                zero: format!("{id}:zero"),
1657                entered: format!("{id}:entered"),
1658                id,
1659                until,
1660            })
1661        };
1662        // The stdlib `body` key proves a same-offset modifier body statement
1663        // on 3.3; on 3.4+ `x while c` runs its body exactly when the loop is
1664        // entered, and any other body statement is proven like a statement.
1665        if let Some(body_span) = self.statements_span(&statements) {
1666            let index = self.stdlib(kind, "body", body_span, KeyKind::List, Vec::new());
1667            let modifier = !begin_modifier
1668                && statements.as_ref().is_some_and(|statements| {
1669                    statements.location().start_offset() < predicate.location().start_offset()
1670                });
1671            let proof = match (&loop_target, modifier) {
1672                (Some(target), true) => KeyProof::Implied(target.entered.clone()),
1673                _ => KeyProof::Probe,
1674            };
1675            self.key_body(statements, index, proof);
1676        }
1677        self.probe_decision(predicate, kind, loop_target, "w");
1678    }
1679
1680    fn for_node(&mut self, node: &ForNode<'_>) {
1681        let location = node.location();
1682        let Some(id) = self.branch(
1683            location.start_offset(),
1684            location.end_offset(),
1685            "for",
1686            [("zero", "zero iterations"), ("entered", "entered")],
1687        ) else {
1688            return;
1689        };
1690        let key = self.probe_key(ProbeTarget::For {
1691            zero: format!("{id}:zero"),
1692            entered: format!("{id}:entered"),
1693            id: id.clone(),
1694        });
1695        let collection = node.collection().location();
1696        self.depth += 1;
1697        self.wrap(
1698            collection.start_offset(),
1699            collection.end_offset(),
1700            format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1701        );
1702        self.depth -= 1;
1703        match node
1704            .statements()
1705            .and_then(|statements| statements.body().iter().next())
1706        {
1707            Some(first) => self.edit(
1708                first.location().start_offset(),
1709                EditRank::StatementProbe,
1710                format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1711                first.location().end_offset(),
1712            ),
1713            None => {
1714                self.manifest.unmeasured.push(format!("{id}:entered"));
1715                self.manifest.unmeasured.push(format!("{id}:zero"));
1716            }
1717        }
1718    }
1719
1720    /// `items.each { ... }` and friends: Ruby's loops are usually method
1721    /// calls with a block. The receiver is wrapped like a `for` collection
1722    /// and the block body gets the entry probe, so zero-versus-entered is
1723    /// exact for every iterator in [`ITERATORS`].
1724    fn iterator_loop(
1725        &mut self,
1726        node: &CallNode<'_>,
1727        receiver: &Node<'_>,
1728        block: &ruby_prism::BlockNode<'_>,
1729    ) {
1730        let location = node.location();
1731        let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
1732        let Some(id) = self.branch(
1733            location.start_offset(),
1734            location.end_offset(),
1735            &format!("iterator-{}", name.trim_end_matches(['?', '!'])),
1736            [("zero", "zero iterations"), ("entered", "entered")],
1737        ) else {
1738            return;
1739        };
1740        let first = block.body().and_then(|body| {
1741            if let Some(statements) = body.as_statements_node() {
1742                statements.body().iter().next()
1743            } else if let Some(begin) = body.as_begin_node() {
1744                begin
1745                    .statements()
1746                    .and_then(|statements| statements.body().iter().next())
1747            } else {
1748                None
1749            }
1750        });
1751        let Some(first) = first else {
1752            // An empty block never enters; both alternatives stay declared
1753            // but nothing can witness them.
1754            self.manifest.unmeasured.push(format!("{id}:entered"));
1755            self.manifest.unmeasured.push(format!("{id}:zero"));
1756            return;
1757        };
1758        let key = self.probe_key(ProbeTarget::For {
1759            zero: format!("{id}:zero"),
1760            entered: format!("{id}:entered"),
1761            id,
1762        });
1763        let receiver_location = receiver.location();
1764        self.depth += 1;
1765        self.wrap(
1766            receiver_location.start_offset(),
1767            receiver_location.end_offset(),
1768            format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1769        );
1770        self.depth -= 1;
1771        self.edit(
1772            first.location().start_offset(),
1773            EditRank::StatementProbe,
1774            format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1775            first.location().end_offset(),
1776        );
1777    }
1778
1779    fn case_node(&mut self, node: &CaseNode<'_>) {
1780        let node_span = self.location_span(&node.location());
1781        let (start, end) = (node.location().start_offset(), node.location().end_offset());
1782        // Known up front so that every clause selection also proves "some
1783        // clause matched" for a `case` without `else`.
1784        let no_match_id = node
1785            .else_clause()
1786            .is_none()
1787            .then(|| stable_id(self.file, "branch", start, end, "case-no-match"));
1788        // An explicit `else` is missed whenever a clause before it is selected.
1789        let else_id = node.else_clause().map(|else_node| {
1790            stable_id(
1791                self.file,
1792                "branch",
1793                else_node.location().start_offset(),
1794                else_node.location().end_offset(),
1795                "case-else",
1796            )
1797        });
1798        let mut clauses = Vec::new();
1799        let mut clause_ids = Vec::new();
1800        for (index, condition) in node.conditions().iter().enumerate() {
1801            let Some(when) = condition.as_when_node() else {
1802                continue;
1803            };
1804            let Some(id) = self.branch(
1805                when.location().start_offset(),
1806                when.location().end_offset(),
1807                &format!("case-when-{index}"),
1808                [("missed", "not selected"), ("selected", "selected")],
1809            ) else {
1810                return;
1811            };
1812            let statements = self.statements_span(&when.statements());
1813            let span = statements.unwrap_or_else(|| self.location_span(&when.location()));
1814            let key_index = self.stdlib(
1815                "case",
1816                "when",
1817                span,
1818                if statements.is_some() {
1819                    KeyKind::List
1820                } else {
1821                    KeyKind::Node
1822                },
1823                vec![format!("{id}:selected")],
1824            );
1825            self.key_body(when.statements(), key_index, KeyProof::Probe);
1826            let proven =
1827                self.clause_proof(&id, &clause_ids, no_match_id.as_deref(), else_id.as_deref());
1828            // An empty body gets its probe after `then`, or as a `then` of
1829            // its own after the last condition.
1830            let (offset, before) = match when.then_keyword_loc() {
1831                Some(then) => (then.end_offset(), " "),
1832                None => (
1833                    when.conditions()
1834                        .iter()
1835                        .last()
1836                        .map(|condition| condition.location().end_offset())
1837                        .unwrap_or_else(|| when.location().end_offset()),
1838                    " then ",
1839                ),
1840            };
1841            self.claim_line_at(offset);
1842            self.body_proves(&when.statements(), proven, offset, before, "");
1843            clauses.push(CaseClausePlan {
1844                key: self.branches[key_index].key.clone(),
1845                missed: format!("{id}:missed"),
1846                selected: format!("{id}:selected"),
1847            });
1848            clause_ids.push(id);
1849        }
1850        let no_match = match node.else_clause() {
1851            Some(else_node) => {
1852                let Some(id) = self.branch(
1853                    else_node.location().start_offset(),
1854                    else_node.location().end_offset(),
1855                    "case-else",
1856                    [("missed", "not selected"), ("selected", "selected")],
1857                ) else {
1858                    return;
1859                };
1860                let statements = self.statements_span(&else_node.statements());
1861                let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1862                let key_index = self.stdlib(
1863                    "case",
1864                    "else",
1865                    span,
1866                    if statements.is_some() {
1867                        KeyKind::List
1868                    } else {
1869                        KeyKind::Node
1870                    },
1871                    vec![format!("{id}:selected")],
1872                );
1873                self.key_body(else_node.statements(), key_index, KeyProof::Probe);
1874                let proven = self.clause_proof(&id, &clause_ids, None, None);
1875                self.claim_line_at(else_node.else_keyword_loc().end_offset());
1876                self.body_proves(
1877                    &else_node.statements(),
1878                    proven,
1879                    else_node.else_keyword_loc().end_offset(),
1880                    " ",
1881                    "",
1882                );
1883                clauses.push(CaseClausePlan {
1884                    key: self.branches[key_index].key.clone(),
1885                    missed: format!("{id}:missed"),
1886                    selected: format!("{id}:selected"),
1887                });
1888                None
1889            }
1890            None => self
1891                .branch(
1892                    start,
1893                    end,
1894                    "case-no-match",
1895                    [
1896                        ("matched", "some clause matched"),
1897                        ("unmatched", "no clause matched"),
1898                    ],
1899                )
1900                .map(|id| {
1901                    let key_index = self.stdlib(
1902                        "case",
1903                        "else",
1904                        node_span,
1905                        KeyKind::Node,
1906                        vec![format!("{id}:unmatched")],
1907                    );
1908                    // Ruby 3.4+: an `else` of Supercov's own, whose only
1909                    // statement is the probe, runs exactly when no clause
1910                    // matched and leaves the value nil as before.
1911                    self.no_match_probe(&id, &clause_ids, node.end_keyword_loc().start_offset());
1912                    CaseNoMatchPlan {
1913                        key: self.branches[key_index].key.clone(),
1914                        matched: format!("{id}:matched"),
1915                        unmatched: format!("{id}:unmatched"),
1916                    }
1917                }),
1918        };
1919        self.cases.push(CasePlan { clauses, no_match });
1920    }
1921
1922    /// What selecting a clause proves: itself selected, every earlier clause
1923    /// tested and missed, the explicit `else` missed, and for a `case`
1924    /// without `else` that a clause matched.
1925    fn clause_proof(
1926        &self,
1927        id: &str,
1928        earlier: &[String],
1929        no_match_id: Option<&str>,
1930        else_id: Option<&str>,
1931    ) -> Vec<String> {
1932        let mut ids = vec![format!("{id}:selected")];
1933        ids.extend(earlier.iter().map(|clause| format!("{clause}:missed")));
1934        if let Some(no_match) = no_match_id {
1935            ids.push(format!("{no_match}:matched"));
1936        }
1937        if let Some(else_id) = else_id {
1938            ids.push(format!("{else_id}:missed"));
1939        }
1940        ids
1941    }
1942
1943    fn no_match_probe(&mut self, id: &str, clauses: &[String], end_keyword: usize) {
1944        let mut ids = vec![format!("{id}:unmatched")];
1945        ids.extend(clauses.iter().map(|clause| format!("{clause}:missed")));
1946        let key = self.probe_key(ProbeTarget::Hits { ids });
1947        self.edit(
1948            end_keyword,
1949            EditRank::StatementProbe,
1950            format!("else {RUBY_PROBE_RECEIVER}.hs({key}); "),
1951            end_keyword,
1952        );
1953    }
1954
1955    /// A `case ... in` with no `else` raises NoMatchingPatternError -- or
1956    /// its NoMatchingPatternKeyError subclass -- naming the last pattern it
1957    /// tried. An inserted `else` would swallow that and return nil, so the
1958    /// case is wrapped instead: the original exception is re-raised as it
1959    /// was, and counted as "no pattern matched" only when this case raised
1960    /// it, at its own line, rather than a nested one.
1961    fn no_match_rescue(&mut self, id: &str, clauses: &[String], start: usize, end: usize) {
1962        let mut ids = vec![format!("{id}:unmatched")];
1963        ids.extend(clauses.iter().map(|clause| format!("{clause}:missed")));
1964        let key = self.probe_key(ProbeTarget::Hits { ids });
1965        let (line, _) = self.line_column(start);
1966        self.edit(start, EditRank::Opener, "begin; ".into(), end);
1967        self.edit(
1968            end,
1969            EditRank::Closer,
1970            format!(
1971                "; rescue ::NoMatchingPatternError; {RUBY_PROBE_RECEIVER}.hs({key}) if \
1972                 $!.backtrace_locations&.first&.then {{ |l| l.lineno == {line} && l.path == __FILE__ }}; \
1973                 raise; end"
1974            ),
1975            start,
1976        );
1977    }
1978
1979    fn case_match_node(&mut self, node: &CaseMatchNode<'_>) {
1980        let node_span = self.location_span(&node.location());
1981        let (start, end) = (node.location().start_offset(), node.location().end_offset());
1982        let no_match_id = node
1983            .else_clause()
1984            .is_none()
1985            .then(|| stable_id(self.file, "branch", start, end, "case-no-match"));
1986        // An explicit `else` is missed whenever a clause before it is selected.
1987        let else_id = node.else_clause().map(|else_node| {
1988            stable_id(
1989                self.file,
1990                "branch",
1991                else_node.location().start_offset(),
1992                else_node.location().end_offset(),
1993                "case-else",
1994            )
1995        });
1996        let mut clauses = Vec::new();
1997        let mut clause_ids = Vec::new();
1998        for (index, condition) in node.conditions().iter().enumerate() {
1999            let Some(in_node) = condition.as_in_node() else {
2000                continue;
2001            };
2002            let Some(id) = self.branch(
2003                in_node.location().start_offset(),
2004                in_node.location().end_offset(),
2005                &format!("case-in-{index}"),
2006                [("missed", "not selected"), ("selected", "selected")],
2007            ) else {
2008                return;
2009            };
2010            let statements = self.statements_span(&in_node.statements());
2011            let span = statements.unwrap_or_else(|| self.location_span(&in_node.location()));
2012            let key_index = self.stdlib(
2013                "case",
2014                "in",
2015                span,
2016                if statements.is_some() {
2017                    KeyKind::List
2018                } else {
2019                    KeyKind::Node
2020                },
2021                vec![format!("{id}:selected")],
2022            );
2023            self.key_body(in_node.statements(), key_index, KeyProof::Probe);
2024            let proven =
2025                self.clause_proof(&id, &clause_ids, no_match_id.as_deref(), else_id.as_deref());
2026            let (offset, before) = match in_node.then_loc() {
2027                Some(then) => (then.end_offset(), " "),
2028                None => (in_node.pattern().location().end_offset(), " then "),
2029            };
2030            self.claim_line_at(offset);
2031            self.body_proves(&in_node.statements(), proven, offset, before, "");
2032            clauses.push(CaseClausePlan {
2033                key: self.branches[key_index].key.clone(),
2034                missed: format!("{id}:missed"),
2035                selected: format!("{id}:selected"),
2036            });
2037            clause_ids.push(id);
2038            // A guard is a decision of its own; Ruby reports no branch key
2039            // for it, so it is always probe-driven.
2040            let pattern = in_node.pattern();
2041            if let Some(guard) = pattern.as_if_node() {
2042                self.guard_nodes.insert(pattern.location().start_offset());
2043                self.probe_decision(guard.predicate(), "in-guard", None, "d");
2044            } else if let Some(guard) = pattern.as_unless_node() {
2045                self.guard_nodes.insert(pattern.location().start_offset());
2046                self.probe_decision(guard.predicate(), "in-guard-unless", None, "d");
2047            }
2048        }
2049        let no_match = match node.else_clause() {
2050            Some(else_node) => {
2051                let Some(id) = self.branch(
2052                    else_node.location().start_offset(),
2053                    else_node.location().end_offset(),
2054                    "case-else",
2055                    [("missed", "not selected"), ("selected", "selected")],
2056                ) else {
2057                    return;
2058                };
2059                let statements = self.statements_span(&else_node.statements());
2060                let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
2061                let key_index = self.stdlib(
2062                    "case",
2063                    "else",
2064                    span,
2065                    if statements.is_some() {
2066                        KeyKind::List
2067                    } else {
2068                        KeyKind::Node
2069                    },
2070                    vec![format!("{id}:selected")],
2071                );
2072                self.key_body(else_node.statements(), key_index, KeyProof::Probe);
2073                let proven = self.clause_proof(&id, &clause_ids, None, None);
2074                self.claim_line_at(else_node.else_keyword_loc().end_offset());
2075                self.body_proves(
2076                    &else_node.statements(),
2077                    proven,
2078                    else_node.else_keyword_loc().end_offset(),
2079                    " ",
2080                    "",
2081                );
2082                clauses.push(CaseClausePlan {
2083                    key: self.branches[key_index].key.clone(),
2084                    missed: format!("{id}:missed"),
2085                    selected: format!("{id}:selected"),
2086                });
2087                None
2088            }
2089            None => self
2090                .branch(
2091                    start,
2092                    end,
2093                    "case-no-match",
2094                    [
2095                        ("matched", "some pattern matched"),
2096                        ("unmatched", "no pattern matched"),
2097                    ],
2098                )
2099                .map(|id| {
2100                    let key_index = self.stdlib(
2101                        "case",
2102                        "else",
2103                        node_span,
2104                        KeyKind::Node,
2105                        vec![format!("{id}:unmatched")],
2106                    );
2107                    self.no_match_rescue(&id, &clause_ids, start, end);
2108                    CaseNoMatchPlan {
2109                        key: self.branches[key_index].key.clone(),
2110                        matched: format!("{id}:matched"),
2111                        unmatched: format!("{id}:unmatched"),
2112                    }
2113                }),
2114        };
2115        self.cases.push(CasePlan { clauses, no_match });
2116    }
2117
2118    fn safe_navigation(&mut self, node: &CallNode<'_>) {
2119        let location = node.location();
2120        let Some(id) = self.branch(
2121            location.start_offset(),
2122            location.end_offset(),
2123            "safe-navigation",
2124            [("nil", "receiver nil"), ("called", "method called")],
2125        ) else {
2126            return;
2127        };
2128        // Ruby's key runs from the receiver to the closing parenthesis or the
2129        // last argument, and to the message when there are no arguments; a
2130        // block or block argument is never part of it.
2131        let end = match node.arguments() {
2132            Some(arguments) => node
2133                .closing_loc()
2134                .map(|closing| closing.end_offset())
2135                .unwrap_or_else(|| arguments.location().end_offset()),
2136            None => node
2137                .message_loc()
2138                .map(|message| message.end_offset())
2139                .unwrap_or_else(|| location.end_offset()),
2140        };
2141        let (start_line, start_column) = self.line_column(location.start_offset());
2142        let (end_line, end_column) = self.line_column(end);
2143        let span = PlanSpan {
2144            start: [start_line, start_column],
2145            end: [end_line, end_column],
2146        };
2147        self.stdlib(
2148            "&.",
2149            "then",
2150            span,
2151            KeyKind::Node,
2152            vec![format!("{id}:called")],
2153        );
2154        self.stdlib("&.", "else", span, KeyKind::Node, vec![format!("{id}:nil")]);
2155        // Ruby 3.4+: the receiver's value decides, so the receiver is probed.
2156        if let Some(receiver) = node.receiver() {
2157            let key = self.probe_key(ProbeTarget::SafeNavigation {
2158                nil: format!("{id}:nil"),
2159                called: format!("{id}:called"),
2160            });
2161            let receiver = receiver.location();
2162            self.depth += 1;
2163            self.wrap(
2164                receiver.start_offset(),
2165                receiver.end_offset(),
2166                format!("{RUBY_PROBE_RECEIVER}.n({key}, ("),
2167            );
2168            self.depth -= 1;
2169        }
2170    }
2171
2172    /// `&&` and `||` used as a value: the left operand is read as a boolean,
2173    /// whatever the operator returns, so a compound one is a decision as much
2174    /// as an `if`'s condition is -- `(a && b) || fallback`, `a || b ||
2175    /// default`. The right is a value, judged as one in its turn. A `!` reads
2176    /// its operand as a boolean too. Inside the operand's own logical probe.
2177    fn value_decision<'n>(&mut self, operand: impl Fn() -> Node<'n>) {
2178        if !self.is_compound(operand()) {
2179            return;
2180        }
2181        let (root, _) = self.strip(operand());
2182        if self.tree_logicals.contains(&root.location().start_offset()) {
2183            return;
2184        }
2185        self.depth += 1;
2186        self.probe_decision(operand(), "value", None, "d");
2187        self.depth -= 1;
2188    }
2189
2190    fn value_logical(&mut self, op: &str, left: &Node<'_>, node_start: usize, node_end: usize) {
2191        let Some(id) = self.branch(
2192            node_start,
2193            node_end,
2194            &format!("logical-{op}"),
2195            [
2196                ("short-circuit", "short-circuited"),
2197                ("evaluated", "right operand evaluated"),
2198            ],
2199        ) else {
2200            return;
2201        };
2202        let key = self.probe_key(ProbeTarget::Logical {
2203            op: op.into(),
2204            short_circuit: format!("{id}:short-circuit"),
2205            evaluated: format!("{id}:evaluated"),
2206        });
2207        let left = left.location();
2208        self.depth += 1;
2209        self.wrap(
2210            left.start_offset(),
2211            left.end_offset(),
2212            format!("{RUBY_PROBE_RECEIVER}.l({key}, ("),
2213        );
2214        self.depth -= 1;
2215    }
2216
2217    /// `x ||= v` / `x &&= v`. A variable target can be re-read without side
2218    /// effects, so the whole expression becomes `(l(k, x); x ||= v)`; other
2219    /// targets only get the evaluated side.
2220    fn op_assign(
2221        &mut self,
2222        op: &str,
2223        node_start: usize,
2224        node_end: usize,
2225        name: Option<&[u8]>,
2226        value: &Node<'_>,
2227    ) {
2228        let Some(id) = self.branch(
2229            node_start,
2230            node_end,
2231            &format!("{op}-assign"),
2232            [
2233                ("short-circuit", "assignment skipped"),
2234                ("evaluated", "value evaluated and assigned"),
2235            ],
2236        ) else {
2237            return;
2238        };
2239        match name {
2240            Some(name) => {
2241                let key = self.probe_key(ProbeTarget::Logical {
2242                    op: op.into(),
2243                    short_circuit: format!("{id}:short-circuit"),
2244                    evaluated: format!("{id}:evaluated"),
2245                });
2246                let name = String::from_utf8_lossy(name);
2247                self.depth += 1;
2248                self.edit(
2249                    node_start,
2250                    EditRank::Opener,
2251                    format!("({RUBY_PROBE_RECEIVER}.l({key}, {name}); "),
2252                    node_end,
2253                );
2254                self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2255                self.depth -= 1;
2256            }
2257            None => {
2258                // `(pre(k); recv[i] ||= (es(k); v))`: the target is evaluated
2259                // exactly once, as before; arrivals and right-side starts are
2260                // counted per phase and their difference is the skipped side.
2261                let key = self.probe_key(ProbeTarget::Arrival {
2262                    short_circuit: format!("{id}:short-circuit"),
2263                    evaluated: format!("{id}:evaluated"),
2264                });
2265                let value_location = value.location();
2266                self.depth += 1;
2267                self.edit(
2268                    node_start,
2269                    EditRank::Opener,
2270                    format!("({RUBY_PROBE_RECEIVER}.pre({key}); "),
2271                    node_end,
2272                );
2273                self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2274                self.depth += 1;
2275                self.edit(
2276                    value_location.start_offset(),
2277                    EditRank::Opener,
2278                    format!("({RUBY_PROBE_RECEIVER}.es({key}); "),
2279                    value_location.end_offset(),
2280                );
2281                self.edit(
2282                    value_location.end_offset(),
2283                    EditRank::Closer,
2284                    ")".into(),
2285                    value_location.start_offset(),
2286                );
2287                self.depth -= 2;
2288            }
2289        }
2290    }
2291
2292    // -- exception flow -----------------------------------------------------
2293
2294    /// `begin`/`rescue`/`else`/`ensure` in any host: explicit `begin`, a
2295    /// method body, or a `do` block. `close` is where the closing keyword
2296    /// lives, which is where the propagation clause is inserted when the
2297    /// construct has no `else` or `ensure`.
2298    fn begin_node(&mut self, node: &BeginNode<'_>, close: Option<usize>) {
2299        let has_rescue = node.rescue_clause().is_some();
2300        let has_ensure = node.ensure_clause().is_some();
2301        if !has_rescue && !has_ensure {
2302            return;
2303        }
2304        let location = node.location();
2305        let (start, end) = (location.start_offset(), location.end_offset());
2306        let Some(id) = self.branch(
2307            start,
2308            end,
2309            "begin",
2310            [
2311                ("success", "body completed"),
2312                ("raised", "exception raised"),
2313            ],
2314        ) else {
2315            return;
2316        };
2317        let mut handlers = Vec::new();
2318        let mut handler_edits = Vec::new();
2319        let mut rescue = node.rescue_clause();
2320        let mut index = 0;
2321        while let Some(clause) = rescue {
2322            let clause_location = clause.location();
2323            let Some(handler_id) = self.branch(
2324                clause_location.start_offset(),
2325                clause_location.end_offset(),
2326                &format!("rescue-{index}"),
2327                [("missed", "not selected"), ("selected", "selected")],
2328            ) else {
2329                return;
2330            };
2331            handlers.push(HandlerTarget {
2332                missed: format!("{handler_id}:missed"),
2333                selected: format!("{handler_id}:selected"),
2334                id: handler_id,
2335            });
2336            handler_edits.push(self.handler_probe_position(&clause));
2337            rescue = clause.subsequent();
2338            index += 1;
2339        }
2340        let key = self.probe_key(ProbeTarget::Try {
2341            success: format!("{id}:success"),
2342            raised: format!("{id}:raised"),
2343            id: id.clone(),
2344            handlers,
2345        });
2346        for (index, (offset, leading, scope_end)) in handler_edits.into_iter().enumerate() {
2347            let text = if leading {
2348                format!("; {RUBY_PROBE_RECEIVER}.h({key}, {index})")
2349            } else {
2350                format!("{RUBY_PROBE_RECEIVER}.h({key}, {index}); ")
2351            };
2352            self.edit(offset, EditRank::StatementProbe, text, scope_end);
2353        }
2354        // Propagation clause: after every user clause, before else/ensure/end.
2355        let clause_offset = node
2356            .else_clause()
2357            .map(|clause| clause.else_keyword_loc().start_offset())
2358            .or_else(|| {
2359                node.ensure_clause()
2360                    .map(|clause| clause.ensure_keyword_loc().start_offset())
2361            })
2362            .or_else(|| node.end_keyword_loc().map(|loc| loc.start_offset()))
2363            .or(close);
2364        match clause_offset {
2365            Some(offset) => self.edit(
2366                offset,
2367                EditRank::Clause,
2368                format!(
2369                    "rescue Exception => __supercov_e; {RUBY_PROBE_RECEIVER}.p({key}); raise; "
2370                ),
2371                offset,
2372            ),
2373            None => {
2374                self.manifest.unmeasured.push(format!("{id}:raised"));
2375                let (line, _) = self.line_column(start);
2376                self.begin_unmeasured.push((id.clone(), line));
2377            }
2378        }
2379        // Completion: the else clause runs only after a completed body.
2380        if let Some(else_clause) = node.else_clause() {
2381            match else_clause
2382                .statements()
2383                .and_then(|statements| statements.body().iter().next())
2384            {
2385                Some(first) => self.edit(
2386                    first.location().start_offset(),
2387                    EditRank::StatementProbe,
2388                    format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2389                    first.location().end_offset(),
2390                ),
2391                None => self.edit(
2392                    else_clause.else_keyword_loc().end_offset(),
2393                    EditRank::StatementProbe,
2394                    format!(" {RUBY_PROBE_RECEIVER}.ok0({key});"),
2395                    else_clause.else_keyword_loc().end_offset(),
2396                ),
2397            }
2398            return;
2399        }
2400        let last = node
2401            .statements()
2402            .and_then(|statements| statements.body().iter().last());
2403        match last {
2404            Some(last) => {
2405                if !self.completion_probe(last, key) {
2406                    self.manifest.unmeasured.push(format!("{id}:success"));
2407                    let (line, _) = self.line_column(start);
2408                    self.begin_unmeasured.push((id, line));
2409                }
2410            }
2411            None => {
2412                self.manifest.unmeasured.push(format!("{id}:success"));
2413                let (line, _) = self.line_column(start);
2414                self.begin_unmeasured.push((id, line));
2415            }
2416        }
2417    }
2418
2419    /// Where the handler-entry probe goes: before the first body statement,
2420    /// or right after the clause header when the body is empty.
2421    fn handler_probe_position(&self, clause: &RescueNode<'_>) -> (usize, bool, usize) {
2422        if let Some(first) = clause
2423            .statements()
2424            .and_then(|statements| statements.body().iter().next())
2425        {
2426            return (
2427                first.location().start_offset(),
2428                false,
2429                first.location().end_offset(),
2430            );
2431        }
2432        let offset = if let Some(then_keyword) = clause.then_keyword_loc() {
2433            then_keyword.end_offset()
2434        } else if let Some(reference) = clause.reference() {
2435            reference.location().end_offset()
2436        } else if let Some(last) = clause.exceptions().iter().last() {
2437            last.location().end_offset()
2438        } else {
2439            clause.keyword_loc().end_offset()
2440        };
2441        (offset, true, offset)
2442    }
2443
2444    /// True for an expression whose own value can be a jump: a `return`,
2445    /// `break`, `next`, `redo` or `retry`, or an `if`, `unless`, ternary,
2446    /// `case` or nested `begin` with an arm that ends in one. Such an
2447    /// expression may not be wrapped. Ruby rejects the parenthesised form
2448    /// outright when every arm is a jump ("void value expression"), and when
2449    /// only some arms are, passing it as an argument -- which is what a
2450    /// wrapper does -- makes the compiler miscount its stack ("argument stack
2451    /// underflow") for shapes that are hard to predict. Its arms are probed
2452    /// instead. A jump reached through a block, a loop or `&&`/`||` belongs to
2453    /// that construct rather than to this expression's value, and is fine.
2454    fn jump_exposed(&self, node: &Node<'_>) -> bool {
2455        if Self::is_jump(node) {
2456            return true;
2457        }
2458        if let Some(if_node) = node.as_if_node() {
2459            let else_exposed = match if_node.subsequent() {
2460                Some(subsequent) => match subsequent.as_else_node() {
2461                    Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2462                    None => self.jump_exposed(&subsequent),
2463                },
2464                None => false,
2465            };
2466            return else_exposed || self.arm_jump_exposed(if_node.statements());
2467        }
2468        if let Some(unless_node) = node.as_unless_node() {
2469            let else_exposed = match unless_node.else_clause() {
2470                Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2471                None => false,
2472            };
2473            return else_exposed || self.arm_jump_exposed(unless_node.statements());
2474        }
2475        if let Some(begin) = node.as_begin_node() {
2476            if self.arm_jump_exposed(begin.statements())
2477                || begin
2478                    .else_clause()
2479                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()))
2480            {
2481                return true;
2482            }
2483            let mut rescue = begin.rescue_clause();
2484            while let Some(clause) = rescue {
2485                if self.arm_jump_exposed(clause.statements()) {
2486                    return true;
2487                }
2488                rescue = clause.subsequent();
2489            }
2490            return false;
2491        }
2492        if let Some(case_node) = node.as_case_node() {
2493            return case_node
2494                .conditions()
2495                .iter()
2496                .any(|condition| match condition.as_when_node() {
2497                    Some(when_node) => self.arm_jump_exposed(when_node.statements()),
2498                    None => false,
2499                })
2500                || case_node
2501                    .else_clause()
2502                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2503        }
2504        if let Some(case_node) = node.as_case_match_node() {
2505            return case_node
2506                .conditions()
2507                .iter()
2508                .any(|condition| match condition.as_in_node() {
2509                    Some(in_node) => self.arm_jump_exposed(in_node.statements()),
2510                    None => false,
2511                })
2512                || case_node
2513                    .else_clause()
2514                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2515        }
2516        if let Some(parentheses) = node.as_parentheses_node() {
2517            return match parentheses.body() {
2518                Some(body) => match body.as_statements_node() {
2519                    Some(statements) => self.arm_jump_exposed(Some(statements)),
2520                    None => self.jump_exposed(&body),
2521                },
2522                None => false,
2523            };
2524        }
2525        false
2526    }
2527
2528    fn arm_jump_exposed(&self, statements: Option<StatementsNode<'_>>) -> bool {
2529        match statements.and_then(|statements| statements.body().iter().last()) {
2530            Some(last) => self.jump_exposed(&last),
2531            None => false,
2532        }
2533    }
2534
2535    /// The expressions to wrap so the construct's normal completion is
2536    /// observed, or `None` when it cannot be observed at all. A statement that
2537    /// may not be wrapped is replaced by its arms, of which exactly one runs;
2538    /// an arm that is missing (an `if` with no `else`, whose fall-through
2539    /// carries no expression) makes the whole construct unobservable.
2540    fn probe_targets<'n>(&self, node: Node<'n>) -> Option<Vec<Node<'n>>> {
2541        if Self::is_jump(&node) {
2542            return Some(vec![node]);
2543        }
2544        if node.as_multi_write_node().is_some()
2545            || node.as_alias_method_node().is_some()
2546            || node.as_alias_global_variable_node().is_some()
2547            || node.as_undef_node().is_some()
2548        {
2549            return None;
2550        }
2551        if !self.jump_exposed(&node) {
2552            return Some(vec![node]);
2553        }
2554        if let Some(if_node) = node.as_if_node() {
2555            let mut targets = self.arm_targets(if_node.statements())?;
2556            match if_node.subsequent() {
2557                Some(subsequent) => match subsequent.as_else_node() {
2558                    Some(else_node) => targets.extend(self.arm_targets(else_node.statements())?),
2559                    None => targets.extend(self.probe_targets(subsequent)?),
2560                },
2561                None => return None,
2562            }
2563            return Some(targets);
2564        }
2565        if let Some(unless_node) = node.as_unless_node() {
2566            let mut targets = self.arm_targets(unless_node.statements())?;
2567            let else_node = unless_node.else_clause()?;
2568            targets.extend(self.arm_targets(else_node.statements())?);
2569            return Some(targets);
2570        }
2571        if let Some(case_node) = node.as_case_node() {
2572            let mut targets = Vec::new();
2573            for condition in case_node.conditions().iter() {
2574                let when_node = condition.as_when_node()?;
2575                targets.extend(self.arm_targets(when_node.statements())?);
2576            }
2577            targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2578            return Some(targets);
2579        }
2580        if let Some(case_node) = node.as_case_match_node() {
2581            let mut targets = Vec::new();
2582            for condition in case_node.conditions().iter() {
2583                let in_node = condition.as_in_node()?;
2584                targets.extend(self.arm_targets(in_node.statements())?);
2585            }
2586            targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2587            return Some(targets);
2588        }
2589        if let Some(begin) = node.as_begin_node() {
2590            let mut targets = match begin.else_clause() {
2591                Some(else_node) => self.arm_targets(else_node.statements())?,
2592                None => self.arm_targets(begin.statements())?,
2593            };
2594            let mut rescue = begin.rescue_clause();
2595            while let Some(clause) = rescue {
2596                targets.extend(self.arm_targets(clause.statements())?);
2597                rescue = clause.subsequent();
2598            }
2599            return Some(targets);
2600        }
2601        if let Some(parentheses) = node.as_parentheses_node() {
2602            let body = parentheses.body()?;
2603            return match body.as_statements_node() {
2604                Some(statements) => self.arm_targets(Some(statements)),
2605                None => self.probe_targets(body),
2606            };
2607        }
2608        None
2609    }
2610
2611    fn arm_targets<'n>(&self, statements: Option<StatementsNode<'n>>) -> Option<Vec<Node<'n>>> {
2612        let last = statements.and_then(|statements| statements.body().iter().last())?;
2613        self.probe_targets(last)
2614    }
2615
2616    /// Wrap the body's final statement so its normal completion is observed
2617    /// without changing the value of the construct. Returns false when the
2618    /// statement has no expression form to wrap (see
2619    /// [`Collector::jump_exposed`]).
2620    fn completion_probe(&mut self, last: Node<'_>, key: u64) -> bool {
2621        let Some(targets) = self.probe_targets(last) else {
2622            return false;
2623        };
2624        for target in &targets {
2625            self.wrap_completion(target, key);
2626        }
2627        true
2628    }
2629
2630    /// One expression whose completion proves the construct completed.
2631    fn wrap_completion(&mut self, last: &Node<'_>, key: u64) {
2632        let location = last.location();
2633        let (start, end) = (location.start_offset(), location.end_offset());
2634        let arguments = if let Some(node) = last.as_return_node() {
2635            Some((node.keyword_loc(), node.arguments()))
2636        } else if let Some(node) = last.as_break_node() {
2637            Some((node.keyword_loc(), node.arguments()))
2638        } else {
2639            last.as_next_node()
2640                .map(|node| (node.keyword_loc(), node.arguments()))
2641        };
2642        if let Some((keyword, arguments)) = arguments {
2643            match arguments {
2644                Some(arguments) => {
2645                    let arguments_location = arguments.location();
2646                    let multiple = arguments.arguments().iter().count() > 1
2647                        || arguments
2648                            .arguments()
2649                            .iter()
2650                            .any(|argument| argument.as_splat_node().is_some());
2651                    let (open, close) = if multiple {
2652                        // `return a, b` already returns `[a, b]`.
2653                        (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ["), "])")
2654                    } else {
2655                        (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("), "))")
2656                    };
2657                    self.depth += 1;
2658                    self.edit(
2659                        arguments_location.start_offset(),
2660                        EditRank::Opener,
2661                        open,
2662                        arguments_location.end_offset(),
2663                    );
2664                    self.edit(
2665                        arguments_location.end_offset(),
2666                        EditRank::Closer,
2667                        close.into(),
2668                        arguments_location.start_offset(),
2669                    );
2670                    self.depth -= 1;
2671                }
2672                None => self.edit(
2673                    keyword.start_offset(),
2674                    EditRank::StatementProbe,
2675                    format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2676                    end,
2677                ),
2678            }
2679            return;
2680        }
2681        if last.as_redo_node().is_some() || last.as_retry_node().is_some() {
2682            self.edit(
2683                start,
2684                EditRank::StatementProbe,
2685                format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2686                end,
2687            );
2688            return;
2689        }
2690        self.depth += 1;
2691        self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("));
2692        self.depth -= 1;
2693    }
2694
2695    fn rescue_modifier(&mut self, node: &RescueModifierNode<'_>) {
2696        let location = node.location();
2697        let (start, end) = (location.start_offset(), location.end_offset());
2698        let Some(id) = self.branch(
2699            start,
2700            end,
2701            "rescue-modifier",
2702            [
2703                ("success", "expression completed"),
2704                ("raised", "fallback used"),
2705            ],
2706        ) else {
2707            return;
2708        };
2709        let key = self.probe_key(ProbeTarget::Try {
2710            success: format!("{id}:success"),
2711            raised: format!("{id}:raised"),
2712            id,
2713            handlers: Vec::new(),
2714        });
2715        let expression = node.expression();
2716        let fallback_node = node.rescue_expression();
2717        let fallback = fallback_node.location();
2718        self.depth += 1;
2719        if Self::is_jump(&expression) {
2720            // `return x rescue y` has no value to wrap: probe the jump's
2721            // argument or the jump itself, as for a body's final statement.
2722            self.completion_probe(expression, key);
2723        } else {
2724            let expression = expression.location();
2725            self.wrap(
2726                expression.start_offset(),
2727                expression.end_offset(),
2728                format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("),
2729            );
2730        }
2731        if Self::is_jump(&fallback_node) {
2732            // `rescue next` has no value either: `rescue (hm0(k); next)`.
2733            self.edit(
2734                fallback.start_offset(),
2735                EditRank::Opener,
2736                format!("({RUBY_PROBE_RECEIVER}.hm0({key}); "),
2737                fallback.end_offset(),
2738            );
2739            self.edit(
2740                fallback.end_offset(),
2741                EditRank::Closer,
2742                ")".into(),
2743                fallback.start_offset(),
2744            );
2745        } else {
2746            self.wrap(
2747                fallback.start_offset(),
2748                fallback.end_offset(),
2749                format!("{RUBY_PROBE_RECEIVER}.hm({key}, ("),
2750            );
2751        }
2752        self.depth -= 1;
2753    }
2754
2755    /// A statement that leaves its frame or loop without producing a value.
2756    fn is_jump(node: &Node<'_>) -> bool {
2757        node.as_return_node().is_some()
2758            || node.as_break_node().is_some()
2759            || node.as_next_node().is_some()
2760            || node.as_redo_node().is_some()
2761            || node.as_retry_node().is_some()
2762    }
2763
2764    fn def_node(&mut self, node: &DefNode<'_>) {
2765        let location = node.location();
2766        let (start, end) = (location.start_offset(), location.end_offset());
2767        let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
2768        let id = stable_id(self.file, "function", start, end, &name);
2769        if !self.point_ids.insert(id.clone()) {
2770            return;
2771        }
2772        self.push_point(&id, start, end, PointKind::Function, Some(name));
2773        let span = self.location_span(&location);
2774        self.methods.push(MethodKeyPlan {
2775            span,
2776            unshifted: span,
2777            id: id.clone(),
2778        });
2779        // Ruby 3.4+: the body's first statement runs exactly when the method
2780        // is entered; a body with none gets a probe after the signature.
2781        let body_statements = node.body().and_then(|body| {
2782            if let Some(statements) = body.as_statements_node() {
2783                Some(statements)
2784            } else {
2785                body.as_begin_node().and_then(|begin| begin.statements())
2786            }
2787        });
2788        let signature_end = node
2789            .rparen_loc()
2790            .map(|rparen| rparen.end_offset())
2791            .or_else(|| {
2792                node.parameters()
2793                    .map(|parameters| parameters.location().end_offset())
2794            })
2795            .unwrap_or_else(|| node.name_loc().end_offset());
2796        self.body_proves(&body_statements, vec![id], signature_end, "; ", "");
2797        if let Some(body) = node.body()
2798            && let Some(begin) = body.as_begin_node()
2799        {
2800            self.begin_node(&begin, node.end_keyword_loc().map(|loc| loc.start_offset()));
2801        }
2802        if node.equal_loc().is_some()
2803            && let Some(body) = node.body()
2804            && let Some(statements) = body.as_statements_node()
2805        {
2806            for statement in statements.body().iter() {
2807                self.endless_bodies
2808                    .insert(statement.location().start_offset());
2809            }
2810        }
2811    }
2812
2813    // -- finishing ----------------------------------------------------------
2814
2815    /// Column shift the insertions cause on one line, for positions the
2816    /// runtime will read back from Ruby's `Coverage`. Insertions strictly
2817    /// inside a key's line range move whatever follows them. At a key's start,
2818    /// a probe moves an expression (it now follows the probe) but not a
2819    /// statement list whose first statement was probed, since the list still
2820    /// starts where the probe does; a list strictly containing the probed
2821    /// statement moves. An opener moves a key whose node it wraps (the node
2822    /// now sits inside the wrapper) and leaves alone a key whose node contains
2823    /// the wrapped one, since that node now begins with the wrapper. At a key's
2824    /// end, only a closer whose opener lies inside the key extends it: a list
2825    /// includes a wrapper around its last statement, an expression does not
2826    /// include the wrapper around itself. A point key follows everything
2827    /// inserted up to it, closers included.
2828    fn shifted(&self, span: PlanSpan, kind: KeyKind, edits: &[PendingEdit]) -> PlanSpan {
2829        let start_offset = self.line_starts[span.start[0] - 1] + span.start[1];
2830        let end_offset = self.line_starts[span.end[0] - 1] + span.end[1];
2831        let mut start_shift = 0;
2832        let mut end_shift = 0;
2833        for edit in edits {
2834            let (line, _) = self.line_column(edit.offset);
2835            let moves_start = edit.offset < start_offset
2836                || (edit.offset == start_offset
2837                    && match (edit.rank, kind) {
2838                        (_, KeyKind::Point) => true,
2839                        (EditRank::Closer, _) => false,
2840                        (EditRank::Opener, KeyKind::List) => end_offset < edit.scope,
2841                        (EditRank::Opener, KeyKind::Node) => end_offset <= edit.scope,
2842                        (_, KeyKind::List) => end_offset < edit.scope,
2843                        (_, KeyKind::Node) => true,
2844                    });
2845            let moves_end = edit.offset < end_offset
2846                || (edit.offset == end_offset
2847                    && match (edit.rank, kind) {
2848                        (_, KeyKind::Point) => true,
2849                        (EditRank::Closer, KeyKind::List) => edit.scope >= start_offset,
2850                        (EditRank::Closer, KeyKind::Node) => edit.scope > start_offset,
2851                        _ => false,
2852                    });
2853            if line == span.start[0] && moves_start {
2854                start_shift += edit.text.len();
2855            }
2856            if line == span.end[0] && moves_end {
2857                end_shift += edit.text.len();
2858            }
2859        }
2860        PlanSpan {
2861            start: [span.start[0], span.start[1] + start_shift],
2862            end: [span.end[0], span.end[1] + end_shift],
2863        }
2864    }
2865
2866    /// Remove every insertion inside a `Ractor.new` block. What those probes
2867    /// alone would have proven -- and what their observations would have
2868    /// implied -- leaves the denominator, declared at the block; lines stay.
2869    fn drop_ractor_insertions(
2870        &mut self,
2871        pending: Vec<PendingEdit>,
2872        blocks: &[(usize, usize)],
2873    ) -> Vec<PendingEdit> {
2874        let inside = |offset: usize| {
2875            blocks
2876                .iter()
2877                .any(|(start, end)| offset >= *start && offset < *end)
2878        };
2879        let (dropped, kept): (Vec<_>, Vec<_>) =
2880            pending.into_iter().partition(|edit| inside(edit.offset));
2881        let mut keys = BTreeSet::new();
2882        for edit in &dropped {
2883            keys.extend(probe_keys_in(&edit.text));
2884        }
2885        let mut targets = BTreeMap::new();
2886        for key in &keys {
2887            if let Some(target) = self.probes.remove(key) {
2888                targets.insert(*key, target);
2889            }
2890        }
2891        let mut unmeasured = probe_obligations_of(&targets);
2892        for target in targets.values() {
2893            for named in target_ids(target) {
2894                if let Some(implied) = self.implied.get(&named) {
2895                    unmeasured.extend(implied.hits.iter().map(|id| obligation_of(id)));
2896                    unmeasured.extend(implied.decisions.iter().map(|d| d.id.clone()));
2897                }
2898            }
2899        }
2900        unmeasured.sort();
2901        unmeasured.dedup();
2902        self.manifest.unmeasured.extend(unmeasured);
2903        for (start, end) in blocks {
2904            let (line, _) = self.line_column(*start);
2905            let source = self
2906                .text(*start, *end)
2907                .lines()
2908                .next()
2909                .unwrap_or_default()
2910                .to_owned();
2911            self.manifest.limitations.push(limitation(
2912                RACTOR_BLOCK_LIMITATION,
2913                self.file,
2914                line,
2915                &source,
2916                "a Ractor block cannot call Supercov's probes (a non-main Ractor cannot read the probe receiver), so what only a probe could prove inside it is unmeasured; its lines are still counted",
2917            ));
2918        }
2919        kept
2920    }
2921
2922    fn finish(mut self) -> RubyFileObligations {
2923        let mut pending = std::mem::take(&mut self.edits);
2924        pending.sort_by(|left, right| {
2925            left.offset
2926                .cmp(&right.offset)
2927                .then(left.rank.cmp(&right.rank))
2928                .then(left.order.cmp(&right.order))
2929                .then(left.sequence.cmp(&right.sequence))
2930        });
2931        let ractor_blocks = std::mem::take(&mut self.ractor_blocks);
2932        if !ractor_blocks.is_empty() {
2933            pending = self.drop_ractor_insertions(pending, &ractor_blocks);
2934        }
2935        let branches = std::mem::take(&mut self.branches)
2936            .into_iter()
2937            .map(|mut branch| {
2938                branch.key.span = self.shifted(branch.key.span, branch.key.kind, &pending);
2939                branch
2940            })
2941            .collect::<Vec<_>>();
2942        let cases: Vec<CasePlan> = std::mem::take(&mut self.cases)
2943            .into_iter()
2944            .map(|mut case| {
2945                for clause in &mut case.clauses {
2946                    clause.key.span = self.shifted(clause.key.span, clause.key.kind, &pending);
2947                }
2948                if let Some(no_match) = &mut case.no_match {
2949                    no_match.key.span =
2950                        self.shifted(no_match.key.span, no_match.key.kind, &pending);
2951                }
2952                case
2953            })
2954            .collect();
2955        let methods: Vec<MethodKeyPlan> = std::mem::take(&mut self.methods)
2956            .into_iter()
2957            .map(|mut method| {
2958                method.span = self.shifted(method.span, KeyKind::Node, &pending);
2959                method
2960            })
2961            .collect();
2962        let edits = pending
2963            .into_iter()
2964            .map(|edit| Edit {
2965                offset: edit.offset,
2966                text: edit.text,
2967                rank: match edit.rank {
2968                    EditRank::Clause => "clause",
2969                    EditRank::StatementProbe => "statement",
2970                    EditRank::Opener => "opener",
2971                    EditRank::Closer => "closer",
2972                }
2973                .into(),
2974                scope: edit.scope,
2975            })
2976            .collect::<Vec<_>>();
2977        if let Some((id, line)) = self.begin_unmeasured.first() {
2978            let source = self
2979                .manifest
2980                .branches
2981                .iter()
2982                .find(|branch| &branch.id == id)
2983                .map(|branch| branch.source.lines().next().unwrap_or_default().to_owned())
2984                .unwrap_or_default();
2985            self.manifest.limitations.push(limitation(
2986                BEGIN_BODY_LIMITATION,
2987                self.file,
2988                *line,
2989                &source,
2990                "a begin body that is empty, or ends in a statement with no expression form, cannot have its completion observed",
2991            ));
2992        }
2993        self.manifest.unmeasured.sort();
2994        self.manifest.unmeasured.dedup();
2995        RubyFileObligations {
2996            manifest: self.manifest,
2997            plan: RubyFilePlan {
2998                refine_blocks: std::mem::take(&mut self.refine_blocks),
2999                ractor_blocks: ractor_blocks
3000                    .iter()
3001                    .map(|(start, end)| [*start, *end])
3002                    .collect(),
3003                // What only a probe proves on 3.3: the probe-provable set
3004                // minus what 3.3 still proves through Coverage's keys.
3005                probe_obligations: {
3006                    let keyed = stdlib_provable(&branches, &cases, &methods);
3007                    probe_obligations_of(&self.probes)
3008                        .into_iter()
3009                        .filter(|id| !keyed.contains(id))
3010                        .collect()
3011                },
3012                edits,
3013                lines: self.lines,
3014                statement_offsets: self.statement_offsets,
3015                branches,
3016                methods,
3017                cases,
3018                implied: self.implied,
3019            },
3020            probes: self.probes,
3021        }
3022    }
3023}
3024
3025fn limitation(id: &str, file: &str, line: usize, source: &str, reason: &str) -> serde_json::Value {
3026    json!({
3027        "id": id,
3028        "kind": "semantic-safety",
3029        "file": file,
3030        "line": line,
3031        "column": 0,
3032        "source": source,
3033        "reason": reason
3034    })
3035}
3036
3037impl<'pr> Visit<'pr> for Collector<'_> {
3038    fn visit_statements_node(&mut self, node: &StatementsNode<'pr>) {
3039        if !self
3040            .expression_lists
3041            .contains(&node.location().start_offset())
3042        {
3043            self.statements(node);
3044        }
3045        ruby_prism::visit_statements_node(self, node);
3046    }
3047
3048    fn visit_parentheses_node(&mut self, node: &ruby_prism::ParenthesesNode<'pr>) {
3049        if let Some(body) = node.body() {
3050            self.expression_lists.insert(body.location().start_offset());
3051        }
3052        ruby_prism::visit_parentheses_node(self, node);
3053    }
3054
3055    fn visit_embedded_statements_node(&mut self, node: &ruby_prism::EmbeddedStatementsNode<'pr>) {
3056        if let Some(statements) = node.statements() {
3057            self.expression_lists
3058                .insert(statements.location().start_offset());
3059        }
3060        ruby_prism::visit_embedded_statements_node(self, node);
3061    }
3062
3063    fn visit_if_node(&mut self, node: &IfNode<'pr>) {
3064        let offset = node.location().start_offset();
3065        if self.guard_nodes.contains(&offset) {
3066            // A `case/in` guard: Prism hands the guarded pattern over as the
3067            // guard's statements, but a pattern is not a statement. Only the
3068            // predicate holds code.
3069            self.depth += 1;
3070            self.visit(&node.predicate());
3071            self.depth -= 1;
3072            return;
3073        }
3074        if self.elsif_nodes.contains(&offset) {
3075            self.depth += 1;
3076            ruby_prism::visit_if_node(self, node);
3077            self.depth -= 1;
3078            return;
3079        }
3080        if let Some(truthy) = self.literal_truth(node.predicate()) {
3081            // Ruby compiles only the live arm of `if false` / `if true` and
3082            // reports no branch for it; the dead arm is not code that can run.
3083            // A folded `x and false` still evaluates `x`, whose own code is
3084            // measured like any expression.
3085            self.depth += 1;
3086            self.visit(&node.predicate());
3087            // A ternary's arms are expressions here too: probed as statements,
3088            // `true ? :yes : :no` became `true ? hit; :yes : :no`, which Ruby
3089            // refuses, and the whole file lost its branches.
3090            if node.if_keyword_loc().is_none() {
3091                if let Some(statements) = node.statements() {
3092                    self.expression_lists
3093                        .insert(statements.location().start_offset());
3094                }
3095                if let Some(subsequent) = node.subsequent()
3096                    && let Some(else_node) = subsequent.as_else_node()
3097                    && let Some(statements) = else_node.statements()
3098                {
3099                    self.expression_lists
3100                        .insert(statements.location().start_offset());
3101                }
3102            }
3103            if truthy {
3104                if let Some(statements) = node.statements() {
3105                    self.visit_statements_node(&statements);
3106                }
3107            } else if let Some(subsequent) = node.subsequent() {
3108                match subsequent.as_if_node() {
3109                    Some(elsif) => self.visit_if_node(&elsif),
3110                    None => {
3111                        if let Some(else_node) = subsequent.as_else_node()
3112                            && let Some(statements) = else_node.statements()
3113                        {
3114                            self.visit_statements_node(&statements);
3115                        }
3116                    }
3117                }
3118            }
3119            self.depth -= 1;
3120            return;
3121        }
3122        // Ternaries have no `if` keyword; `elsif` is reached through
3123        // `subsequent` and handled by the parent's chain walk below.
3124        let kind = if node.if_keyword_loc().is_none() {
3125            "ternary"
3126        } else {
3127            "if"
3128        };
3129        self.if_node(node, kind);
3130        let mut subsequent = node.subsequent();
3131        while let Some(next) = subsequent {
3132            match next.as_if_node() {
3133                Some(elsif) => {
3134                    self.elsif_nodes.insert(elsif.location().start_offset());
3135                    self.if_node(&elsif, "elsif");
3136                    subsequent = elsif.subsequent();
3137                }
3138                None => break,
3139            }
3140        }
3141        self.depth += 1;
3142        // Children: predicate, statements, then the chain. Elsif nodes are
3143        // visited as children here too, but `if_node` deduplicates by
3144        // decision id.
3145        ruby_prism::visit_if_node(self, node);
3146        self.depth -= 1;
3147    }
3148
3149    fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
3150        if self.guard_nodes.contains(&node.location().start_offset()) {
3151            self.depth += 1;
3152            self.visit(&node.predicate());
3153            self.depth -= 1;
3154            return;
3155        }
3156        if let Some(truthy) = self.literal_truth(node.predicate()) {
3157            self.depth += 1;
3158            self.visit(&node.predicate());
3159            if truthy {
3160                if let Some(else_node) = node.else_clause()
3161                    && let Some(statements) = else_node.statements()
3162                {
3163                    self.visit_statements_node(&statements);
3164                }
3165            } else if let Some(statements) = node.statements() {
3166                self.visit_statements_node(&statements);
3167            }
3168            self.depth -= 1;
3169            return;
3170        }
3171        self.unless_node(node);
3172        self.depth += 1;
3173        ruby_prism::visit_unless_node(self, node);
3174        self.depth -= 1;
3175    }
3176
3177    fn visit_while_node(&mut self, node: &WhileNode<'pr>) {
3178        self.loop_node(
3179            &node.location(),
3180            node.predicate(),
3181            node.statements(),
3182            node.is_begin_modifier(),
3183            false,
3184        );
3185        self.depth += 1;
3186        ruby_prism::visit_while_node(self, node);
3187        self.depth -= 1;
3188    }
3189
3190    fn visit_until_node(&mut self, node: &UntilNode<'pr>) {
3191        self.loop_node(
3192            &node.location(),
3193            node.predicate(),
3194            node.statements(),
3195            node.is_begin_modifier(),
3196            true,
3197        );
3198        self.depth += 1;
3199        ruby_prism::visit_until_node(self, node);
3200        self.depth -= 1;
3201    }
3202
3203    fn visit_for_node(&mut self, node: &ForNode<'pr>) {
3204        self.for_node(node);
3205        self.depth += 1;
3206        ruby_prism::visit_for_node(self, node);
3207        self.depth -= 1;
3208    }
3209
3210    fn visit_case_node(&mut self, node: &CaseNode<'pr>) {
3211        self.case_node(node);
3212        self.depth += 1;
3213        ruby_prism::visit_case_node(self, node);
3214        self.depth -= 1;
3215    }
3216
3217    fn visit_case_match_node(&mut self, node: &CaseMatchNode<'pr>) {
3218        self.case_match_node(node);
3219        self.depth += 1;
3220        ruby_prism::visit_case_match_node(self, node);
3221        self.depth -= 1;
3222    }
3223
3224    fn visit_and_node(&mut self, node: &AndNode<'pr>) {
3225        let location = node.location();
3226        if !self.tree_logicals.contains(&location.start_offset()) {
3227            let left = node.left();
3228            self.value_logical("and", &left, location.start_offset(), location.end_offset());
3229            self.value_decision(|| node.left());
3230        }
3231        self.depth += 1;
3232        ruby_prism::visit_and_node(self, node);
3233        self.depth -= 1;
3234    }
3235
3236    fn visit_or_node(&mut self, node: &OrNode<'pr>) {
3237        let location = node.location();
3238        if !self.tree_logicals.contains(&location.start_offset()) {
3239            let left = node.left();
3240            self.value_logical("or", &left, location.start_offset(), location.end_offset());
3241            self.value_decision(|| node.left());
3242        }
3243        self.depth += 1;
3244        ruby_prism::visit_or_node(self, node);
3245        self.depth -= 1;
3246    }
3247
3248    fn visit_call_node(&mut self, node: &CallNode<'pr>) {
3249        if node.name().as_slice() == b"!" && node.arguments().is_none() && node.block().is_none() {
3250            self.value_decision(|| node.as_node());
3251        }
3252        if node.name().as_slice() == b"new"
3253            && node.receiver().is_some_and(|receiver| {
3254                receiver
3255                    .as_constant_read_node()
3256                    .is_some_and(|constant| constant.name().as_slice() == b"Ractor")
3257            })
3258            && let Some(block) = node.block()
3259            && block.as_block_node().is_some()
3260        {
3261            let location = block.location();
3262            self.ractor_blocks
3263                .push((location.start_offset(), location.end_offset()));
3264        }
3265        if node.name().as_slice() == b"refine"
3266            && node.receiver().is_none()
3267            && let Some(block) = node.block()
3268            && block.as_block_node().is_some()
3269        {
3270            let location = block.location();
3271            self.refine_blocks
3272                .push([location.start_offset(), location.end_offset()]);
3273        }
3274        if node.is_safe_navigation() {
3275            self.safe_navigation(node);
3276        }
3277        if let Some(block) = node.block()
3278            && let Some(block) = block.as_block_node()
3279            && let Some(receiver) = node.receiver()
3280            && !node.is_safe_navigation()
3281            && ITERATORS.contains(&node.name().as_slice())
3282        {
3283            self.iterator_loop(node, &receiver, &block);
3284        }
3285        self.depth += 1;
3286        ruby_prism::visit_call_node(self, node);
3287        self.depth -= 1;
3288    }
3289
3290    fn visit_local_variable_or_write_node(
3291        &mut self,
3292        node: &ruby_prism::LocalVariableOrWriteNode<'pr>,
3293    ) {
3294        let location = node.location();
3295        let value = node.value();
3296        self.op_assign(
3297            "or",
3298            location.start_offset(),
3299            location.end_offset(),
3300            Some(node.name().as_slice()),
3301            &value,
3302        );
3303        self.depth += 1;
3304        ruby_prism::visit_local_variable_or_write_node(self, node);
3305        self.depth -= 1;
3306    }
3307
3308    fn visit_local_variable_and_write_node(
3309        &mut self,
3310        node: &ruby_prism::LocalVariableAndWriteNode<'pr>,
3311    ) {
3312        let location = node.location();
3313        let value = node.value();
3314        self.op_assign(
3315            "and",
3316            location.start_offset(),
3317            location.end_offset(),
3318            Some(node.name().as_slice()),
3319            &value,
3320        );
3321        self.depth += 1;
3322        ruby_prism::visit_local_variable_and_write_node(self, node);
3323        self.depth -= 1;
3324    }
3325
3326    fn visit_instance_variable_or_write_node(
3327        &mut self,
3328        node: &ruby_prism::InstanceVariableOrWriteNode<'pr>,
3329    ) {
3330        let location = node.location();
3331        let value = node.value();
3332        self.op_assign(
3333            "or",
3334            location.start_offset(),
3335            location.end_offset(),
3336            Some(node.name().as_slice()),
3337            &value,
3338        );
3339        self.depth += 1;
3340        ruby_prism::visit_instance_variable_or_write_node(self, node);
3341        self.depth -= 1;
3342    }
3343
3344    fn visit_instance_variable_and_write_node(
3345        &mut self,
3346        node: &ruby_prism::InstanceVariableAndWriteNode<'pr>,
3347    ) {
3348        let location = node.location();
3349        let value = node.value();
3350        self.op_assign(
3351            "and",
3352            location.start_offset(),
3353            location.end_offset(),
3354            Some(node.name().as_slice()),
3355            &value,
3356        );
3357        self.depth += 1;
3358        ruby_prism::visit_instance_variable_and_write_node(self, node);
3359        self.depth -= 1;
3360    }
3361
3362    fn visit_global_variable_or_write_node(
3363        &mut self,
3364        node: &ruby_prism::GlobalVariableOrWriteNode<'pr>,
3365    ) {
3366        let location = node.location();
3367        let value = node.value();
3368        self.op_assign(
3369            "or",
3370            location.start_offset(),
3371            location.end_offset(),
3372            Some(node.name().as_slice()),
3373            &value,
3374        );
3375        self.depth += 1;
3376        ruby_prism::visit_global_variable_or_write_node(self, node);
3377        self.depth -= 1;
3378    }
3379
3380    fn visit_class_variable_or_write_node(
3381        &mut self,
3382        node: &ruby_prism::ClassVariableOrWriteNode<'pr>,
3383    ) {
3384        let location = node.location();
3385        let value = node.value();
3386        self.op_assign(
3387            "or",
3388            location.start_offset(),
3389            location.end_offset(),
3390            // Reading an uninitialized class variable raises (unlike an
3391            // instance or global variable), so it cannot be re-read up front;
3392            // the arrival form counts the skipped side instead. Found by the
3393            // corpus sweep on railties' `@@extensions ||= {}`.
3394            None,
3395            &value,
3396        );
3397        self.depth += 1;
3398        ruby_prism::visit_class_variable_or_write_node(self, node);
3399        self.depth -= 1;
3400    }
3401
3402    fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode<'pr>) {
3403        let location = node.location();
3404        let value = node.value();
3405        self.op_assign(
3406            "or",
3407            location.start_offset(),
3408            location.end_offset(),
3409            None,
3410            &value,
3411        );
3412        self.depth += 1;
3413        ruby_prism::visit_call_or_write_node(self, node);
3414        self.depth -= 1;
3415    }
3416
3417    fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode<'pr>) {
3418        let location = node.location();
3419        let value = node.value();
3420        self.op_assign(
3421            "and",
3422            location.start_offset(),
3423            location.end_offset(),
3424            None,
3425            &value,
3426        );
3427        self.depth += 1;
3428        ruby_prism::visit_call_and_write_node(self, node);
3429        self.depth -= 1;
3430    }
3431
3432    fn visit_index_or_write_node(&mut self, node: &ruby_prism::IndexOrWriteNode<'pr>) {
3433        let location = node.location();
3434        let value = node.value();
3435        self.op_assign(
3436            "or",
3437            location.start_offset(),
3438            location.end_offset(),
3439            None,
3440            &value,
3441        );
3442        self.depth += 1;
3443        ruby_prism::visit_index_or_write_node(self, node);
3444        self.depth -= 1;
3445    }
3446
3447    fn visit_index_and_write_node(&mut self, node: &ruby_prism::IndexAndWriteNode<'pr>) {
3448        let location = node.location();
3449        let value = node.value();
3450        self.op_assign(
3451            "and",
3452            location.start_offset(),
3453            location.end_offset(),
3454            None,
3455            &value,
3456        );
3457        self.depth += 1;
3458        ruby_prism::visit_index_and_write_node(self, node);
3459        self.depth -= 1;
3460    }
3461
3462    fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode<'pr>) {
3463        let location = node.location();
3464        let value = node.value();
3465        self.op_assign(
3466            "or",
3467            location.start_offset(),
3468            location.end_offset(),
3469            None,
3470            &value,
3471        );
3472        self.depth += 1;
3473        ruby_prism::visit_constant_or_write_node(self, node);
3474        self.depth -= 1;
3475    }
3476
3477    fn visit_def_node(&mut self, node: &DefNode<'pr>) {
3478        self.def_node(node);
3479        self.depth += 1;
3480        ruby_prism::visit_def_node(self, node);
3481        self.depth -= 1;
3482    }
3483
3484    fn visit_begin_node(&mut self, node: &BeginNode<'pr>) {
3485        // Explicit `begin ... end`. Method and block bodies reach `begin_node`
3486        // through their hosts, which know the closing keyword; the branch id
3487        // dedupes the second visit.
3488        if node.begin_keyword_loc().is_some() {
3489            self.begin_node(node, None);
3490        }
3491        self.depth += 1;
3492        ruby_prism::visit_begin_node(self, node);
3493        self.depth -= 1;
3494    }
3495
3496    fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
3497        if let Some(body) = node.body()
3498            && let Some(begin) = body.as_begin_node()
3499        {
3500            self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3501        }
3502        self.depth += 1;
3503        ruby_prism::visit_block_node(self, node);
3504        self.depth -= 1;
3505    }
3506
3507    fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
3508        if let Some(body) = node.body()
3509            && let Some(begin) = body.as_begin_node()
3510        {
3511            self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3512        }
3513        self.depth += 1;
3514        ruby_prism::visit_lambda_node(self, node);
3515        self.depth -= 1;
3516    }
3517
3518    fn visit_rescue_modifier_node(&mut self, node: &RescueModifierNode<'pr>) {
3519        self.rescue_modifier(node);
3520        self.depth += 1;
3521        ruby_prism::visit_rescue_modifier_node(self, node);
3522        self.depth -= 1;
3523    }
3524}
3525
3526/// Build the complete obligation manifest and probe plan for one Ruby file.
3527/// `next_probe` numbers probes uniquely across the whole project.
3528pub fn build_ruby_obligations(
3529    file: &str,
3530    source: &[u8],
3531    next_probe: &mut u64,
3532) -> Result<RubyFileObligations, RubyInstrumenterError> {
3533    let result = ruby_prism::parse(source);
3534    let errors = result
3535        .errors()
3536        .map(|error| error.message().to_owned())
3537        .collect::<Vec<_>>();
3538    if !errors.is_empty() {
3539        return Err(RubyInstrumenterError::Parse(errors.join("; ")));
3540    }
3541    let mut collector = Collector::new(file, source, next_probe);
3542    collector.visit(&result.node());
3543    if let Some(error) = collector.error.take() {
3544        return Err(error);
3545    }
3546    Ok(collector.finish())
3547}
3548
3549/// Apply a plan's edits to the original source the way the runtime does.
3550pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
3551    let mut output =
3552        Vec::with_capacity(source.len() + edits.iter().map(|e| e.text.len()).sum::<usize>());
3553    let mut cursor = 0;
3554    for edit in edits {
3555        output.extend_from_slice(&source[cursor..edit.offset]);
3556        output.extend_from_slice(edit.text.as_bytes());
3557        cursor = edit.offset;
3558    }
3559    output.extend_from_slice(&source[cursor..]);
3560    output
3561}
3562
3563#[cfg(test)]
3564mod tests {
3565    use super::*;
3566
3567    #[test]
3568    fn a_ternary_on_a_literal_still_parses_once_probed() {
3569        // Ruby compiles only the live arm of `true ? a : b`; that arm alone is
3570        // probed. Probed as a statement, the arm left `true ? hit; :yes : :no`,
3571        // which Ruby refuses, and the whole file lost its branches.
3572        for condition in ["true", "1", "1.0", "'s'", ":s", "false", "nil", "(true)"] {
3573            let source = format!("def go\n  x = {condition} ? :yes : :no\n  x\nend\n");
3574            let mut next = 0;
3575            let obligations =
3576                build_ruby_obligations("lib/l.rb", source.as_bytes(), &mut next).unwrap();
3577            let probed = apply_edits(source.as_bytes(), &obligations.plan.edits);
3578            let parsed = ruby_prism::parse(&probed);
3579            let errors = parsed
3580                .errors()
3581                .map(|error| error.message().to_owned())
3582                .collect::<Vec<_>>();
3583            assert!(
3584                errors.is_empty(),
3585                "{condition}: {errors:?}\n{}",
3586                String::from_utf8_lossy(&probed)
3587            );
3588        }
3589    }
3590
3591    const SOURCE: &str = r#"class Shapes
3592  def classify(a, b, c)
3593    if a && (b || c)
3594      :yes
3595    elsif a
3596      :half
3597    else
3598      :no
3599    end
3600  end
3601
3602  def loops(items, flag)
3603    total = 0
3604    items.each { |i| total += i if i > 2 && flag }
3605    while total > 100
3606      total -= 50
3607    end
3608    for x in items do total += x end
3609    total
3610  end
3611
3612  def logical(a, b)
3613    x = a || b
3614    @cache ||= {}
3615    @cache[a] ||= b
3616    y = a ? 1 : 2; z = a&.size
3617    [x, y, z]
3618  end
3619
3620  def guarded(s)
3621    Integer(s)
3622  rescue ArgumentError
3623    -1
3624  ensure
3625    @done = true
3626  end
3627
3628  def cases(v)
3629    case v
3630    when 0 then :zero
3631    else :other
3632    end
3633    v.to_s rescue "bad"
3634  end
3635end
3636"#;
3637
3638    #[test]
3639    fn discovers_obligations_with_stable_ids_and_newline_free_edits() {
3640        let mut probe = 0;
3641        let first = build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3642        let mut probe = 0;
3643        let second =
3644            build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3645        assert_eq!(first, second);
3646        let manifest = &first.manifest;
3647        let functions = manifest
3648            .points
3649            .iter()
3650            .filter(|point| point.kind == PointKind::Function)
3651            .map(|point| point.label.clone().unwrap())
3652            .collect::<Vec<_>>();
3653        assert_eq!(
3654            functions,
3655            ["classify", "loops", "logical", "guarded", "cases"]
3656        );
3657        let compound = manifest
3658            .decisions
3659            .iter()
3660            .find(|decision| decision.source == "a && (b || c)")
3661            .unwrap();
3662        assert_eq!(compound.conditions, ["a", "b", "c"]);
3663        assert_eq!(compound.kind, "if");
3664        assert!(
3665            manifest
3666                .decisions
3667                .iter()
3668                .any(|d| d.kind == "elsif" && d.conditions == ["a"])
3669        );
3670        assert!(manifest.decisions.iter().any(|d| d.kind == "ternary"));
3671        assert!(manifest.decisions.iter().any(|d| d.kind == "while"));
3672        for kind in [
3673            "for",
3674            "logical-or",
3675            "or-assign",
3676            "safe-navigation",
3677            "begin",
3678            "rescue-0",
3679            "case-when-0",
3680            "case-else",
3681            "rescue-modifier",
3682        ] {
3683            assert!(
3684                manifest.branches.iter().any(|branch| branch.kind == kind),
3685                "missing branch kind {kind}"
3686            );
3687        }
3688        for edit in &first.plan.edits {
3689            assert!(!edit.text.contains('\n'));
3690        }
3691        assert!(
3692            first
3693                .plan
3694                .edits
3695                .windows(2)
3696                .all(|pair| pair[0].offset <= pair[1].offset)
3697        );
3698        // `@cache[a] ||= b` is measured through arrival and right-side
3699        // probes; nothing is unmeasured.
3700        assert!(manifest.unmeasured.is_empty());
3701        assert!(manifest.limitations.is_empty());
3702    }
3703
3704    #[test]
3705    fn transformed_source_keeps_line_count_and_carries_probes() {
3706        let mut probe = 0;
3707        let obligations =
3708            build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3709        let transformed =
3710            String::from_utf8(apply_edits(SOURCE.as_bytes(), &obligations.plan.edits)).unwrap();
3711        assert_eq!(transformed.lines().count(), SOURCE.lines().count());
3712        assert!(
3713            transformed.contains("if $__supercov.d(0, ($__supercov.c(0, 0, (a)) && ($__supercov.c(0, 1, (b)) || $__supercov.c(0, 2, (c)))))"),
3714            "{transformed}"
3715        );
3716        assert!(transformed.contains("while $__supercov.w("));
3717        assert!(transformed.contains("for x in $__supercov.f("));
3718        assert!(transformed.contains("do $__supercov.fb("));
3719        assert!(transformed.contains("x = $__supercov.l("));
3720        assert!(transformed.contains("($__supercov.pre("));
3721        assert!(transformed.contains("||= ($__supercov.es("));
3722        assert!(transformed.contains("rescue Exception => __supercov_e; $__supercov.p("));
3723        assert!(transformed.contains("$__supercov.h("));
3724        assert!(transformed.contains("$__supercov.ok("));
3725        assert!(transformed.contains("rescue $__supercov.hm("));
3726        // The second statement on the `y = ...; z = ...` line gets a probe.
3727        assert!(transformed.contains("; $__supercov.s("));
3728        // Same-offset modifier bodies are proven by the stdlib branch key.
3729        assert!(
3730            obligations
3731                .plan
3732                .branches
3733                .iter()
3734                .any(|branch| !branch.hits.is_empty() && branch.key.group == "if")
3735        );
3736    }
3737
3738    #[test]
3739    fn jumps_and_endless_bodies_take_wrapped_probes_and_literal_predicates_fold() {
3740        let source = "def inc(x) = x + 1\n\
3741                      [1].each { |v| y = Integer(v) rescue next }\n\
3742                      if false\n  dead\nelse\n  live\nend\n";
3743        let mut probe = 0;
3744        let obligations =
3745            build_ruby_obligations("lib/x.rb", source.as_bytes(), &mut probe).unwrap();
3746        let transformed =
3747            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3748        assert!(
3749            transformed.contains("def inc(x) = ($__supercov.s("),
3750            "{transformed}"
3751        );
3752        assert!(
3753            transformed.contains("rescue ($__supercov.hm0("),
3754            "{transformed}"
3755        );
3756        // `if false` has no branch and its dead arm no statements.
3757        assert!(
3758            obligations
3759                .plan
3760                .branches
3761                .iter()
3762                .all(|branch| branch.key.group != "if")
3763        );
3764        assert!(
3765            obligations
3766                .manifest
3767                .points
3768                .iter()
3769                .all(|point| point.source != "dead")
3770        );
3771        assert!(
3772            obligations
3773                .manifest
3774                .points
3775                .iter()
3776                .any(|point| point.source == "live")
3777        );
3778    }
3779
3780    #[test]
3781    fn void_valued_last_statements_are_probed_arm_by_arm() {
3782        // Wrapping `if ... return ... else return ... end` in a value context
3783        // is a syntax error; each arm carries the completion probe instead.
3784        let source =
3785            "def m(c)\n  if c\n    return 1\n  else\n    return 2\n  end\nrescue\n  nil\nend\n";
3786        let mut probe = 0;
3787        let obligations =
3788            build_ruby_obligations("lib/v.rb", source.as_bytes(), &mut probe).unwrap();
3789        let transformed =
3790            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3791        assert_eq!(
3792            transformed.matches("$__supercov.ok(").count(),
3793            2,
3794            "{transformed}"
3795        );
3796        assert!(
3797            transformed.contains("return $__supercov.ok("),
3798            "{transformed}"
3799        );
3800        assert!(
3801            !obligations
3802                .manifest
3803                .unmeasured
3804                .iter()
3805                .any(|id| id.ends_with(":success")),
3806            "{:?}",
3807            obligations.manifest.unmeasured
3808        );
3809        assert!(!obligations.plan.probe_obligations.is_empty());
3810    }
3811
3812    #[test]
3813    fn expressions_that_can_return_are_never_wrapped() {
3814        // `ok(k, (if a then return b end))` passes an expression that can jump
3815        // as an argument, which makes Ruby's compiler miscount its stack.
3816        let source = "def m(a, d)\n  begin\n    if a\n      return d\n    end\n  ensure\n    unlock\n  end\n  d\nend\n";
3817        let mut probe = 0;
3818        let obligations =
3819            build_ruby_obligations("lib/e.rb", source.as_bytes(), &mut probe).unwrap();
3820        let transformed =
3821            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3822        assert!(!transformed.contains("$__supercov.ok"), "{transformed}");
3823        assert!(
3824            obligations
3825                .manifest
3826                .unmeasured
3827                .iter()
3828                .any(|id| id.ends_with(":success")),
3829            "the begin's completion is declared instead"
3830        );
3831        // With both arms present the arms carry the probe and nothing is lost.
3832        let both = "def m(a, d)\n  begin\n    if a\n      return d\n    else\n      d + 1\n    end\n  ensure\n    unlock\n  end\nend\n";
3833        let mut probe = 0;
3834        let obligations = build_ruby_obligations("lib/f.rb", both.as_bytes(), &mut probe).unwrap();
3835        let transformed =
3836            String::from_utf8(apply_edits(both.as_bytes(), &obligations.plan.edits)).unwrap();
3837        assert!(
3838            transformed.contains("return $__supercov.ok("),
3839            "{transformed}"
3840        );
3841        assert!(
3842            !obligations
3843                .manifest
3844                .unmeasured
3845                .iter()
3846                .any(|id| id.ends_with(":success")),
3847            "{:?}",
3848            obligations.manifest.unmeasured
3849        );
3850    }
3851
3852    #[test]
3853    fn stdlib_keys_shift_with_insertions_on_their_line() {
3854        let source = "def f(a, b)\n  x = 1 if a && b\nend\n";
3855        let mut probe = 0;
3856        let obligations = build_ruby_obligations("m.rb", source.as_bytes(), &mut probe).unwrap();
3857        let then_key = obligations
3858            .plan
3859            .branches
3860            .iter()
3861            .find(|branch| branch.key.branch == "then")
3862            .unwrap();
3863        // `x = 1` starts at column 2 and nothing is inserted before it.
3864        assert_eq!(then_key.key.span.start, [2, 2]);
3865        // Its end (column 7) is untouched too: insertions land in the
3866        // predicate, which comes after the body on this line.
3867        assert_eq!(then_key.key.span.end, [2, 7]);
3868        let else_key = obligations
3869            .plan
3870            .branches
3871            .iter()
3872            .find(|branch| branch.key.branch == "else")
3873            .unwrap();
3874        // The implicit else uses the whole if node, whose end moves right by
3875        // every inserted byte on that line.
3876        let inserted: usize = obligations
3877            .plan
3878            .edits
3879            .iter()
3880            .map(|edit| edit.text.len())
3881            .sum();
3882        assert_eq!(else_key.key.span.end, [2, 17 + inserted]);
3883        assert!(
3884            then_key.hits.len() == 1,
3885            "modifier body statement proven by the then key"
3886        );
3887    }
3888
3889    #[test]
3890    fn line_events_prove_bodies_and_probes_prove_the_rest() {
3891        // Ruby 3.4+ reads no branch or method keys. A body's first statement
3892        // implies the branch, the method and the decision outcome; an `if`
3893        // without `else`, a `&.` and a `case` without `else` are probed; the
3894        // ids Ruby 3.3 still proves through its keys are not declared as
3895        // probe-only.
3896        let source = "def both(a)\n  if a\n    1\n  else\n    2\n  end\nend\n\ndef guard(a)\n  return 0 if a\n  a&.size\nend\n\ndef pick(v)\n  case v\n  when 1 then :one\n  when 2\n    :two\n  end\nend\n\ndef short = 3\n\ndef empty; end\n";
3897        let mut probe = 0;
3898        let obligations =
3899            build_ruby_obligations("lib/m.rb", source.as_bytes(), &mut probe).unwrap();
3900        let plan = &obligations.plan;
3901        let manifest = &obligations.manifest;
3902        let function = |name: &str| {
3903            manifest
3904                .points
3905                .iter()
3906                .find(|point| {
3907                    point.kind == PointKind::Function && point.label.as_deref() == Some(name)
3908                })
3909                .unwrap()
3910                .id
3911                .clone()
3912        };
3913        let statement_on = |line: usize| plan.lines.get(&line).cloned().unwrap();
3914        let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3915        assert_eq!(transformed.lines().count(), source.lines().count());
3916
3917        // `if a ... else ... end`: line 3 proves `both` and the true outcome,
3918        // line 5 the false outcome; no predicate probe.
3919        assert_eq!(
3920            plan.implied[&statement_on(2)].hits,
3921            [function("both")],
3922            "the `if` statement opens the method"
3923        );
3924        let three = &plan.implied[&statement_on(3)];
3925        assert!(three.hits.is_empty());
3926        assert_eq!(three.decisions.len(), 1);
3927        assert!(three.decisions[0].value);
3928        let five = &plan.implied[&statement_on(5)];
3929        assert!(!five.decisions[0].value);
3930        assert!(!transformed.contains(".d(") || transformed.matches(".d(").count() == 1);
3931
3932        // `return 0 if a`: the predicate is probed (one `d`, no `c`), and the
3933        // body is implied by the true outcome, not by its line.
3934        let guard_decision = manifest
3935            .decisions
3936            .iter()
3937            .find(|d| d.kind == "if" && d.conditions == ["a"] && d.line == 10)
3938            .unwrap();
3939        let outcome_true = format!("{}:outcome:true", guard_decision.id);
3940        assert!(
3941            plan.implied.contains_key(&outcome_true),
3942            "modifier body implied by its outcome"
3943        );
3944        assert!(transformed.contains("return 0 if $__supercov.d("));
3945        assert!(
3946            !transformed.contains(".c("),
3947            "a lone condition needs no wrapper"
3948        );
3949        // `a&.size`: the receiver is probed.
3950        assert!(transformed.contains("$__supercov.n("));
3951        // `case` without `else` gets an else of its own; the one-line `when`
3952        // body is a probed statement.
3953        assert!(transformed.contains("else $__supercov.hs("));
3954        assert!(transformed.contains("when 1 then $__supercov.s("));
3955        // Endless and empty methods.
3956        assert!(transformed.contains("def short = ($__supercov.s("));
3957        assert!(transformed.contains("def empty; $__supercov.hs("));
3958        let empty_probe = plan
3959            .edits
3960            .iter()
3961            .find(|edit| edit.text.contains(".hs(") && edit.text.starts_with("; "))
3962            .unwrap();
3963        assert!(empty_probe.text.contains("hs"));
3964        // Everything 3.3 proves through keys stays out of the 3.3 declaration:
3965        // the modifier body statement, the guard decision, the `&.` branch,
3966        // the case alternatives and the methods.
3967        for id in [
3968            guard_decision.id.clone(),
3969            function("empty"),
3970            function("short"),
3971        ] {
3972            assert!(
3973                !plan.probe_obligations.contains(&id),
3974                "{id} is key-provable on 3.3"
3975            );
3976        }
3977        let safe = manifest
3978            .branches
3979            .iter()
3980            .find(|b| b.kind == "safe-navigation")
3981            .unwrap();
3982        assert!(!plan.probe_obligations.contains(&safe.id));
3983    }
3984
3985    #[test]
3986    fn ractor_blocks_get_no_probes_and_declare_what_only_probes_could_prove() {
3987        // A probe inside a Ractor reads a global the Ractor cannot see and
3988        // raises where the untouched program ran. Nothing is inserted inside
3989        // the block; the same shapes outside it are probed as ever.
3990        let source = "def inside(a)\n  Ractor.new(a) { |v| v ? 1 : 2 }.take\nend\n\ndef outside(a)\n  a ? 1 : 2\nend\n";
3991        let mut probe = 0;
3992        let obligations =
3993            build_ruby_obligations("lib/r.rb", source.as_bytes(), &mut probe).unwrap();
3994        let plan = &obligations.plan;
3995        let manifest = &obligations.manifest;
3996        let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3997        let block_line = transformed.lines().nth(1).unwrap();
3998        assert!(
3999            !block_line.contains("$__supercov"),
4000            "no probe inside the Ractor block: {block_line}"
4001        );
4002        assert!(
4003            transformed
4004                .lines()
4005                .nth(5)
4006                .unwrap()
4007                .contains("$__supercov.d("),
4008            "the ternary outside is probed"
4009        );
4010        let inside = manifest.decisions.iter().find(|d| d.line == 2).unwrap();
4011        assert!(
4012            manifest.unmeasured.contains(&inside.id),
4013            "the block's ternary leaves the denominator"
4014        );
4015        let outside = manifest.decisions.iter().find(|d| d.line == 6).unwrap();
4016        assert!(!manifest.unmeasured.contains(&outside.id));
4017        assert_eq!(plan.ractor_blocks.len(), 1);
4018        assert!(
4019            manifest
4020                .limitations
4021                .iter()
4022                .any(|l| l["id"] == RACTOR_BLOCK_LIMITATION && l["line"] == 2)
4023        );
4024        // Dropped probes are not in the plan either, so 3.3 declares nothing for them.
4025        assert!(!plan.probe_obligations.contains(&inside.id));
4026    }
4027
4028    #[test]
4029    fn an_empty_guarded_in_body_takes_its_probe_after_the_guard_wrapper() {
4030        // net-imap: `in Array if data.all? { ... }` with no body. The guard's
4031        // decision wrapper closes exactly where the body would start; the
4032        // body probe must come after it. Found by the corpus sweep.
4033        let source = "def f(data)\n  case data\n  in String then data = 1\n  in Array if data.all? { _1 > 0 }\n  else\n    raise TypeError\n  end\nend\n";
4034        let mut probe = 0;
4035        let obligations =
4036            build_ruby_obligations("lib/g.rb", source.as_bytes(), &mut probe).unwrap();
4037        let transformed =
4038            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
4039        let guard_line = transformed.lines().nth(3).unwrap();
4040        assert!(
4041            guard_line.contains(")) then $__supercov.hs("),
4042            "probe after the wrapper: {guard_line}"
4043        );
4044        assert!(
4045            !guard_line.contains("then $__supercov.hs(")
4046                || !guard_line.contains("hs(")
4047                || guard_line.matches(')').count() == guard_line.matches('(').count(),
4048            "balanced: {guard_line}"
4049        );
4050    }
4051
4052    #[test]
4053    fn a_class_variable_or_assignment_is_never_read_before_it_exists() {
4054        let source = "class C\n  def self.ext\n    @@ext ||= {}\n  end\nend\n";
4055        let mut probe = 0;
4056        let obligations =
4057            build_ruby_obligations("lib/c.rb", source.as_bytes(), &mut probe).unwrap();
4058        let transformed =
4059            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
4060        let line = transformed.lines().nth(2).unwrap();
4061        assert!(
4062            !line.contains(".l("),
4063            "no read of @@ext before the assignment: {line}"
4064        );
4065        assert!(
4066            line.contains(".pre(") && line.contains(".es("),
4067            "arrival form: {line}"
4068        );
4069    }
4070
4071    #[test]
4072    fn a_predicate_ruby_folds_leaves_no_obligations_in_the_dead_arm() {
4073        // optparse: `if Process.respond_to?(:fork) and false` -- Ruby never
4074        // compiles the then arm and reports no branch for the ternary inside
4075        // it, so neither is an obligation; the live operand still runs.
4076        let source = "def f(x)\n  if x and false\n    x ? 1 : 2\n  end\n  if x or true\n    3\n  else\n    4\n  end\nend\n";
4077        let mut probe = 0;
4078        let obligations =
4079            build_ruby_obligations("lib/f.rb", source.as_bytes(), &mut probe).unwrap();
4080        let manifest = &obligations.manifest;
4081        assert!(
4082            !manifest.decisions.iter().any(|d| d.kind == "ternary"),
4083            "the dead arm's ternary is not an obligation"
4084        );
4085        assert!(
4086            !manifest.decisions.iter().any(|d| d.kind == "if"),
4087            "a folded predicate is no decision"
4088        );
4089        assert!(
4090            !manifest.points.iter().any(|p| p.line == 8),
4091            "the dead else arm holds no statement"
4092        );
4093        assert!(
4094            manifest.points.iter().any(|p| p.line == 6),
4095            "the live arm does"
4096        );
4097    }
4098
4099    #[test]
4100    fn rejects_invalid_ruby() {
4101        let mut probe = 0;
4102        assert!(matches!(
4103            build_ruby_obligations("m.rb", b"def x(\n", &mut probe),
4104            Err(RubyInstrumenterError::Parse(_))
4105        ));
4106    }
4107}