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. Ruby's `Coverage` module already reports
9//! lines, `if`/`unless`/`case`/`&.` branches and method entry with byte
10//! columns, so those obligations are proven by matching its keys (shifted for
11//! the text the runtime inserts). What `Coverage` cannot see — the operands of
12//! `&&`/`||` for MC/DC, `||=`, loop entry, `rescue` flow and a second statement
13//! on a line — is proven by probe calls the runtime splices into the source in
14//! memory at load time. No insertion contains a newline, so line numbers,
15//! backtraces and the stdlib line table stay exact.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use ruby_prism::{
20    AndNode, BeginNode, CallNode, CaseMatchNode, CaseNode, DefNode, ForNode, IfNode, Location,
21    Node, OrNode, RescueModifierNode, RescueNode, StatementsNode, UnlessNode, UntilNode, Visit,
22    WhileNode,
23};
24use serde::{Deserialize, Serialize};
25use serde_json::json;
26use sha2::{Digest, Sha256};
27
28use crate::{
29    coverage_analysis::PointKind,
30    coverage_report::{
31        BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
32    },
33};
34
35pub const RUBY_PROBE_PLAN_VERSION: u32 = 1;
36/// Global the runtime binds its probe receiver to. Chosen to be unpronounceable
37/// in application code.
38pub const RUBY_PROBE_RECEIVER: &str = "$__supercov";
39
40/// Block-taking methods whose block runs once per element (or per count):
41/// the idiomatic Ruby loops. Methods that may call the block zero times on a
42/// non-empty receiver (`cycle`, `loop`, `lazy`) are deliberately absent.
43const ITERATORS: &[&[u8]] = &[
44    b"each",
45    b"each_with_index",
46    b"each_with_object",
47    b"each_pair",
48    b"each_key",
49    b"each_value",
50    b"each_char",
51    b"each_byte",
52    b"each_line",
53    b"each_slice",
54    b"each_cons",
55    b"each_entry",
56    b"each_index",
57    b"reverse_each",
58    b"map",
59    b"collect",
60    b"flat_map",
61    b"collect_concat",
62    b"filter_map",
63    b"select",
64    b"filter",
65    b"reject",
66    b"find",
67    b"detect",
68    b"find_index",
69    b"find_all",
70    b"all?",
71    b"any?",
72    b"none?",
73    b"one?",
74    b"count",
75    b"sum",
76    b"min_by",
77    b"max_by",
78    b"sort_by",
79    b"group_by",
80    b"partition",
81    b"inject",
82    b"reduce",
83    b"take_while",
84    b"drop_while",
85    b"times",
86    b"upto",
87    b"downto",
88    b"step",
89];
90pub const BEGIN_BODY_LIMITATION: &str = "ruby-begin-completion-unmeasured";
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum RubyInstrumenterError {
94    Parse(String),
95    InvalidRange,
96}
97
98impl std::fmt::Display for RubyInstrumenterError {
99    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::Parse(error) => write!(formatter, "Ruby parse failed: {error}"),
102            Self::InvalidRange => write!(formatter, "Ruby parser returned an invalid range"),
103        }
104    }
105}
106
107impl std::error::Error for RubyInstrumenterError {}
108
109/// A source span in one-based lines and zero-based byte columns, the units
110/// Ruby's `Coverage` module reports. Serialized as `[[line, col], [line, col]]`.
111/// What a stdlib key's span names.
112#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(rename_all = "camelCase")]
114pub enum KeyKind {
115    /// A statement list (`then`, `else`, `when`, `in`, a loop body): Ruby
116    /// spans it from its first statement's start to its last statement's
117    /// end, so a probe or wrapper on its first statement becomes part of it.
118    List,
119    /// One expression node: a wrapper around it is a different node.
120    Node,
121    /// A zero-width position at the end of a predicate, which Ruby reports
122    /// for an `if` without a body; it follows anything inserted up to there.
123    Point,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
127#[serde(from = "[[usize; 2]; 2]", into = "[[usize; 2]; 2]")]
128pub struct PlanSpan {
129    pub start: [usize; 2],
130    pub end: [usize; 2],
131}
132
133impl From<[[usize; 2]; 2]> for PlanSpan {
134    fn from(value: [[usize; 2]; 2]) -> Self {
135        Self {
136            start: value[0],
137            end: value[1],
138        }
139    }
140}
141
142impl From<PlanSpan> for [[usize; 2]; 2] {
143    fn from(value: PlanSpan) -> Self {
144        [value.start, value.end]
145    }
146}
147
148/// One text insertion the runtime applies to the original source before
149/// compiling it. Offsets are bytes into the original file; the runtime applies
150/// insertions from the end of the file backwards, so offsets stay valid.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase", deny_unknown_fields)]
153pub struct Edit {
154    pub offset: usize,
155    pub text: String,
156    /// `clause`, `statement`, `opener` or `closer`: how the insertion sits
157    /// relative to the node at its offset, which decides whether keys
158    /// starting or ending there move (see [`Collector::shifted`]).
159    pub rank: String,
160    /// The other end of the range the insertion belongs to: the end of the
161    /// wrapped node or probed statement for an opener or probe, the opener's
162    /// offset for a closer.
163    pub scope: usize,
164}
165
166/// A `Coverage` branch key: the group type (`if`, `case`, `&.`, `while`), the
167/// branch type (`then`, `else`, `when`, `in`, `body`) and the branch span in
168/// post-insertion coordinates.
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct StdlibKey {
172    pub group: String,
173    pub branch: String,
174    /// What the key's span names, which decides how insertions at its edges
175    /// move it (see [`Collector::shifted`]).
176    pub kind: KeyKind,
177    /// Span after the plan's insertions are applied.
178    pub span: PlanSpan,
179    /// Span in the untouched source, for interpreters that cannot apply the
180    /// insertions (Ruby 3.3 does not cover code compiled by a load hook).
181    pub unshifted: PlanSpan,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct StdlibDecision {
187    pub id: String,
188    pub value: bool,
189    pub outcome: String,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194pub struct BranchKeyPlan {
195    pub key: StdlibKey,
196    /// Obligation IDs proven when this branch executed: alternatives and any
197    /// statement whose first line is shared with an earlier statement.
198    pub hits: Vec<String>,
199    /// A single-condition decision whose vector this branch witnesses.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub decision: Option<StdlibDecision>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase", deny_unknown_fields)]
206pub struct MethodKeyPlan {
207    pub span: PlanSpan,
208    pub unshifted: PlanSpan,
209    pub id: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase", deny_unknown_fields)]
214pub struct CaseClausePlan {
215    pub key: StdlibKey,
216    pub missed: String,
217    pub selected: String,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct CaseNoMatchPlan {
223    pub key: StdlibKey,
224    pub matched: String,
225    pub unmatched: String,
226}
227
228/// `case` clauses are tested in order, so a clause was missed exactly when a
229/// later clause (or the implicit else) was selected. The runtime derives that
230/// per phase from the selected counts.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase", deny_unknown_fields)]
233pub struct CasePlan {
234    pub clauses: Vec<CaseClausePlan>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub no_match: Option<CaseNoMatchPlan>,
237}
238
239/// Short-circuit structure of a decision. Leaves are condition indexes.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(untagged)]
242pub enum ConditionTree {
243    Leaf(usize),
244    Node {
245        op: String,
246        items: Vec<ConditionTree>,
247        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
248        negate: bool,
249    },
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase", deny_unknown_fields)]
254pub struct DerivedLogical {
255    pub previous_leaves: Vec<usize>,
256    pub operand_leaves: Vec<usize>,
257    pub short_circuit: String,
258    pub evaluated: String,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase", deny_unknown_fields)]
263pub struct LoopTarget {
264    pub id: String,
265    pub zero: String,
266    pub entered: String,
267    /// `until` enters the body when the predicate is falsy.
268    pub until: bool,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase", deny_unknown_fields)]
273pub struct HandlerTarget {
274    pub id: String,
275    pub missed: String,
276    pub selected: String,
277}
278
279/// What a probe call reports. The runtime looks the key up and records the
280/// obligations named here.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
283pub enum ProbeTarget {
284    /// `s(k)`: a statement sharing a line with an earlier statement ran.
285    #[serde(rename_all = "camelCase")]
286    Statement { id: String },
287    /// `c(k, i, v)` per condition, `d(k, v)` or `w(k, v)` for the outcome.
288    #[serde(rename_all = "camelCase")]
289    Decision {
290        id: String,
291        width: usize,
292        not: Vec<bool>,
293        tree: ConditionTree,
294        outcome_true: String,
295        outcome_false: String,
296        logical: Vec<DerivedLogical>,
297        #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
298        loop_: Option<LoopTarget>,
299    },
300    /// `f(k, collection)` at the loop head and `fb(k)` as the first body statement.
301    #[serde(rename_all = "camelCase")]
302    For {
303        id: String,
304        zero: String,
305        entered: String,
306    },
307    /// `l(k, left)` for value-context `&&`/`||`/`||=`/`&&=`.
308    #[serde(rename_all = "camelCase")]
309    Logical {
310        op: String,
311        short_circuit: String,
312        evaluated: String,
313    },
314    /// `pre(k)` before an operator assignment whose target cannot be re-read
315    /// without side effects, `es(k)` as the first thing its right side does:
316    /// arrivals that never started the right side are short-circuits.
317    #[serde(rename_all = "camelCase")]
318    Arrival {
319        short_circuit: String,
320        evaluated: String,
321    },
322    /// `ok(k, v)`/`ok0(k)` completion, `h(k, n)` handler entry, `p(k)`
323    /// propagation, `hm(k, v)` rescue-modifier fallback.
324    #[serde(rename_all = "camelCase")]
325    Try {
326        id: String,
327        success: String,
328        raised: String,
329        handlers: Vec<HandlerTarget>,
330    },
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
334#[serde(rename_all = "camelCase", deny_unknown_fields)]
335pub struct RubyFilePlan {
336    pub edits: Vec<Edit>,
337    /// Line -> statement id for statements that own their first line.
338    pub lines: BTreeMap<usize, String>,
339    /// Statement id -> [start, end] byte offsets for line-owned statements,
340    /// so the runtime can probe one whose first line Ruby turns out not to
341    /// count (`begin`, `case` without subject, multi-line literals).
342    pub statement_offsets: BTreeMap<String, [usize; 2]>,
343    pub branches: Vec<BranchKeyPlan>,
344    pub methods: Vec<MethodKeyPlan>,
345    pub cases: Vec<CasePlan>,
346    /// This file's share of [`RubyProbePlan::probe_obligations`], so a file
347    /// the runtime fails to instrument can declare exactly its own.
348    #[serde(default)]
349    pub probe_obligations: Vec<String>,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase", deny_unknown_fields)]
354pub struct RubyProbePlan {
355    pub version: u32,
356    pub root: String,
357    pub receiver: String,
358    pub files: BTreeMap<String, RubyFilePlan>,
359    pub probes: BTreeMap<u64, ProbeTarget>,
360    /// See [`RubyProbePlan::probe_obligations`]; stored so the runtime can
361    /// declare them without re-deriving alternative ids.
362    #[serde(default)]
363    pub probe_obligations: Vec<String>,
364}
365
366impl RubyProbePlan {
367    /// Manifest obligations (points, decisions, branches) that only a probe
368    /// can prove. An interpreter that cannot apply the insertions reports
369    /// them as unmeasured.
370    pub fn probe_obligations(&self) -> Vec<String> {
371        probe_obligations_of(&self.probes)
372    }
373}
374
375/// Manifest obligations (points, decisions, branches) that only a probe can
376/// prove.
377fn probe_obligations_of(probes: &BTreeMap<u64, ProbeTarget>) -> Vec<String> {
378    {
379        let mut ids = BTreeSet::new();
380        for target in probes.values() {
381            match target {
382                ProbeTarget::Statement { id } => {
383                    ids.insert(id.clone());
384                }
385                ProbeTarget::Decision {
386                    id,
387                    outcome_true,
388                    logical,
389                    loop_,
390                    ..
391                } => {
392                    ids.insert(id.clone());
393                    ids.insert(branch_of(outcome_true));
394                    for derived in logical {
395                        ids.insert(branch_of(&derived.short_circuit));
396                    }
397                    if let Some(loop_) = loop_ {
398                        ids.insert(loop_.id.clone());
399                    }
400                }
401                ProbeTarget::For { id, .. } => {
402                    ids.insert(id.clone());
403                }
404                ProbeTarget::Logical { short_circuit, .. } => {
405                    ids.insert(branch_of(short_circuit));
406                }
407                ProbeTarget::Arrival { short_circuit, .. } => {
408                    ids.insert(branch_of(short_circuit));
409                }
410                ProbeTarget::Try { id, handlers, .. } => {
411                    ids.insert(id.clone());
412                    for handler in handlers {
413                        ids.insert(handler.id.clone());
414                    }
415                }
416            }
417        }
418        ids.into_iter().collect()
419    }
420}
421
422/// `rb:branch:<hash>:alternative` -> `rb:branch:<hash>`; decision outcome
423/// branches are `rb:decision:<hash>:outcome`.
424fn branch_of(alternative: &str) -> String {
425    alternative
426        .rsplit_once(':')
427        .map(|(branch, _)| branch.to_owned())
428        .unwrap_or_else(|| alternative.to_owned())
429}
430
431#[derive(Debug, Clone, PartialEq)]
432pub struct RubyFileObligations {
433    pub manifest: CoverageManifest,
434    pub plan: RubyFilePlan,
435    pub probes: BTreeMap<u64, ProbeTarget>,
436}
437
438fn stable_id(file: &str, kind: &str, start: usize, end: usize, suffix: &str) -> String {
439    let mut hash = Sha256::new();
440    for value in [file, kind, &start.to_string(), &end.to_string(), suffix] {
441        hash.update(value.as_bytes());
442        hash.update([0]);
443    }
444    let digest = hash.finalize();
445    let mut encoded = String::with_capacity(24);
446    for byte in &digest[..12] {
447        use std::fmt::Write as _;
448        write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
449    }
450    format!("rb:{kind}:{encoded}")
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
454enum EditRank {
455    Clause,
456    StatementProbe,
457    Opener,
458    Closer,
459}
460
461#[derive(Debug, Clone)]
462struct PendingEdit {
463    offset: usize,
464    rank: EditRank,
465    /// Openers sort by ascending depth (outer first), closers by descending.
466    order: i64,
467    sequence: usize,
468    text: String,
469    scope: usize,
470}
471
472struct Collector<'a> {
473    file: &'a str,
474    source: &'a [u8],
475    line_starts: Vec<usize>,
476    manifest: CoverageManifest,
477    lines: BTreeMap<usize, String>,
478    statement_offsets: BTreeMap<String, [usize; 2]>,
479    branches: Vec<BranchKeyPlan>,
480    methods: Vec<MethodKeyPlan>,
481    cases: Vec<CasePlan>,
482    probes: BTreeMap<u64, ProbeTarget>,
483    edits: Vec<PendingEdit>,
484    next_probe: &'a mut u64,
485    point_ids: BTreeSet<String>,
486    decision_ids: BTreeSet<String>,
487    branch_ids: BTreeSet<String>,
488    claimed_lines: BTreeSet<usize>,
489    /// Statement start offsets proven by a stdlib branch key (index into
490    /// `branches`) instead of a line or a probe.
491    key_statements: BTreeMap<usize, usize>,
492    /// Start offsets of the body expressions of endless method definitions,
493    /// which take a wrapped probe because `def m = s(k); expr` would end the
494    /// definition at the probe.
495    endless_bodies: std::collections::BTreeSet<usize>,
496    /// Offsets of `&&`/`||` nodes that belong to a decision's tree.
497    tree_logicals: BTreeSet<usize>,
498    /// Statement lists that are expressions in disguise (parentheses, string
499    /// interpolation): their children are not statements in the denominator.
500    expression_lists: BTreeSet<usize>,
501    /// `if`/`unless` nodes that are `case/in` guards, owned by the clause.
502    guard_nodes: BTreeSet<usize>,
503    /// `elsif` nodes already handled through their parent's chain walk.
504    elsif_nodes: BTreeSet<usize>,
505    depth: i64,
506    begin_unmeasured: Vec<(String, usize)>,
507    error: Option<RubyInstrumenterError>,
508}
509
510impl<'a> Collector<'a> {
511    fn new(file: &'a str, source: &'a [u8], next_probe: &'a mut u64) -> Self {
512        let mut line_starts = vec![0];
513        line_starts.extend(
514            source
515                .iter()
516                .enumerate()
517                .filter_map(|(index, byte)| (*byte == b'\n').then_some(index + 1)),
518        );
519        Self {
520            file,
521            source,
522            line_starts,
523            manifest: CoverageManifest {
524                unmeasured: Vec::new(),
525                decisions: Vec::new(),
526                points: Vec::new(),
527                branches: Vec::new(),
528                limitations: Vec::new(),
529                scope: None,
530            },
531            lines: BTreeMap::new(),
532            statement_offsets: BTreeMap::new(),
533            branches: Vec::new(),
534            methods: Vec::new(),
535            cases: Vec::new(),
536            probes: BTreeMap::new(),
537            edits: Vec::new(),
538            next_probe,
539            point_ids: BTreeSet::new(),
540            decision_ids: BTreeSet::new(),
541            branch_ids: BTreeSet::new(),
542            claimed_lines: BTreeSet::new(),
543            key_statements: BTreeMap::new(),
544            endless_bodies: std::collections::BTreeSet::new(),
545            tree_logicals: BTreeSet::new(),
546            expression_lists: BTreeSet::new(),
547            guard_nodes: BTreeSet::new(),
548            elsif_nodes: BTreeSet::new(),
549            depth: 0,
550            begin_unmeasured: Vec::new(),
551            error: None,
552        }
553    }
554
555    // -- positions ----------------------------------------------------------
556
557    fn line_column(&self, offset: usize) -> (usize, usize) {
558        let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
559        (line_index + 1, offset - self.line_starts[line_index])
560    }
561
562    fn span(&self, start: usize, end: usize) -> PlanSpan {
563        let (start_line, start_column) = self.line_column(start);
564        let (end_line, end_column) = self.line_column(end);
565        PlanSpan {
566            start: [start_line, start_column],
567            end: [end_line, end_column],
568        }
569    }
570
571    fn point_span(&self, offset: usize) -> PlanSpan {
572        let (line, column) = self.line_column(offset);
573        PlanSpan {
574            start: [line, column],
575            end: [line, column],
576        }
577    }
578
579    fn location_span(&self, location: &Location<'_>) -> PlanSpan {
580        self.span(location.start_offset(), location.end_offset())
581    }
582
583    fn node_span(&self, node: &Node<'_>) -> PlanSpan {
584        self.location_span(&node.location())
585    }
586
587    fn text(&self, start: usize, end: usize) -> String {
588        String::from_utf8_lossy(&self.source[start.min(end)..end.min(self.source.len())])
589            .trim()
590            .to_owned()
591    }
592
593    fn statements_span(&self, statements: &Option<StatementsNode<'_>>) -> Option<PlanSpan> {
594        statements
595            .as_ref()
596            .map(|statements| self.location_span(&statements.location()))
597    }
598
599    // -- edits --------------------------------------------------------------
600
601    fn edit(&mut self, offset: usize, rank: EditRank, text: String, scope: usize) {
602        debug_assert!(!text.contains('\n'));
603        let order = match rank {
604            EditRank::Opener => self.depth,
605            EditRank::Closer => -self.depth,
606            _ => 0,
607        };
608        let sequence = self.edits.len();
609        self.edits.push(PendingEdit {
610            offset,
611            rank,
612            order,
613            sequence,
614            text,
615            scope,
616        });
617    }
618
619    fn probe_key(&mut self, target: ProbeTarget) -> u64 {
620        let key = *self.next_probe;
621        *self.next_probe += 1;
622        self.probes.insert(key, target);
623        key
624    }
625
626    fn wrap(&mut self, start: usize, end: usize, opener: String) {
627        self.edit(start, EditRank::Opener, opener, end);
628        self.edit(end, EditRank::Closer, "))".into(), start);
629    }
630
631    // -- manifest helpers ---------------------------------------------------
632
633    fn push_point(
634        &mut self,
635        id: &str,
636        start: usize,
637        end: usize,
638        kind: PointKind,
639        label: Option<String>,
640    ) {
641        let (line, column) = self.line_column(start);
642        self.manifest.points.push(PointMeta {
643            id: id.into(),
644            kind,
645            file: self.file.into(),
646            line,
647            column,
648            source: self.text(start, end),
649            label,
650        });
651    }
652
653    fn branch<const N: usize>(
654        &mut self,
655        start: usize,
656        end: usize,
657        kind: &str,
658        alternatives: [(&str, &str); N],
659    ) -> Option<String> {
660        let id = stable_id(self.file, "branch", start, end, kind);
661        let source = self.text(start, end);
662        self.branch_with_id(id, start, end, kind, source, alternatives)
663    }
664
665    fn branch_with_id<const N: usize>(
666        &mut self,
667        id: String,
668        start: usize,
669        _end: usize,
670        kind: &str,
671        source: String,
672        alternatives: [(&str, &str); N],
673    ) -> Option<String> {
674        if !self.branch_ids.insert(id.clone()) {
675            return None;
676        }
677        let (line, column) = self.line_column(start);
678        self.manifest.branches.push(BranchMeta {
679            id: id.clone(),
680            kind: kind.into(),
681            file: self.file.into(),
682            line,
683            column,
684            source,
685            alternatives: alternatives
686                .into_iter()
687                .map(|(suffix, label)| BranchAlternativeMeta {
688                    id: format!("{id}:{suffix}"),
689                    label: label.into(),
690                })
691                .collect(),
692        });
693        Some(id)
694    }
695
696    fn stdlib(
697        &mut self,
698        group: &str,
699        branch: &str,
700        span: PlanSpan,
701        kind: KeyKind,
702        hits: Vec<String>,
703    ) -> usize {
704        self.branches.push(BranchKeyPlan {
705            key: StdlibKey {
706                group: group.into(),
707                branch: branch.into(),
708                kind,
709                span,
710                unshifted: span,
711            },
712            hits,
713            decision: None,
714        });
715        self.branches.len() - 1
716    }
717
718    // -- statements ---------------------------------------------------------
719
720    fn statements(&mut self, statements: &StatementsNode<'_>) {
721        for statement in statements.body().iter() {
722            self.statement(&statement);
723        }
724    }
725
726    fn statement(&mut self, node: &Node<'_>) {
727        let location = node.location();
728        let (start, end) = (location.start_offset(), location.end_offset());
729        let id = stable_id(self.file, "statement", start, end, "");
730        if !self.point_ids.insert(id.clone()) {
731            return;
732        }
733        self.push_point(&id, start, end, PointKind::Statement, None);
734        let (line, _) = self.line_column(start);
735        if let Some(index) = self.key_statements.get(&start).copied() {
736            // The body of a modifier or one-line branch: the stdlib branch
737            // key that proves the branch proves this statement.
738            self.branches[index].hits.push(id);
739        } else if self.needs_probe(node) {
740            // No instruction carries this statement's first line: `x = begin`
741            // starts executing inside the begin body.
742            self.claimed_lines.insert(line);
743            let key = self.probe_key(ProbeTarget::Statement { id });
744            self.statement_probe(start, end, key);
745        } else if self.claimed_lines.insert(line) {
746            // Ruby's line table counts the statement's first line; the
747            // offsets let the runtime probe it instead where the interpreter
748            // turns out not to count that line.
749            self.lines.insert(line, id.clone());
750            self.statement_offsets.insert(id, [start, end]);
751        } else {
752            let key = self.probe_key(ProbeTarget::Statement { id });
753            self.statement_probe(start, end, key);
754        }
755    }
756
757    /// `s(k); statement`, or `(s(k); expression)` for the body of an endless
758    /// method definition, which admits exactly one expression.
759    fn statement_probe(&mut self, start: usize, end: usize, key: u64) {
760        if self.endless_bodies.contains(&start) {
761            self.depth += 1;
762            self.edit(
763                start,
764                EditRank::Opener,
765                format!("({RUBY_PROBE_RECEIVER}.s({key}); "),
766                end,
767            );
768            self.edit(end, EditRank::Closer, ")".into(), start);
769            self.depth -= 1;
770        } else {
771            self.edit(
772                start,
773                EditRank::StatementProbe,
774                format!("{RUBY_PROBE_RECEIVER}.s({key}); "),
775                end,
776            );
777        }
778    }
779
780    fn needs_probe(&self, node: &Node<'_>) -> bool {
781        let value = if let Some(write) = node.as_local_variable_write_node() {
782            Some(write.value())
783        } else if let Some(write) = node.as_instance_variable_write_node() {
784            Some(write.value())
785        } else if let Some(write) = node.as_class_variable_write_node() {
786            Some(write.value())
787        } else if let Some(write) = node.as_global_variable_write_node() {
788            Some(write.value())
789        } else if let Some(write) = node.as_constant_write_node() {
790            Some(write.value())
791        } else {
792            node.as_multi_write_node().map(|write| write.value())
793        };
794        // `x = begin ... end` and `x = (\n ... )` start executing inside the
795        // value on a later line; Ruby records nothing for the assignment line.
796        value.is_some_and(|value| {
797            value
798                .as_begin_node()
799                .is_some_and(|begin| begin.begin_keyword_loc().is_some())
800                || value.as_parentheses_node().is_some()
801        })
802    }
803
804    /// Register the first statement of a body as proven by a stdlib key.
805    fn key_body(&mut self, statements: Option<StatementsNode<'_>>, index: usize) {
806        if let Some(first) = statements.and_then(|statements| statements.body().iter().next()) {
807            self.key_statements
808                .insert(first.location().start_offset(), index);
809        }
810    }
811
812    // -- decisions ----------------------------------------------------------
813
814    /// Strip parentheses and `!`/`not`, counting the negations.
815    fn strip<'n>(&self, node: Node<'n>) -> (Node<'n>, usize) {
816        let mut current = node;
817        let mut not = 0;
818        loop {
819            if let Some(parens) = current.as_parentheses_node() {
820                if !parens.is_multiple_statements()
821                    && let Some(body) = parens.body()
822                    && let Some(statements) = body.as_statements_node()
823                    && statements.body().iter().count() == 1
824                    && let Some(inner) = statements.body().iter().next()
825                {
826                    current = inner;
827                    continue;
828                }
829                break;
830            }
831            if let Some(call) = current.as_call_node()
832                && call.name().as_slice() == b"!"
833                && call.arguments().is_none()
834                && call.block().is_none()
835                && let Some(receiver) = call.receiver()
836            {
837                not += 1;
838                current = receiver;
839                continue;
840            }
841            break;
842        }
843        (current, not)
844    }
845
846    fn tree(
847        &mut self,
848        node: Node<'_>,
849        leaves: &mut Vec<(usize, usize, usize)>,
850        logicals: &mut Vec<(String, Vec<Vec<usize>>)>,
851    ) -> ConditionTree {
852        let (operand, not) = self.strip(node);
853        let logical: Option<(&str, Node<'_>, Node<'_>, usize)> =
854            if let Some(and) = operand.as_and_node() {
855                Some((
856                    "and",
857                    and.left(),
858                    and.right(),
859                    operand.location().start_offset(),
860                ))
861            } else if let Some(or) = operand.as_or_node() {
862                Some((
863                    "or",
864                    or.left(),
865                    or.right(),
866                    operand.location().start_offset(),
867                ))
868            } else {
869                None
870            };
871        if let Some((op, left, right, offset)) = logical {
872            self.tree_logicals.insert(offset);
873            let first = leaves.len();
874            let left_tree = self.tree(left, leaves, logicals);
875            let middle = leaves.len();
876            let right_tree = self.tree(right, leaves, logicals);
877            logicals.push((
878                op.into(),
879                vec![(first..middle).collect(), (middle..leaves.len()).collect()],
880            ));
881            return ConditionTree::Node {
882                op: op.into(),
883                items: vec![left_tree, right_tree],
884                negate: not % 2 == 1,
885            };
886        }
887        let location = operand.location();
888        leaves.push((location.start_offset(), location.end_offset(), not));
889        ConditionTree::Leaf(leaves.len() - 1)
890    }
891
892    /// A decision proven by probes: multi-condition predicates, loop
893    /// predicates and pattern guards. Returns the probe key the outcome
894    /// wrapper must use.
895    fn probe_decision(
896        &mut self,
897        predicate: Node<'_>,
898        kind: &str,
899        loop_: Option<LoopTarget>,
900        wrapper: &str,
901    ) -> Option<u64> {
902        let location = predicate.location();
903        let (start, end) = (location.start_offset(), location.end_offset());
904        let id = stable_id(self.file, "decision", start, end, kind);
905        if !self.decision_ids.insert(id.clone()) {
906            return None;
907        }
908        let mut leaves = Vec::new();
909        let mut logicals = Vec::new();
910        let tree = self.tree(predicate, &mut leaves, &mut logicals);
911        let conditions = leaves
912            .iter()
913            .map(|(leaf_start, leaf_end, not)| {
914                let text = self.text(*leaf_start, *leaf_end);
915                if not % 2 == 1 {
916                    format!("!{text}")
917                } else {
918                    text
919                }
920            })
921            .collect::<Vec<_>>();
922        let (line, column) = self.line_column(start);
923        let source = self.text(start, end);
924        self.manifest.decisions.push(DecisionMeta {
925            id: id.clone(),
926            file: self.file.into(),
927            line,
928            column,
929            source: source.clone(),
930            conditions,
931            kind: kind.into(),
932        });
933        let outcome_id = format!("{id}:outcome");
934        self.branch_with_id(
935            outcome_id.clone(),
936            start,
937            end,
938            kind,
939            source,
940            [("true", "true"), ("false", "false")],
941        );
942        let mut derived = Vec::new();
943        for (op, groups) in logicals {
944            // The logical branch lives on the right operand's range.
945            let right_leaf = groups[1].first().copied().unwrap_or(0);
946            let (right_start, right_end, _) = leaves[right_leaf];
947            if let Some(branch_id) = self.branch(
948                right_start,
949                right_end,
950                &format!("logical-{op}"),
951                [
952                    ("short-circuit", "short-circuited"),
953                    ("evaluated", "right operand evaluated"),
954                ],
955            ) {
956                derived.push(DerivedLogical {
957                    previous_leaves: groups[0].clone(),
958                    operand_leaves: groups[1].clone(),
959                    short_circuit: format!("{branch_id}:short-circuit"),
960                    evaluated: format!("{branch_id}:evaluated"),
961                });
962            }
963        }
964        let key = self.probe_key(ProbeTarget::Decision {
965            id,
966            width: leaves.len(),
967            not: leaves.iter().map(|(_, _, not)| not % 2 == 1).collect(),
968            tree,
969            outcome_true: format!("{outcome_id}:true"),
970            outcome_false: format!("{outcome_id}:false"),
971            logical: derived,
972            loop_,
973        });
974        self.depth += 1;
975        self.wrap(
976            start,
977            end,
978            format!("{RUBY_PROBE_RECEIVER}.{wrapper}({key}, ("),
979        );
980        self.depth += 1;
981        for (index, (leaf_start, leaf_end, _)) in leaves.iter().enumerate() {
982            self.wrap(
983                *leaf_start,
984                *leaf_end,
985                format!("{RUBY_PROBE_RECEIVER}.c({key}, {index}, ("),
986            );
987        }
988        self.depth -= 2;
989        Some(key)
990    }
991
992    /// A single-condition `if`/`unless`/ternary decision: Ruby's `then`/`else`
993    /// counts already witness both outcomes, so no probe is inserted.
994    fn stdlib_decision(
995        &mut self,
996        predicate: Node<'_>,
997        kind: &str,
998        then_index: usize,
999        else_index: usize,
1000        then_is_true: bool,
1001    ) {
1002        let location = predicate.location();
1003        let (start, end) = (location.start_offset(), location.end_offset());
1004        let id = stable_id(self.file, "decision", start, end, kind);
1005        if !self.decision_ids.insert(id.clone()) {
1006            return;
1007        }
1008        let (operand, not) = self.strip(predicate);
1009        let operand_location = operand.location();
1010        let mut condition = self.text(
1011            operand_location.start_offset(),
1012            operand_location.end_offset(),
1013        );
1014        if not % 2 == 1 {
1015            condition = format!("!{condition}");
1016        }
1017        let (line, column) = self.line_column(start);
1018        let source = self.text(start, end);
1019        self.manifest.decisions.push(DecisionMeta {
1020            id: id.clone(),
1021            file: self.file.into(),
1022            line,
1023            column,
1024            source: source.clone(),
1025            conditions: vec![condition],
1026            kind: kind.into(),
1027        });
1028        let outcome_id = format!("{id}:outcome");
1029        self.branch_with_id(
1030            outcome_id.clone(),
1031            start,
1032            end,
1033            kind,
1034            source,
1035            [("true", "true"), ("false", "false")],
1036        );
1037        let (true_index, false_index) = if then_is_true {
1038            (then_index, else_index)
1039        } else {
1040            (else_index, then_index)
1041        };
1042        self.branches[true_index].decision = Some(StdlibDecision {
1043            id: id.clone(),
1044            value: true,
1045            outcome: format!("{outcome_id}:true"),
1046        });
1047        self.branches[false_index].decision = Some(StdlibDecision {
1048            id,
1049            value: false,
1050            outcome: format!("{outcome_id}:false"),
1051        });
1052    }
1053
1054    /// `Some(truthiness)` for a predicate Ruby folds at compile time: a
1055    /// `true`, `false` or `nil` literal, or a numeric, string or symbol
1056    /// literal, possibly parenthesised. Ruby emits neither a branch nor the
1057    /// dead arm for these.
1058    fn literal_truth(&self, predicate: Node<'_>) -> Option<bool> {
1059        let mut node = predicate;
1060        loop {
1061            let inner = node.as_parentheses_node().and_then(|parens| {
1062                let body = parens.body()?;
1063                let statements = body.as_statements_node()?;
1064                let mut iter = statements.body().iter();
1065                let only = iter.next()?;
1066                iter.next().is_none().then_some(only)
1067            });
1068            match inner {
1069                Some(inner) => node = inner,
1070                None => break,
1071            }
1072        }
1073        if node.as_true_node().is_some()
1074            || node.as_integer_node().is_some()
1075            || node.as_float_node().is_some()
1076            || node.as_rational_node().is_some()
1077            || node.as_imaginary_node().is_some()
1078            || node.as_string_node().is_some()
1079            || node.as_symbol_node().is_some()
1080        {
1081            Some(true)
1082        } else if node.as_false_node().is_some() || node.as_nil_node().is_some() {
1083            Some(false)
1084        } else {
1085            None
1086        }
1087    }
1088
1089    fn is_compound(&self, predicate: Node<'_>) -> bool {
1090        let (operand, _) = self.strip(predicate);
1091        operand.as_and_node().is_some() || operand.as_or_node().is_some()
1092    }
1093
1094    /// `predicate` is called twice because Prism nodes are handles that
1095    /// cannot be copied; every call returns the same node.
1096    fn predicate_decision<'n>(
1097        &mut self,
1098        predicate: impl Fn() -> Node<'n>,
1099        kind: &str,
1100        then_index: usize,
1101        else_index: usize,
1102        then_is_true: bool,
1103    ) {
1104        if self.is_compound(predicate()) {
1105            self.probe_decision(predicate(), kind, None, "d");
1106        } else {
1107            self.stdlib_decision(predicate(), kind, then_index, else_index, then_is_true);
1108        }
1109    }
1110
1111    // -- constructs ---------------------------------------------------------
1112
1113    fn if_node(&mut self, node: &IfNode<'_>, kind: &str) {
1114        let location = node.location();
1115        let node_span = self.location_span(&location);
1116        let then_statements = self.statements_span(&node.statements());
1117        // An `if` without a body gets a zero-width key at its predicate's end.
1118        let (then_span, then_kind) = match then_statements {
1119            Some(span) => (span, KeyKind::List),
1120            None => (
1121                self.point_span(node.predicate().location().end_offset()),
1122                KeyKind::Point,
1123            ),
1124        };
1125        let (else_span, else_kind) = match node.subsequent() {
1126            Some(subsequent) => match subsequent.as_else_node() {
1127                Some(else_node) => match self.statements_span(&else_node.statements()) {
1128                    Some(span) => (span, KeyKind::List),
1129                    None => (self.node_span(&subsequent), KeyKind::Node),
1130                },
1131                None => (self.node_span(&subsequent), KeyKind::Node),
1132            },
1133            None => (node_span, KeyKind::Node),
1134        };
1135        let then_index = self.stdlib("if", "then", then_span, then_kind, Vec::new());
1136        let else_index = self.stdlib("if", "else", else_span, else_kind, Vec::new());
1137        if kind == "ternary" {
1138            // A ternary's arms are expressions, not statements.
1139            if let Some(statements) = node.statements() {
1140                self.expression_lists
1141                    .insert(statements.location().start_offset());
1142            }
1143            if let Some(subsequent) = node.subsequent()
1144                && let Some(else_node) = subsequent.as_else_node()
1145                && let Some(statements) = else_node.statements()
1146            {
1147                self.expression_lists
1148                    .insert(statements.location().start_offset());
1149            }
1150        } else {
1151            self.key_body(node.statements(), then_index);
1152            if let Some(subsequent) = node.subsequent()
1153                && let Some(else_node) = subsequent.as_else_node()
1154            {
1155                self.key_body(else_node.statements(), else_index);
1156            }
1157        }
1158        self.predicate_decision(|| node.predicate(), kind, then_index, else_index, true);
1159    }
1160
1161    fn unless_node(&mut self, node: &UnlessNode<'_>) {
1162        let node_span = self.location_span(&node.location());
1163        let then_statements = self.statements_span(&node.statements());
1164        let (then_span, then_kind) = match then_statements {
1165            Some(span) => (span, KeyKind::List),
1166            None => (
1167                self.point_span(node.predicate().location().end_offset()),
1168                KeyKind::Point,
1169            ),
1170        };
1171        let (else_span, else_kind) = match node.else_clause() {
1172            Some(else_node) => match self.statements_span(&else_node.statements()) {
1173                Some(span) => (span, KeyKind::List),
1174                None => (self.location_span(&else_node.location()), KeyKind::Node),
1175            },
1176            None => (node_span, KeyKind::Node),
1177        };
1178        let then_index = self.stdlib("unless", "then", then_span, then_kind, Vec::new());
1179        let else_index = self.stdlib("unless", "else", else_span, else_kind, Vec::new());
1180        self.key_body(node.statements(), then_index);
1181        if let Some(else_node) = node.else_clause() {
1182            self.key_body(else_node.statements(), else_index);
1183        }
1184        // `unless` runs `then` when the predicate is falsy.
1185        self.predicate_decision(|| node.predicate(), "unless", then_index, else_index, false);
1186    }
1187
1188    fn loop_node(
1189        &mut self,
1190        location: &Location<'_>,
1191        predicate: Node<'_>,
1192        statements: Option<StatementsNode<'_>>,
1193        begin_modifier: bool,
1194        until: bool,
1195    ) {
1196        let kind = if until { "until" } else { "while" };
1197        let (start, end) = (location.start_offset(), location.end_offset());
1198        // The stdlib `body` key proves a same-offset modifier body statement.
1199        if let Some(body_span) = self.statements_span(&statements) {
1200            let index = self.stdlib(kind, "body", body_span, KeyKind::List, Vec::new());
1201            self.key_body(statements, index);
1202        }
1203        let loop_target = if begin_modifier {
1204            // `begin ... end while` always enters the body once.
1205            None
1206        } else {
1207            self.branch(
1208                start,
1209                end,
1210                kind,
1211                [("zero", "zero iterations"), ("entered", "entered")],
1212            )
1213            .map(|id| LoopTarget {
1214                zero: format!("{id}:zero"),
1215                entered: format!("{id}:entered"),
1216                id,
1217                until,
1218            })
1219        };
1220        self.probe_decision(predicate, kind, loop_target, "w");
1221    }
1222
1223    fn for_node(&mut self, node: &ForNode<'_>) {
1224        let location = node.location();
1225        let Some(id) = self.branch(
1226            location.start_offset(),
1227            location.end_offset(),
1228            "for",
1229            [("zero", "zero iterations"), ("entered", "entered")],
1230        ) else {
1231            return;
1232        };
1233        let key = self.probe_key(ProbeTarget::For {
1234            zero: format!("{id}:zero"),
1235            entered: format!("{id}:entered"),
1236            id: id.clone(),
1237        });
1238        let collection = node.collection().location();
1239        self.depth += 1;
1240        self.wrap(
1241            collection.start_offset(),
1242            collection.end_offset(),
1243            format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1244        );
1245        self.depth -= 1;
1246        match node
1247            .statements()
1248            .and_then(|statements| statements.body().iter().next())
1249        {
1250            Some(first) => self.edit(
1251                first.location().start_offset(),
1252                EditRank::StatementProbe,
1253                format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1254                first.location().end_offset(),
1255            ),
1256            None => {
1257                self.manifest.unmeasured.push(format!("{id}:entered"));
1258                self.manifest.unmeasured.push(format!("{id}:zero"));
1259            }
1260        }
1261    }
1262
1263    /// `items.each { ... }` and friends: Ruby's loops are usually method
1264    /// calls with a block. The receiver is wrapped like a `for` collection
1265    /// and the block body gets the entry probe, so zero-versus-entered is
1266    /// exact for every iterator in [`ITERATORS`].
1267    fn iterator_loop(
1268        &mut self,
1269        node: &CallNode<'_>,
1270        receiver: &Node<'_>,
1271        block: &ruby_prism::BlockNode<'_>,
1272    ) {
1273        let location = node.location();
1274        let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
1275        let Some(id) = self.branch(
1276            location.start_offset(),
1277            location.end_offset(),
1278            &format!("iterator-{}", name.trim_end_matches(['?', '!'])),
1279            [("zero", "zero iterations"), ("entered", "entered")],
1280        ) else {
1281            return;
1282        };
1283        let first = block.body().and_then(|body| {
1284            if let Some(statements) = body.as_statements_node() {
1285                statements.body().iter().next()
1286            } else if let Some(begin) = body.as_begin_node() {
1287                begin
1288                    .statements()
1289                    .and_then(|statements| statements.body().iter().next())
1290            } else {
1291                None
1292            }
1293        });
1294        let Some(first) = first else {
1295            // An empty block never enters; both alternatives stay declared
1296            // but nothing can witness them.
1297            self.manifest.unmeasured.push(format!("{id}:entered"));
1298            self.manifest.unmeasured.push(format!("{id}:zero"));
1299            return;
1300        };
1301        let key = self.probe_key(ProbeTarget::For {
1302            zero: format!("{id}:zero"),
1303            entered: format!("{id}:entered"),
1304            id,
1305        });
1306        let receiver_location = receiver.location();
1307        self.depth += 1;
1308        self.wrap(
1309            receiver_location.start_offset(),
1310            receiver_location.end_offset(),
1311            format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1312        );
1313        self.depth -= 1;
1314        self.edit(
1315            first.location().start_offset(),
1316            EditRank::StatementProbe,
1317            format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1318            first.location().end_offset(),
1319        );
1320    }
1321
1322    fn case_node(&mut self, node: &CaseNode<'_>) {
1323        let node_span = self.location_span(&node.location());
1324        let (start, end) = (node.location().start_offset(), node.location().end_offset());
1325        let mut clauses = Vec::new();
1326        for (index, condition) in node.conditions().iter().enumerate() {
1327            let Some(when) = condition.as_when_node() else {
1328                continue;
1329            };
1330            let Some(id) = self.branch(
1331                when.location().start_offset(),
1332                when.location().end_offset(),
1333                &format!("case-when-{index}"),
1334                [("missed", "not selected"), ("selected", "selected")],
1335            ) else {
1336                return;
1337            };
1338            let statements = self.statements_span(&when.statements());
1339            let span = statements.unwrap_or_else(|| self.location_span(&when.location()));
1340            let key_index = self.stdlib(
1341                "case",
1342                "when",
1343                span,
1344                if statements.is_some() {
1345                    KeyKind::List
1346                } else {
1347                    KeyKind::Node
1348                },
1349                vec![format!("{id}:selected")],
1350            );
1351            self.key_body(when.statements(), key_index);
1352            clauses.push(CaseClausePlan {
1353                key: self.branches[key_index].key.clone(),
1354                missed: format!("{id}:missed"),
1355                selected: format!("{id}:selected"),
1356            });
1357        }
1358        let no_match = match node.else_clause() {
1359            Some(else_node) => {
1360                let Some(id) = self.branch(
1361                    else_node.location().start_offset(),
1362                    else_node.location().end_offset(),
1363                    "case-else",
1364                    [("missed", "not selected"), ("selected", "selected")],
1365                ) else {
1366                    return;
1367                };
1368                let statements = self.statements_span(&else_node.statements());
1369                let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1370                let key_index = self.stdlib(
1371                    "case",
1372                    "else",
1373                    span,
1374                    if statements.is_some() {
1375                        KeyKind::List
1376                    } else {
1377                        KeyKind::Node
1378                    },
1379                    vec![format!("{id}:selected")],
1380                );
1381                self.key_body(else_node.statements(), key_index);
1382                clauses.push(CaseClausePlan {
1383                    key: self.branches[key_index].key.clone(),
1384                    missed: format!("{id}:missed"),
1385                    selected: format!("{id}:selected"),
1386                });
1387                None
1388            }
1389            None => self
1390                .branch(
1391                    start,
1392                    end,
1393                    "case-no-match",
1394                    [
1395                        ("matched", "some clause matched"),
1396                        ("unmatched", "no clause matched"),
1397                    ],
1398                )
1399                .map(|id| {
1400                    let key_index = self.stdlib(
1401                        "case",
1402                        "else",
1403                        node_span,
1404                        KeyKind::Node,
1405                        vec![format!("{id}:unmatched")],
1406                    );
1407                    CaseNoMatchPlan {
1408                        key: self.branches[key_index].key.clone(),
1409                        matched: format!("{id}:matched"),
1410                        unmatched: format!("{id}:unmatched"),
1411                    }
1412                }),
1413        };
1414        self.cases.push(CasePlan { clauses, no_match });
1415    }
1416
1417    fn case_match_node(&mut self, node: &CaseMatchNode<'_>) {
1418        let node_span = self.location_span(&node.location());
1419        let (start, end) = (node.location().start_offset(), node.location().end_offset());
1420        let mut clauses = Vec::new();
1421        for (index, condition) in node.conditions().iter().enumerate() {
1422            let Some(in_node) = condition.as_in_node() else {
1423                continue;
1424            };
1425            let Some(id) = self.branch(
1426                in_node.location().start_offset(),
1427                in_node.location().end_offset(),
1428                &format!("case-in-{index}"),
1429                [("missed", "not selected"), ("selected", "selected")],
1430            ) else {
1431                return;
1432            };
1433            let statements = self.statements_span(&in_node.statements());
1434            let span = statements.unwrap_or_else(|| self.location_span(&in_node.location()));
1435            let key_index = self.stdlib(
1436                "case",
1437                "in",
1438                span,
1439                if statements.is_some() {
1440                    KeyKind::List
1441                } else {
1442                    KeyKind::Node
1443                },
1444                vec![format!("{id}:selected")],
1445            );
1446            self.key_body(in_node.statements(), key_index);
1447            clauses.push(CaseClausePlan {
1448                key: self.branches[key_index].key.clone(),
1449                missed: format!("{id}:missed"),
1450                selected: format!("{id}:selected"),
1451            });
1452            // A guard is a decision of its own; Ruby reports no branch key
1453            // for it, so it is always probe-driven.
1454            let pattern = in_node.pattern();
1455            if let Some(guard) = pattern.as_if_node() {
1456                self.guard_nodes.insert(pattern.location().start_offset());
1457                self.probe_decision(guard.predicate(), "in-guard", None, "d");
1458            } else if let Some(guard) = pattern.as_unless_node() {
1459                self.guard_nodes.insert(pattern.location().start_offset());
1460                self.probe_decision(guard.predicate(), "in-guard-unless", None, "d");
1461            }
1462        }
1463        let no_match = match node.else_clause() {
1464            Some(else_node) => {
1465                let Some(id) = self.branch(
1466                    else_node.location().start_offset(),
1467                    else_node.location().end_offset(),
1468                    "case-else",
1469                    [("missed", "not selected"), ("selected", "selected")],
1470                ) else {
1471                    return;
1472                };
1473                let statements = self.statements_span(&else_node.statements());
1474                let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1475                let key_index = self.stdlib(
1476                    "case",
1477                    "else",
1478                    span,
1479                    if statements.is_some() {
1480                        KeyKind::List
1481                    } else {
1482                        KeyKind::Node
1483                    },
1484                    vec![format!("{id}:selected")],
1485                );
1486                self.key_body(else_node.statements(), key_index);
1487                clauses.push(CaseClausePlan {
1488                    key: self.branches[key_index].key.clone(),
1489                    missed: format!("{id}:missed"),
1490                    selected: format!("{id}:selected"),
1491                });
1492                None
1493            }
1494            None => self
1495                .branch(
1496                    start,
1497                    end,
1498                    "case-no-match",
1499                    [
1500                        ("matched", "some pattern matched"),
1501                        ("unmatched", "no pattern matched"),
1502                    ],
1503                )
1504                .map(|id| {
1505                    let key_index = self.stdlib(
1506                        "case",
1507                        "else",
1508                        node_span,
1509                        KeyKind::Node,
1510                        vec![format!("{id}:unmatched")],
1511                    );
1512                    CaseNoMatchPlan {
1513                        key: self.branches[key_index].key.clone(),
1514                        matched: format!("{id}:matched"),
1515                        unmatched: format!("{id}:unmatched"),
1516                    }
1517                }),
1518        };
1519        self.cases.push(CasePlan { clauses, no_match });
1520    }
1521
1522    fn safe_navigation(&mut self, node: &CallNode<'_>) {
1523        let location = node.location();
1524        let Some(id) = self.branch(
1525            location.start_offset(),
1526            location.end_offset(),
1527            "safe-navigation",
1528            [("nil", "receiver nil"), ("called", "method called")],
1529        ) else {
1530            return;
1531        };
1532        // Ruby's key runs from the receiver to the closing parenthesis or the
1533        // last argument, and to the message when there are no arguments; a
1534        // block or block argument is never part of it.
1535        let end = match node.arguments() {
1536            Some(arguments) => node
1537                .closing_loc()
1538                .map(|closing| closing.end_offset())
1539                .unwrap_or_else(|| arguments.location().end_offset()),
1540            None => node
1541                .message_loc()
1542                .map(|message| message.end_offset())
1543                .unwrap_or_else(|| location.end_offset()),
1544        };
1545        let (start_line, start_column) = self.line_column(location.start_offset());
1546        let (end_line, end_column) = self.line_column(end);
1547        let span = PlanSpan {
1548            start: [start_line, start_column],
1549            end: [end_line, end_column],
1550        };
1551        self.stdlib(
1552            "&.",
1553            "then",
1554            span,
1555            KeyKind::Node,
1556            vec![format!("{id}:called")],
1557        );
1558        self.stdlib("&.", "else", span, KeyKind::Node, vec![format!("{id}:nil")]);
1559    }
1560
1561    fn value_logical(&mut self, op: &str, left: &Node<'_>, node_start: usize, node_end: usize) {
1562        let Some(id) = self.branch(
1563            node_start,
1564            node_end,
1565            &format!("logical-{op}"),
1566            [
1567                ("short-circuit", "short-circuited"),
1568                ("evaluated", "right operand evaluated"),
1569            ],
1570        ) else {
1571            return;
1572        };
1573        let key = self.probe_key(ProbeTarget::Logical {
1574            op: op.into(),
1575            short_circuit: format!("{id}:short-circuit"),
1576            evaluated: format!("{id}:evaluated"),
1577        });
1578        let left = left.location();
1579        self.depth += 1;
1580        self.wrap(
1581            left.start_offset(),
1582            left.end_offset(),
1583            format!("{RUBY_PROBE_RECEIVER}.l({key}, ("),
1584        );
1585        self.depth -= 1;
1586    }
1587
1588    /// `x ||= v` / `x &&= v`. A variable target can be re-read without side
1589    /// effects, so the whole expression becomes `(l(k, x); x ||= v)`; other
1590    /// targets only get the evaluated side.
1591    fn op_assign(
1592        &mut self,
1593        op: &str,
1594        node_start: usize,
1595        node_end: usize,
1596        name: Option<&[u8]>,
1597        value: &Node<'_>,
1598    ) {
1599        let Some(id) = self.branch(
1600            node_start,
1601            node_end,
1602            &format!("{op}-assign"),
1603            [
1604                ("short-circuit", "assignment skipped"),
1605                ("evaluated", "value evaluated and assigned"),
1606            ],
1607        ) else {
1608            return;
1609        };
1610        match name {
1611            Some(name) => {
1612                let key = self.probe_key(ProbeTarget::Logical {
1613                    op: op.into(),
1614                    short_circuit: format!("{id}:short-circuit"),
1615                    evaluated: format!("{id}:evaluated"),
1616                });
1617                let name = String::from_utf8_lossy(name);
1618                self.depth += 1;
1619                self.edit(
1620                    node_start,
1621                    EditRank::Opener,
1622                    format!("({RUBY_PROBE_RECEIVER}.l({key}, {name}); "),
1623                    node_end,
1624                );
1625                self.edit(node_end, EditRank::Closer, ")".into(), node_start);
1626                self.depth -= 1;
1627            }
1628            None => {
1629                // `(pre(k); recv[i] ||= (es(k); v))`: the target is evaluated
1630                // exactly once, as before; arrivals and right-side starts are
1631                // counted per phase and their difference is the skipped side.
1632                let key = self.probe_key(ProbeTarget::Arrival {
1633                    short_circuit: format!("{id}:short-circuit"),
1634                    evaluated: format!("{id}:evaluated"),
1635                });
1636                let value_location = value.location();
1637                self.depth += 1;
1638                self.edit(
1639                    node_start,
1640                    EditRank::Opener,
1641                    format!("({RUBY_PROBE_RECEIVER}.pre({key}); "),
1642                    node_end,
1643                );
1644                self.edit(node_end, EditRank::Closer, ")".into(), node_start);
1645                self.depth += 1;
1646                self.edit(
1647                    value_location.start_offset(),
1648                    EditRank::Opener,
1649                    format!("({RUBY_PROBE_RECEIVER}.es({key}); "),
1650                    value_location.end_offset(),
1651                );
1652                self.edit(
1653                    value_location.end_offset(),
1654                    EditRank::Closer,
1655                    ")".into(),
1656                    value_location.start_offset(),
1657                );
1658                self.depth -= 2;
1659            }
1660        }
1661    }
1662
1663    // -- exception flow -----------------------------------------------------
1664
1665    /// `begin`/`rescue`/`else`/`ensure` in any host: explicit `begin`, a
1666    /// method body, or a `do` block. `close` is where the closing keyword
1667    /// lives, which is where the propagation clause is inserted when the
1668    /// construct has no `else` or `ensure`.
1669    fn begin_node(&mut self, node: &BeginNode<'_>, close: Option<usize>) {
1670        let has_rescue = node.rescue_clause().is_some();
1671        let has_ensure = node.ensure_clause().is_some();
1672        if !has_rescue && !has_ensure {
1673            return;
1674        }
1675        let location = node.location();
1676        let (start, end) = (location.start_offset(), location.end_offset());
1677        let Some(id) = self.branch(
1678            start,
1679            end,
1680            "begin",
1681            [
1682                ("success", "body completed"),
1683                ("raised", "exception raised"),
1684            ],
1685        ) else {
1686            return;
1687        };
1688        let mut handlers = Vec::new();
1689        let mut handler_edits = Vec::new();
1690        let mut rescue = node.rescue_clause();
1691        let mut index = 0;
1692        while let Some(clause) = rescue {
1693            let clause_location = clause.location();
1694            let Some(handler_id) = self.branch(
1695                clause_location.start_offset(),
1696                clause_location.end_offset(),
1697                &format!("rescue-{index}"),
1698                [("missed", "not selected"), ("selected", "selected")],
1699            ) else {
1700                return;
1701            };
1702            handlers.push(HandlerTarget {
1703                missed: format!("{handler_id}:missed"),
1704                selected: format!("{handler_id}:selected"),
1705                id: handler_id,
1706            });
1707            handler_edits.push(self.handler_probe_position(&clause));
1708            rescue = clause.subsequent();
1709            index += 1;
1710        }
1711        let key = self.probe_key(ProbeTarget::Try {
1712            success: format!("{id}:success"),
1713            raised: format!("{id}:raised"),
1714            id: id.clone(),
1715            handlers,
1716        });
1717        for (index, (offset, leading, scope_end)) in handler_edits.into_iter().enumerate() {
1718            let text = if leading {
1719                format!("; {RUBY_PROBE_RECEIVER}.h({key}, {index})")
1720            } else {
1721                format!("{RUBY_PROBE_RECEIVER}.h({key}, {index}); ")
1722            };
1723            self.edit(offset, EditRank::StatementProbe, text, scope_end);
1724        }
1725        // Propagation clause: after every user clause, before else/ensure/end.
1726        let clause_offset = node
1727            .else_clause()
1728            .map(|clause| clause.else_keyword_loc().start_offset())
1729            .or_else(|| {
1730                node.ensure_clause()
1731                    .map(|clause| clause.ensure_keyword_loc().start_offset())
1732            })
1733            .or_else(|| node.end_keyword_loc().map(|loc| loc.start_offset()))
1734            .or(close);
1735        match clause_offset {
1736            Some(offset) => self.edit(
1737                offset,
1738                EditRank::Clause,
1739                format!(
1740                    "rescue Exception => __supercov_e; {RUBY_PROBE_RECEIVER}.p({key}); raise; "
1741                ),
1742                offset,
1743            ),
1744            None => {
1745                self.manifest.unmeasured.push(format!("{id}:raised"));
1746                let (line, _) = self.line_column(start);
1747                self.begin_unmeasured.push((id.clone(), line));
1748            }
1749        }
1750        // Completion: the else clause runs only after a completed body.
1751        if let Some(else_clause) = node.else_clause() {
1752            match else_clause
1753                .statements()
1754                .and_then(|statements| statements.body().iter().next())
1755            {
1756                Some(first) => self.edit(
1757                    first.location().start_offset(),
1758                    EditRank::StatementProbe,
1759                    format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
1760                    first.location().end_offset(),
1761                ),
1762                None => self.edit(
1763                    else_clause.else_keyword_loc().end_offset(),
1764                    EditRank::StatementProbe,
1765                    format!(" {RUBY_PROBE_RECEIVER}.ok0({key});"),
1766                    else_clause.else_keyword_loc().end_offset(),
1767                ),
1768            }
1769            return;
1770        }
1771        let last = node
1772            .statements()
1773            .and_then(|statements| statements.body().iter().last());
1774        match last {
1775            Some(last) => {
1776                if !self.completion_probe(last, key) {
1777                    self.manifest.unmeasured.push(format!("{id}:success"));
1778                    let (line, _) = self.line_column(start);
1779                    self.begin_unmeasured.push((id, line));
1780                }
1781            }
1782            None => {
1783                self.manifest.unmeasured.push(format!("{id}:success"));
1784                let (line, _) = self.line_column(start);
1785                self.begin_unmeasured.push((id, line));
1786            }
1787        }
1788    }
1789
1790    /// Where the handler-entry probe goes: before the first body statement,
1791    /// or right after the clause header when the body is empty.
1792    fn handler_probe_position(&self, clause: &RescueNode<'_>) -> (usize, bool, usize) {
1793        if let Some(first) = clause
1794            .statements()
1795            .and_then(|statements| statements.body().iter().next())
1796        {
1797            return (
1798                first.location().start_offset(),
1799                false,
1800                first.location().end_offset(),
1801            );
1802        }
1803        let offset = if let Some(then_keyword) = clause.then_keyword_loc() {
1804            then_keyword.end_offset()
1805        } else if let Some(reference) = clause.reference() {
1806            reference.location().end_offset()
1807        } else if let Some(last) = clause.exceptions().iter().last() {
1808            last.location().end_offset()
1809        } else {
1810            clause.keyword_loc().end_offset()
1811        };
1812        (offset, true, offset)
1813    }
1814
1815    /// True for an expression whose own value can be a jump: a `return`,
1816    /// `break`, `next`, `redo` or `retry`, or an `if`, `unless`, ternary,
1817    /// `case` or nested `begin` with an arm that ends in one. Such an
1818    /// expression may not be wrapped. Ruby rejects the parenthesised form
1819    /// outright when every arm is a jump ("void value expression"), and when
1820    /// only some arms are, passing it as an argument -- which is what a
1821    /// wrapper does -- makes the compiler miscount its stack ("argument stack
1822    /// underflow") for shapes that are hard to predict. Its arms are probed
1823    /// instead. A jump reached through a block, a loop or `&&`/`||` belongs to
1824    /// that construct rather than to this expression's value, and is fine.
1825    fn jump_exposed(&self, node: &Node<'_>) -> bool {
1826        if Self::is_jump(node) {
1827            return true;
1828        }
1829        if let Some(if_node) = node.as_if_node() {
1830            let else_exposed = match if_node.subsequent() {
1831                Some(subsequent) => match subsequent.as_else_node() {
1832                    Some(else_node) => self.arm_jump_exposed(else_node.statements()),
1833                    None => self.jump_exposed(&subsequent),
1834                },
1835                None => false,
1836            };
1837            return else_exposed || self.arm_jump_exposed(if_node.statements());
1838        }
1839        if let Some(unless_node) = node.as_unless_node() {
1840            let else_exposed = match unless_node.else_clause() {
1841                Some(else_node) => self.arm_jump_exposed(else_node.statements()),
1842                None => false,
1843            };
1844            return else_exposed || self.arm_jump_exposed(unless_node.statements());
1845        }
1846        if let Some(begin) = node.as_begin_node() {
1847            if self.arm_jump_exposed(begin.statements())
1848                || begin
1849                    .else_clause()
1850                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()))
1851            {
1852                return true;
1853            }
1854            let mut rescue = begin.rescue_clause();
1855            while let Some(clause) = rescue {
1856                if self.arm_jump_exposed(clause.statements()) {
1857                    return true;
1858                }
1859                rescue = clause.subsequent();
1860            }
1861            return false;
1862        }
1863        if let Some(case_node) = node.as_case_node() {
1864            return case_node
1865                .conditions()
1866                .iter()
1867                .any(|condition| match condition.as_when_node() {
1868                    Some(when_node) => self.arm_jump_exposed(when_node.statements()),
1869                    None => false,
1870                })
1871                || case_node
1872                    .else_clause()
1873                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
1874        }
1875        if let Some(case_node) = node.as_case_match_node() {
1876            return case_node
1877                .conditions()
1878                .iter()
1879                .any(|condition| match condition.as_in_node() {
1880                    Some(in_node) => self.arm_jump_exposed(in_node.statements()),
1881                    None => false,
1882                })
1883                || case_node
1884                    .else_clause()
1885                    .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
1886        }
1887        if let Some(parentheses) = node.as_parentheses_node() {
1888            return match parentheses.body() {
1889                Some(body) => match body.as_statements_node() {
1890                    Some(statements) => self.arm_jump_exposed(Some(statements)),
1891                    None => self.jump_exposed(&body),
1892                },
1893                None => false,
1894            };
1895        }
1896        false
1897    }
1898
1899    fn arm_jump_exposed(&self, statements: Option<StatementsNode<'_>>) -> bool {
1900        match statements.and_then(|statements| statements.body().iter().last()) {
1901            Some(last) => self.jump_exposed(&last),
1902            None => false,
1903        }
1904    }
1905
1906    /// The expressions to wrap so the construct's normal completion is
1907    /// observed, or `None` when it cannot be observed at all. A statement that
1908    /// may not be wrapped is replaced by its arms, of which exactly one runs;
1909    /// an arm that is missing (an `if` with no `else`, whose fall-through
1910    /// carries no expression) makes the whole construct unobservable.
1911    fn probe_targets<'n>(&self, node: Node<'n>) -> Option<Vec<Node<'n>>> {
1912        if Self::is_jump(&node) {
1913            return Some(vec![node]);
1914        }
1915        if node.as_multi_write_node().is_some()
1916            || node.as_alias_method_node().is_some()
1917            || node.as_alias_global_variable_node().is_some()
1918            || node.as_undef_node().is_some()
1919        {
1920            return None;
1921        }
1922        if !self.jump_exposed(&node) {
1923            return Some(vec![node]);
1924        }
1925        if let Some(if_node) = node.as_if_node() {
1926            let mut targets = self.arm_targets(if_node.statements())?;
1927            match if_node.subsequent() {
1928                Some(subsequent) => match subsequent.as_else_node() {
1929                    Some(else_node) => targets.extend(self.arm_targets(else_node.statements())?),
1930                    None => targets.extend(self.probe_targets(subsequent)?),
1931                },
1932                None => return None,
1933            }
1934            return Some(targets);
1935        }
1936        if let Some(unless_node) = node.as_unless_node() {
1937            let mut targets = self.arm_targets(unless_node.statements())?;
1938            let else_node = unless_node.else_clause()?;
1939            targets.extend(self.arm_targets(else_node.statements())?);
1940            return Some(targets);
1941        }
1942        if let Some(case_node) = node.as_case_node() {
1943            let mut targets = Vec::new();
1944            for condition in case_node.conditions().iter() {
1945                let when_node = condition.as_when_node()?;
1946                targets.extend(self.arm_targets(when_node.statements())?);
1947            }
1948            targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
1949            return Some(targets);
1950        }
1951        if let Some(case_node) = node.as_case_match_node() {
1952            let mut targets = Vec::new();
1953            for condition in case_node.conditions().iter() {
1954                let in_node = condition.as_in_node()?;
1955                targets.extend(self.arm_targets(in_node.statements())?);
1956            }
1957            targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
1958            return Some(targets);
1959        }
1960        if let Some(begin) = node.as_begin_node() {
1961            let mut targets = match begin.else_clause() {
1962                Some(else_node) => self.arm_targets(else_node.statements())?,
1963                None => self.arm_targets(begin.statements())?,
1964            };
1965            let mut rescue = begin.rescue_clause();
1966            while let Some(clause) = rescue {
1967                targets.extend(self.arm_targets(clause.statements())?);
1968                rescue = clause.subsequent();
1969            }
1970            return Some(targets);
1971        }
1972        if let Some(parentheses) = node.as_parentheses_node() {
1973            let body = parentheses.body()?;
1974            return match body.as_statements_node() {
1975                Some(statements) => self.arm_targets(Some(statements)),
1976                None => self.probe_targets(body),
1977            };
1978        }
1979        None
1980    }
1981
1982    fn arm_targets<'n>(&self, statements: Option<StatementsNode<'n>>) -> Option<Vec<Node<'n>>> {
1983        let last = statements.and_then(|statements| statements.body().iter().last())?;
1984        self.probe_targets(last)
1985    }
1986
1987    /// Wrap the body's final statement so its normal completion is observed
1988    /// without changing the value of the construct. Returns false when the
1989    /// statement has no expression form to wrap (see
1990    /// [`Collector::jump_exposed`]).
1991    fn completion_probe(&mut self, last: Node<'_>, key: u64) -> bool {
1992        let Some(targets) = self.probe_targets(last) else {
1993            return false;
1994        };
1995        for target in &targets {
1996            self.wrap_completion(target, key);
1997        }
1998        true
1999    }
2000
2001    /// One expression whose completion proves the construct completed.
2002    fn wrap_completion(&mut self, last: &Node<'_>, key: u64) {
2003        let location = last.location();
2004        let (start, end) = (location.start_offset(), location.end_offset());
2005        let arguments = if let Some(node) = last.as_return_node() {
2006            Some((node.keyword_loc(), node.arguments()))
2007        } else if let Some(node) = last.as_break_node() {
2008            Some((node.keyword_loc(), node.arguments()))
2009        } else {
2010            last.as_next_node()
2011                .map(|node| (node.keyword_loc(), node.arguments()))
2012        };
2013        if let Some((keyword, arguments)) = arguments {
2014            match arguments {
2015                Some(arguments) => {
2016                    let arguments_location = arguments.location();
2017                    let multiple = arguments.arguments().iter().count() > 1
2018                        || arguments
2019                            .arguments()
2020                            .iter()
2021                            .any(|argument| argument.as_splat_node().is_some());
2022                    let (open, close) = if multiple {
2023                        // `return a, b` already returns `[a, b]`.
2024                        (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ["), "])")
2025                    } else {
2026                        (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("), "))")
2027                    };
2028                    self.depth += 1;
2029                    self.edit(
2030                        arguments_location.start_offset(),
2031                        EditRank::Opener,
2032                        open,
2033                        arguments_location.end_offset(),
2034                    );
2035                    self.edit(
2036                        arguments_location.end_offset(),
2037                        EditRank::Closer,
2038                        close.into(),
2039                        arguments_location.start_offset(),
2040                    );
2041                    self.depth -= 1;
2042                }
2043                None => self.edit(
2044                    keyword.start_offset(),
2045                    EditRank::StatementProbe,
2046                    format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2047                    end,
2048                ),
2049            }
2050            return;
2051        }
2052        if last.as_redo_node().is_some() || last.as_retry_node().is_some() {
2053            self.edit(
2054                start,
2055                EditRank::StatementProbe,
2056                format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2057                end,
2058            );
2059            return;
2060        }
2061        self.depth += 1;
2062        self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("));
2063        self.depth -= 1;
2064    }
2065
2066    fn rescue_modifier(&mut self, node: &RescueModifierNode<'_>) {
2067        let location = node.location();
2068        let (start, end) = (location.start_offset(), location.end_offset());
2069        let Some(id) = self.branch(
2070            start,
2071            end,
2072            "rescue-modifier",
2073            [
2074                ("success", "expression completed"),
2075                ("raised", "fallback used"),
2076            ],
2077        ) else {
2078            return;
2079        };
2080        let key = self.probe_key(ProbeTarget::Try {
2081            success: format!("{id}:success"),
2082            raised: format!("{id}:raised"),
2083            id,
2084            handlers: Vec::new(),
2085        });
2086        let expression = node.expression();
2087        let fallback_node = node.rescue_expression();
2088        let fallback = fallback_node.location();
2089        self.depth += 1;
2090        if Self::is_jump(&expression) {
2091            // `return x rescue y` has no value to wrap: probe the jump's
2092            // argument or the jump itself, as for a body's final statement.
2093            self.completion_probe(expression, key);
2094        } else {
2095            let expression = expression.location();
2096            self.wrap(
2097                expression.start_offset(),
2098                expression.end_offset(),
2099                format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("),
2100            );
2101        }
2102        if Self::is_jump(&fallback_node) {
2103            // `rescue next` has no value either: `rescue (hm0(k); next)`.
2104            self.edit(
2105                fallback.start_offset(),
2106                EditRank::Opener,
2107                format!("({RUBY_PROBE_RECEIVER}.hm0({key}); "),
2108                fallback.end_offset(),
2109            );
2110            self.edit(
2111                fallback.end_offset(),
2112                EditRank::Closer,
2113                ")".into(),
2114                fallback.start_offset(),
2115            );
2116        } else {
2117            self.wrap(
2118                fallback.start_offset(),
2119                fallback.end_offset(),
2120                format!("{RUBY_PROBE_RECEIVER}.hm({key}, ("),
2121            );
2122        }
2123        self.depth -= 1;
2124    }
2125
2126    /// A statement that leaves its frame or loop without producing a value.
2127    fn is_jump(node: &Node<'_>) -> bool {
2128        node.as_return_node().is_some()
2129            || node.as_break_node().is_some()
2130            || node.as_next_node().is_some()
2131            || node.as_redo_node().is_some()
2132            || node.as_retry_node().is_some()
2133    }
2134
2135    fn def_node(&mut self, node: &DefNode<'_>) {
2136        let location = node.location();
2137        let (start, end) = (location.start_offset(), location.end_offset());
2138        let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
2139        let id = stable_id(self.file, "function", start, end, &name);
2140        if !self.point_ids.insert(id.clone()) {
2141            return;
2142        }
2143        self.push_point(&id, start, end, PointKind::Function, Some(name));
2144        let span = self.location_span(&location);
2145        self.methods.push(MethodKeyPlan {
2146            span,
2147            unshifted: span,
2148            id,
2149        });
2150        if let Some(body) = node.body()
2151            && let Some(begin) = body.as_begin_node()
2152        {
2153            self.begin_node(&begin, node.end_keyword_loc().map(|loc| loc.start_offset()));
2154        }
2155        if node.equal_loc().is_some()
2156            && let Some(body) = node.body()
2157            && let Some(statements) = body.as_statements_node()
2158        {
2159            for statement in statements.body().iter() {
2160                self.endless_bodies
2161                    .insert(statement.location().start_offset());
2162            }
2163        }
2164    }
2165
2166    // -- finishing ----------------------------------------------------------
2167
2168    /// Column shift the insertions cause on one line, for positions the
2169    /// runtime will read back from Ruby's `Coverage`. Insertions strictly
2170    /// inside a key's line range move whatever follows them. At a key's start,
2171    /// a probe moves an expression (it now follows the probe) but not a
2172    /// statement list whose first statement was probed, since the list still
2173    /// starts where the probe does; a list strictly containing the probed
2174    /// statement moves. An opener moves a key whose node it wraps (the node
2175    /// now sits inside the wrapper) and leaves alone a key whose node contains
2176    /// the wrapped one, since that node now begins with the wrapper. At a key's
2177    /// end, only a closer whose opener lies inside the key extends it: a list
2178    /// includes a wrapper around its last statement, an expression does not
2179    /// include the wrapper around itself. A point key follows everything
2180    /// inserted up to it, closers included.
2181    fn shifted(&self, span: PlanSpan, kind: KeyKind, edits: &[PendingEdit]) -> PlanSpan {
2182        let start_offset = self.line_starts[span.start[0] - 1] + span.start[1];
2183        let end_offset = self.line_starts[span.end[0] - 1] + span.end[1];
2184        let mut start_shift = 0;
2185        let mut end_shift = 0;
2186        for edit in edits {
2187            let (line, _) = self.line_column(edit.offset);
2188            let moves_start = edit.offset < start_offset
2189                || (edit.offset == start_offset
2190                    && match (edit.rank, kind) {
2191                        (_, KeyKind::Point) => true,
2192                        (EditRank::Closer, _) => false,
2193                        (EditRank::Opener, KeyKind::List) => end_offset < edit.scope,
2194                        (EditRank::Opener, KeyKind::Node) => end_offset <= edit.scope,
2195                        (_, KeyKind::List) => end_offset < edit.scope,
2196                        (_, KeyKind::Node) => true,
2197                    });
2198            let moves_end = edit.offset < end_offset
2199                || (edit.offset == end_offset
2200                    && match (edit.rank, kind) {
2201                        (_, KeyKind::Point) => true,
2202                        (EditRank::Closer, KeyKind::List) => edit.scope >= start_offset,
2203                        (EditRank::Closer, KeyKind::Node) => edit.scope > start_offset,
2204                        _ => false,
2205                    });
2206            if line == span.start[0] && moves_start {
2207                start_shift += edit.text.len();
2208            }
2209            if line == span.end[0] && moves_end {
2210                end_shift += edit.text.len();
2211            }
2212        }
2213        PlanSpan {
2214            start: [span.start[0], span.start[1] + start_shift],
2215            end: [span.end[0], span.end[1] + end_shift],
2216        }
2217    }
2218
2219    fn finish(mut self) -> RubyFileObligations {
2220        let mut pending = std::mem::take(&mut self.edits);
2221        pending.sort_by(|left, right| {
2222            left.offset
2223                .cmp(&right.offset)
2224                .then(left.rank.cmp(&right.rank))
2225                .then(left.order.cmp(&right.order))
2226                .then(left.sequence.cmp(&right.sequence))
2227        });
2228        let branches = std::mem::take(&mut self.branches)
2229            .into_iter()
2230            .map(|mut branch| {
2231                branch.key.span = self.shifted(branch.key.span, branch.key.kind, &pending);
2232                branch
2233            })
2234            .collect::<Vec<_>>();
2235        let cases = std::mem::take(&mut self.cases)
2236            .into_iter()
2237            .map(|mut case| {
2238                for clause in &mut case.clauses {
2239                    clause.key.span = self.shifted(clause.key.span, clause.key.kind, &pending);
2240                }
2241                if let Some(no_match) = &mut case.no_match {
2242                    no_match.key.span =
2243                        self.shifted(no_match.key.span, no_match.key.kind, &pending);
2244                }
2245                case
2246            })
2247            .collect();
2248        let methods = std::mem::take(&mut self.methods)
2249            .into_iter()
2250            .map(|mut method| {
2251                method.span = self.shifted(method.span, KeyKind::Node, &pending);
2252                method
2253            })
2254            .collect();
2255        let edits = pending
2256            .into_iter()
2257            .map(|edit| Edit {
2258                offset: edit.offset,
2259                text: edit.text,
2260                rank: match edit.rank {
2261                    EditRank::Clause => "clause",
2262                    EditRank::StatementProbe => "statement",
2263                    EditRank::Opener => "opener",
2264                    EditRank::Closer => "closer",
2265                }
2266                .into(),
2267                scope: edit.scope,
2268            })
2269            .collect::<Vec<_>>();
2270        if let Some((id, line)) = self.begin_unmeasured.first() {
2271            let source = self
2272                .manifest
2273                .branches
2274                .iter()
2275                .find(|branch| &branch.id == id)
2276                .map(|branch| branch.source.lines().next().unwrap_or_default().to_owned())
2277                .unwrap_or_default();
2278            self.manifest.limitations.push(limitation(
2279                BEGIN_BODY_LIMITATION,
2280                self.file,
2281                *line,
2282                &source,
2283                "a begin body that is empty, or ends in a statement with no expression form, cannot have its completion observed",
2284            ));
2285        }
2286        self.manifest.unmeasured.sort();
2287        self.manifest.unmeasured.dedup();
2288        RubyFileObligations {
2289            manifest: self.manifest,
2290            plan: RubyFilePlan {
2291                probe_obligations: probe_obligations_of(&self.probes),
2292                edits,
2293                lines: self.lines,
2294                statement_offsets: self.statement_offsets,
2295                branches,
2296                methods,
2297                cases,
2298            },
2299            probes: self.probes,
2300        }
2301    }
2302}
2303
2304fn limitation(id: &str, file: &str, line: usize, source: &str, reason: &str) -> serde_json::Value {
2305    json!({
2306        "id": id,
2307        "kind": "semantic-safety",
2308        "file": file,
2309        "line": line,
2310        "column": 0,
2311        "source": source,
2312        "reason": reason
2313    })
2314}
2315
2316impl<'pr> Visit<'pr> for Collector<'_> {
2317    fn visit_statements_node(&mut self, node: &StatementsNode<'pr>) {
2318        if !self
2319            .expression_lists
2320            .contains(&node.location().start_offset())
2321        {
2322            self.statements(node);
2323        }
2324        ruby_prism::visit_statements_node(self, node);
2325    }
2326
2327    fn visit_parentheses_node(&mut self, node: &ruby_prism::ParenthesesNode<'pr>) {
2328        if let Some(body) = node.body() {
2329            self.expression_lists.insert(body.location().start_offset());
2330        }
2331        ruby_prism::visit_parentheses_node(self, node);
2332    }
2333
2334    fn visit_embedded_statements_node(&mut self, node: &ruby_prism::EmbeddedStatementsNode<'pr>) {
2335        if let Some(statements) = node.statements() {
2336            self.expression_lists
2337                .insert(statements.location().start_offset());
2338        }
2339        ruby_prism::visit_embedded_statements_node(self, node);
2340    }
2341
2342    fn visit_if_node(&mut self, node: &IfNode<'pr>) {
2343        let offset = node.location().start_offset();
2344        if self.guard_nodes.contains(&offset) || self.elsif_nodes.contains(&offset) {
2345            self.depth += 1;
2346            ruby_prism::visit_if_node(self, node);
2347            self.depth -= 1;
2348            return;
2349        }
2350        if let Some(truthy) = self.literal_truth(node.predicate()) {
2351            // Ruby compiles only the live arm of `if false` / `if true` and
2352            // reports no branch for it; the dead arm is not code that can run.
2353            self.depth += 1;
2354            if truthy {
2355                if let Some(statements) = node.statements() {
2356                    self.visit_statements_node(&statements);
2357                }
2358            } else if let Some(subsequent) = node.subsequent() {
2359                match subsequent.as_if_node() {
2360                    Some(elsif) => self.visit_if_node(&elsif),
2361                    None => {
2362                        if let Some(else_node) = subsequent.as_else_node()
2363                            && let Some(statements) = else_node.statements()
2364                        {
2365                            self.visit_statements_node(&statements);
2366                        }
2367                    }
2368                }
2369            }
2370            self.depth -= 1;
2371            return;
2372        }
2373        // Ternaries have no `if` keyword; `elsif` is reached through
2374        // `subsequent` and handled by the parent's chain walk below.
2375        let kind = if node.if_keyword_loc().is_none() {
2376            "ternary"
2377        } else {
2378            "if"
2379        };
2380        self.if_node(node, kind);
2381        let mut subsequent = node.subsequent();
2382        while let Some(next) = subsequent {
2383            match next.as_if_node() {
2384                Some(elsif) => {
2385                    self.elsif_nodes.insert(elsif.location().start_offset());
2386                    self.if_node(&elsif, "elsif");
2387                    subsequent = elsif.subsequent();
2388                }
2389                None => break,
2390            }
2391        }
2392        self.depth += 1;
2393        // Children: predicate, statements, then the chain. Elsif nodes are
2394        // visited as children here too, but `if_node` deduplicates by
2395        // decision id.
2396        ruby_prism::visit_if_node(self, node);
2397        self.depth -= 1;
2398    }
2399
2400    fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
2401        if self.guard_nodes.contains(&node.location().start_offset()) {
2402            self.depth += 1;
2403            ruby_prism::visit_unless_node(self, node);
2404            self.depth -= 1;
2405            return;
2406        }
2407        if let Some(truthy) = self.literal_truth(node.predicate()) {
2408            self.depth += 1;
2409            if truthy {
2410                if let Some(else_node) = node.else_clause()
2411                    && let Some(statements) = else_node.statements()
2412                {
2413                    self.visit_statements_node(&statements);
2414                }
2415            } else if let Some(statements) = node.statements() {
2416                self.visit_statements_node(&statements);
2417            }
2418            self.depth -= 1;
2419            return;
2420        }
2421        self.unless_node(node);
2422        self.depth += 1;
2423        ruby_prism::visit_unless_node(self, node);
2424        self.depth -= 1;
2425    }
2426
2427    fn visit_while_node(&mut self, node: &WhileNode<'pr>) {
2428        self.loop_node(
2429            &node.location(),
2430            node.predicate(),
2431            node.statements(),
2432            node.is_begin_modifier(),
2433            false,
2434        );
2435        self.depth += 1;
2436        ruby_prism::visit_while_node(self, node);
2437        self.depth -= 1;
2438    }
2439
2440    fn visit_until_node(&mut self, node: &UntilNode<'pr>) {
2441        self.loop_node(
2442            &node.location(),
2443            node.predicate(),
2444            node.statements(),
2445            node.is_begin_modifier(),
2446            true,
2447        );
2448        self.depth += 1;
2449        ruby_prism::visit_until_node(self, node);
2450        self.depth -= 1;
2451    }
2452
2453    fn visit_for_node(&mut self, node: &ForNode<'pr>) {
2454        self.for_node(node);
2455        self.depth += 1;
2456        ruby_prism::visit_for_node(self, node);
2457        self.depth -= 1;
2458    }
2459
2460    fn visit_case_node(&mut self, node: &CaseNode<'pr>) {
2461        self.case_node(node);
2462        self.depth += 1;
2463        ruby_prism::visit_case_node(self, node);
2464        self.depth -= 1;
2465    }
2466
2467    fn visit_case_match_node(&mut self, node: &CaseMatchNode<'pr>) {
2468        self.case_match_node(node);
2469        self.depth += 1;
2470        ruby_prism::visit_case_match_node(self, node);
2471        self.depth -= 1;
2472    }
2473
2474    fn visit_and_node(&mut self, node: &AndNode<'pr>) {
2475        let location = node.location();
2476        if !self.tree_logicals.contains(&location.start_offset()) {
2477            let left = node.left();
2478            self.value_logical("and", &left, location.start_offset(), location.end_offset());
2479        }
2480        self.depth += 1;
2481        ruby_prism::visit_and_node(self, node);
2482        self.depth -= 1;
2483    }
2484
2485    fn visit_or_node(&mut self, node: &OrNode<'pr>) {
2486        let location = node.location();
2487        if !self.tree_logicals.contains(&location.start_offset()) {
2488            let left = node.left();
2489            self.value_logical("or", &left, location.start_offset(), location.end_offset());
2490        }
2491        self.depth += 1;
2492        ruby_prism::visit_or_node(self, node);
2493        self.depth -= 1;
2494    }
2495
2496    fn visit_call_node(&mut self, node: &CallNode<'pr>) {
2497        if node.is_safe_navigation() {
2498            self.safe_navigation(node);
2499        }
2500        if let Some(block) = node.block()
2501            && let Some(block) = block.as_block_node()
2502            && let Some(receiver) = node.receiver()
2503            && !node.is_safe_navigation()
2504            && ITERATORS.contains(&node.name().as_slice())
2505        {
2506            self.iterator_loop(node, &receiver, &block);
2507        }
2508        self.depth += 1;
2509        ruby_prism::visit_call_node(self, node);
2510        self.depth -= 1;
2511    }
2512
2513    fn visit_local_variable_or_write_node(
2514        &mut self,
2515        node: &ruby_prism::LocalVariableOrWriteNode<'pr>,
2516    ) {
2517        let location = node.location();
2518        let value = node.value();
2519        self.op_assign(
2520            "or",
2521            location.start_offset(),
2522            location.end_offset(),
2523            Some(node.name().as_slice()),
2524            &value,
2525        );
2526        self.depth += 1;
2527        ruby_prism::visit_local_variable_or_write_node(self, node);
2528        self.depth -= 1;
2529    }
2530
2531    fn visit_local_variable_and_write_node(
2532        &mut self,
2533        node: &ruby_prism::LocalVariableAndWriteNode<'pr>,
2534    ) {
2535        let location = node.location();
2536        let value = node.value();
2537        self.op_assign(
2538            "and",
2539            location.start_offset(),
2540            location.end_offset(),
2541            Some(node.name().as_slice()),
2542            &value,
2543        );
2544        self.depth += 1;
2545        ruby_prism::visit_local_variable_and_write_node(self, node);
2546        self.depth -= 1;
2547    }
2548
2549    fn visit_instance_variable_or_write_node(
2550        &mut self,
2551        node: &ruby_prism::InstanceVariableOrWriteNode<'pr>,
2552    ) {
2553        let location = node.location();
2554        let value = node.value();
2555        self.op_assign(
2556            "or",
2557            location.start_offset(),
2558            location.end_offset(),
2559            Some(node.name().as_slice()),
2560            &value,
2561        );
2562        self.depth += 1;
2563        ruby_prism::visit_instance_variable_or_write_node(self, node);
2564        self.depth -= 1;
2565    }
2566
2567    fn visit_instance_variable_and_write_node(
2568        &mut self,
2569        node: &ruby_prism::InstanceVariableAndWriteNode<'pr>,
2570    ) {
2571        let location = node.location();
2572        let value = node.value();
2573        self.op_assign(
2574            "and",
2575            location.start_offset(),
2576            location.end_offset(),
2577            Some(node.name().as_slice()),
2578            &value,
2579        );
2580        self.depth += 1;
2581        ruby_prism::visit_instance_variable_and_write_node(self, node);
2582        self.depth -= 1;
2583    }
2584
2585    fn visit_global_variable_or_write_node(
2586        &mut self,
2587        node: &ruby_prism::GlobalVariableOrWriteNode<'pr>,
2588    ) {
2589        let location = node.location();
2590        let value = node.value();
2591        self.op_assign(
2592            "or",
2593            location.start_offset(),
2594            location.end_offset(),
2595            Some(node.name().as_slice()),
2596            &value,
2597        );
2598        self.depth += 1;
2599        ruby_prism::visit_global_variable_or_write_node(self, node);
2600        self.depth -= 1;
2601    }
2602
2603    fn visit_class_variable_or_write_node(
2604        &mut self,
2605        node: &ruby_prism::ClassVariableOrWriteNode<'pr>,
2606    ) {
2607        let location = node.location();
2608        let value = node.value();
2609        self.op_assign(
2610            "or",
2611            location.start_offset(),
2612            location.end_offset(),
2613            Some(node.name().as_slice()),
2614            &value,
2615        );
2616        self.depth += 1;
2617        ruby_prism::visit_class_variable_or_write_node(self, node);
2618        self.depth -= 1;
2619    }
2620
2621    fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode<'pr>) {
2622        let location = node.location();
2623        let value = node.value();
2624        self.op_assign(
2625            "or",
2626            location.start_offset(),
2627            location.end_offset(),
2628            None,
2629            &value,
2630        );
2631        self.depth += 1;
2632        ruby_prism::visit_call_or_write_node(self, node);
2633        self.depth -= 1;
2634    }
2635
2636    fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode<'pr>) {
2637        let location = node.location();
2638        let value = node.value();
2639        self.op_assign(
2640            "and",
2641            location.start_offset(),
2642            location.end_offset(),
2643            None,
2644            &value,
2645        );
2646        self.depth += 1;
2647        ruby_prism::visit_call_and_write_node(self, node);
2648        self.depth -= 1;
2649    }
2650
2651    fn visit_index_or_write_node(&mut self, node: &ruby_prism::IndexOrWriteNode<'pr>) {
2652        let location = node.location();
2653        let value = node.value();
2654        self.op_assign(
2655            "or",
2656            location.start_offset(),
2657            location.end_offset(),
2658            None,
2659            &value,
2660        );
2661        self.depth += 1;
2662        ruby_prism::visit_index_or_write_node(self, node);
2663        self.depth -= 1;
2664    }
2665
2666    fn visit_index_and_write_node(&mut self, node: &ruby_prism::IndexAndWriteNode<'pr>) {
2667        let location = node.location();
2668        let value = node.value();
2669        self.op_assign(
2670            "and",
2671            location.start_offset(),
2672            location.end_offset(),
2673            None,
2674            &value,
2675        );
2676        self.depth += 1;
2677        ruby_prism::visit_index_and_write_node(self, node);
2678        self.depth -= 1;
2679    }
2680
2681    fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode<'pr>) {
2682        let location = node.location();
2683        let value = node.value();
2684        self.op_assign(
2685            "or",
2686            location.start_offset(),
2687            location.end_offset(),
2688            None,
2689            &value,
2690        );
2691        self.depth += 1;
2692        ruby_prism::visit_constant_or_write_node(self, node);
2693        self.depth -= 1;
2694    }
2695
2696    fn visit_def_node(&mut self, node: &DefNode<'pr>) {
2697        self.def_node(node);
2698        self.depth += 1;
2699        ruby_prism::visit_def_node(self, node);
2700        self.depth -= 1;
2701    }
2702
2703    fn visit_begin_node(&mut self, node: &BeginNode<'pr>) {
2704        // Explicit `begin ... end`. Method and block bodies reach `begin_node`
2705        // through their hosts, which know the closing keyword; the branch id
2706        // dedupes the second visit.
2707        if node.begin_keyword_loc().is_some() {
2708            self.begin_node(node, None);
2709        }
2710        self.depth += 1;
2711        ruby_prism::visit_begin_node(self, node);
2712        self.depth -= 1;
2713    }
2714
2715    fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
2716        if let Some(body) = node.body()
2717            && let Some(begin) = body.as_begin_node()
2718        {
2719            self.begin_node(&begin, Some(node.closing_loc().start_offset()));
2720        }
2721        self.depth += 1;
2722        ruby_prism::visit_block_node(self, node);
2723        self.depth -= 1;
2724    }
2725
2726    fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
2727        if let Some(body) = node.body()
2728            && let Some(begin) = body.as_begin_node()
2729        {
2730            self.begin_node(&begin, Some(node.closing_loc().start_offset()));
2731        }
2732        self.depth += 1;
2733        ruby_prism::visit_lambda_node(self, node);
2734        self.depth -= 1;
2735    }
2736
2737    fn visit_rescue_modifier_node(&mut self, node: &RescueModifierNode<'pr>) {
2738        self.rescue_modifier(node);
2739        self.depth += 1;
2740        ruby_prism::visit_rescue_modifier_node(self, node);
2741        self.depth -= 1;
2742    }
2743}
2744
2745/// Build the complete obligation manifest and probe plan for one Ruby file.
2746/// `next_probe` numbers probes uniquely across the whole project.
2747pub fn build_ruby_obligations(
2748    file: &str,
2749    source: &[u8],
2750    next_probe: &mut u64,
2751) -> Result<RubyFileObligations, RubyInstrumenterError> {
2752    let result = ruby_prism::parse(source);
2753    let errors = result
2754        .errors()
2755        .map(|error| error.message().to_owned())
2756        .collect::<Vec<_>>();
2757    if !errors.is_empty() {
2758        return Err(RubyInstrumenterError::Parse(errors.join("; ")));
2759    }
2760    let mut collector = Collector::new(file, source, next_probe);
2761    collector.visit(&result.node());
2762    if let Some(error) = collector.error.take() {
2763        return Err(error);
2764    }
2765    Ok(collector.finish())
2766}
2767
2768/// Apply a plan's edits to the original source the way the runtime does.
2769pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
2770    let mut output =
2771        Vec::with_capacity(source.len() + edits.iter().map(|e| e.text.len()).sum::<usize>());
2772    let mut cursor = 0;
2773    for edit in edits {
2774        output.extend_from_slice(&source[cursor..edit.offset]);
2775        output.extend_from_slice(edit.text.as_bytes());
2776        cursor = edit.offset;
2777    }
2778    output.extend_from_slice(&source[cursor..]);
2779    output
2780}
2781
2782#[cfg(test)]
2783mod tests {
2784    use super::*;
2785
2786    const SOURCE: &str = r#"class Shapes
2787  def classify(a, b, c)
2788    if a && (b || c)
2789      :yes
2790    elsif a
2791      :half
2792    else
2793      :no
2794    end
2795  end
2796
2797  def loops(items, flag)
2798    total = 0
2799    items.each { |i| total += i if i > 2 && flag }
2800    while total > 100
2801      total -= 50
2802    end
2803    for x in items do total += x end
2804    total
2805  end
2806
2807  def logical(a, b)
2808    x = a || b
2809    @cache ||= {}
2810    @cache[a] ||= b
2811    y = a ? 1 : 2; z = a&.size
2812    [x, y, z]
2813  end
2814
2815  def guarded(s)
2816    Integer(s)
2817  rescue ArgumentError
2818    -1
2819  ensure
2820    @done = true
2821  end
2822
2823  def cases(v)
2824    case v
2825    when 0 then :zero
2826    else :other
2827    end
2828    v.to_s rescue "bad"
2829  end
2830end
2831"#;
2832
2833    #[test]
2834    fn discovers_obligations_with_stable_ids_and_newline_free_edits() {
2835        let mut probe = 0;
2836        let first = build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2837        let mut probe = 0;
2838        let second =
2839            build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2840        assert_eq!(first, second);
2841        let manifest = &first.manifest;
2842        let functions = manifest
2843            .points
2844            .iter()
2845            .filter(|point| point.kind == PointKind::Function)
2846            .map(|point| point.label.clone().unwrap())
2847            .collect::<Vec<_>>();
2848        assert_eq!(
2849            functions,
2850            ["classify", "loops", "logical", "guarded", "cases"]
2851        );
2852        let compound = manifest
2853            .decisions
2854            .iter()
2855            .find(|decision| decision.source == "a && (b || c)")
2856            .unwrap();
2857        assert_eq!(compound.conditions, ["a", "b", "c"]);
2858        assert_eq!(compound.kind, "if");
2859        assert!(
2860            manifest
2861                .decisions
2862                .iter()
2863                .any(|d| d.kind == "elsif" && d.conditions == ["a"])
2864        );
2865        assert!(manifest.decisions.iter().any(|d| d.kind == "ternary"));
2866        assert!(manifest.decisions.iter().any(|d| d.kind == "while"));
2867        for kind in [
2868            "for",
2869            "logical-or",
2870            "or-assign",
2871            "safe-navigation",
2872            "begin",
2873            "rescue-0",
2874            "case-when-0",
2875            "case-else",
2876            "rescue-modifier",
2877        ] {
2878            assert!(
2879                manifest.branches.iter().any(|branch| branch.kind == kind),
2880                "missing branch kind {kind}"
2881            );
2882        }
2883        for edit in &first.plan.edits {
2884            assert!(!edit.text.contains('\n'));
2885        }
2886        assert!(
2887            first
2888                .plan
2889                .edits
2890                .windows(2)
2891                .all(|pair| pair[0].offset <= pair[1].offset)
2892        );
2893        // `@cache[a] ||= b` is measured through arrival and right-side
2894        // probes; nothing is unmeasured.
2895        assert!(manifest.unmeasured.is_empty());
2896        assert!(manifest.limitations.is_empty());
2897    }
2898
2899    #[test]
2900    fn transformed_source_keeps_line_count_and_carries_probes() {
2901        let mut probe = 0;
2902        let obligations =
2903            build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2904        let transformed =
2905            String::from_utf8(apply_edits(SOURCE.as_bytes(), &obligations.plan.edits)).unwrap();
2906        assert_eq!(transformed.lines().count(), SOURCE.lines().count());
2907        assert!(
2908            transformed.contains("if $__supercov.d(0, ($__supercov.c(0, 0, (a)) && ($__supercov.c(0, 1, (b)) || $__supercov.c(0, 2, (c)))))"),
2909            "{transformed}"
2910        );
2911        assert!(transformed.contains("while $__supercov.w("));
2912        assert!(transformed.contains("for x in $__supercov.f("));
2913        assert!(transformed.contains("do $__supercov.fb("));
2914        assert!(transformed.contains("x = $__supercov.l("));
2915        assert!(transformed.contains("($__supercov.pre("));
2916        assert!(transformed.contains("||= ($__supercov.es("));
2917        assert!(transformed.contains("rescue Exception => __supercov_e; $__supercov.p("));
2918        assert!(transformed.contains("$__supercov.h("));
2919        assert!(transformed.contains("$__supercov.ok("));
2920        assert!(transformed.contains("rescue $__supercov.hm("));
2921        // The second statement on the `y = ...; z = ...` line gets a probe.
2922        assert!(transformed.contains("; $__supercov.s("));
2923        // Same-offset modifier bodies are proven by the stdlib branch key.
2924        assert!(
2925            obligations
2926                .plan
2927                .branches
2928                .iter()
2929                .any(|branch| !branch.hits.is_empty() && branch.key.group == "if")
2930        );
2931    }
2932
2933    #[test]
2934    fn jumps_and_endless_bodies_take_wrapped_probes_and_literal_predicates_fold() {
2935        let source = "def inc(x) = x + 1\n\
2936                      [1].each { |v| y = Integer(v) rescue next }\n\
2937                      if false\n  dead\nelse\n  live\nend\n";
2938        let mut probe = 0;
2939        let obligations =
2940            build_ruby_obligations("lib/x.rb", source.as_bytes(), &mut probe).unwrap();
2941        let transformed =
2942            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
2943        assert!(
2944            transformed.contains("def inc(x) = ($__supercov.s("),
2945            "{transformed}"
2946        );
2947        assert!(
2948            transformed.contains("rescue ($__supercov.hm0("),
2949            "{transformed}"
2950        );
2951        // `if false` has no branch and its dead arm no statements.
2952        assert!(
2953            obligations
2954                .plan
2955                .branches
2956                .iter()
2957                .all(|branch| branch.key.group != "if")
2958        );
2959        assert!(
2960            obligations
2961                .manifest
2962                .points
2963                .iter()
2964                .all(|point| point.source != "dead")
2965        );
2966        assert!(
2967            obligations
2968                .manifest
2969                .points
2970                .iter()
2971                .any(|point| point.source == "live")
2972        );
2973    }
2974
2975    #[test]
2976    fn void_valued_last_statements_are_probed_arm_by_arm() {
2977        // Wrapping `if ... return ... else return ... end` in a value context
2978        // is a syntax error; each arm carries the completion probe instead.
2979        let source =
2980            "def m(c)\n  if c\n    return 1\n  else\n    return 2\n  end\nrescue\n  nil\nend\n";
2981        let mut probe = 0;
2982        let obligations =
2983            build_ruby_obligations("lib/v.rb", source.as_bytes(), &mut probe).unwrap();
2984        let transformed =
2985            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
2986        assert_eq!(
2987            transformed.matches("$__supercov.ok(").count(),
2988            2,
2989            "{transformed}"
2990        );
2991        assert!(
2992            transformed.contains("return $__supercov.ok("),
2993            "{transformed}"
2994        );
2995        assert!(
2996            !obligations
2997                .manifest
2998                .unmeasured
2999                .iter()
3000                .any(|id| id.ends_with(":success")),
3001            "{:?}",
3002            obligations.manifest.unmeasured
3003        );
3004        assert!(!obligations.plan.probe_obligations.is_empty());
3005    }
3006
3007    #[test]
3008    fn expressions_that_can_return_are_never_wrapped() {
3009        // `ok(k, (if a then return b end))` passes an expression that can jump
3010        // as an argument, which makes Ruby's compiler miscount its stack.
3011        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";
3012        let mut probe = 0;
3013        let obligations =
3014            build_ruby_obligations("lib/e.rb", source.as_bytes(), &mut probe).unwrap();
3015        let transformed =
3016            String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3017        assert!(!transformed.contains("$__supercov.ok"), "{transformed}");
3018        assert!(
3019            obligations
3020                .manifest
3021                .unmeasured
3022                .iter()
3023                .any(|id| id.ends_with(":success")),
3024            "the begin's completion is declared instead"
3025        );
3026        // With both arms present the arms carry the probe and nothing is lost.
3027        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";
3028        let mut probe = 0;
3029        let obligations = build_ruby_obligations("lib/f.rb", both.as_bytes(), &mut probe).unwrap();
3030        let transformed =
3031            String::from_utf8(apply_edits(both.as_bytes(), &obligations.plan.edits)).unwrap();
3032        assert!(
3033            transformed.contains("return $__supercov.ok("),
3034            "{transformed}"
3035        );
3036        assert!(
3037            !obligations
3038                .manifest
3039                .unmeasured
3040                .iter()
3041                .any(|id| id.ends_with(":success")),
3042            "{:?}",
3043            obligations.manifest.unmeasured
3044        );
3045    }
3046
3047    #[test]
3048    fn stdlib_keys_shift_with_insertions_on_their_line() {
3049        let source = "def f(a, b)\n  x = 1 if a && b\nend\n";
3050        let mut probe = 0;
3051        let obligations = build_ruby_obligations("m.rb", source.as_bytes(), &mut probe).unwrap();
3052        let then_key = obligations
3053            .plan
3054            .branches
3055            .iter()
3056            .find(|branch| branch.key.branch == "then")
3057            .unwrap();
3058        // `x = 1` starts at column 2 and nothing is inserted before it.
3059        assert_eq!(then_key.key.span.start, [2, 2]);
3060        // Its end (column 7) is untouched too: insertions land in the
3061        // predicate, which comes after the body on this line.
3062        assert_eq!(then_key.key.span.end, [2, 7]);
3063        let else_key = obligations
3064            .plan
3065            .branches
3066            .iter()
3067            .find(|branch| branch.key.branch == "else")
3068            .unwrap();
3069        // The implicit else uses the whole if node, whose end moves right by
3070        // every inserted byte on that line.
3071        let inserted: usize = obligations
3072            .plan
3073            .edits
3074            .iter()
3075            .map(|edit| edit.text.len())
3076            .sum();
3077        assert_eq!(else_key.key.span.end, [2, 17 + inserted]);
3078        assert!(
3079            then_key.hits.len() == 1,
3080            "modifier body statement proven by the then key"
3081        );
3082    }
3083
3084    #[test]
3085    fn rejects_invalid_ruby() {
3086        let mut probe = 0;
3087        assert!(matches!(
3088            build_ruby_obligations("m.rb", b"def x(\n", &mut probe),
3089            Err(RubyInstrumenterError::Parse(_))
3090        ));
3091    }
3092}