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