Skip to main content

codehelion_helper_protocol/
ir.rs

1//! What a compiler knows, in the shape both sides agree on.
2//!
3//! This is the payload the whole process boundary exists to carry: resolved
4//! symbols and types, a control-flow graph, what each call actually calls, and
5//! where a generic body was instantiated from. A helper produces it with one
6//! compiler; the analysis crates consume it without knowing which.
7//!
8//! # Anchoring, and why expansion is the side that anchors
9//!
10//! Every node carries an [`Anchor`] rather than a single range, because code
11//! that came from a macro or a template has two places and they answer
12//! different questions. The *expansion* site is where the code physically sits
13//! in the file someone reads, which is the only place a syntax fragment can be
14//! cut from — so that is what a node anchors to. The *definition* site is where
15//! the text was actually written, and it is kept because it is what tells
16//! repetition apart from duplication.
17//!
18//! The distinction is not academic. A macro invoked twenty times produces
19//! twenty identical bodies, and a detector that anchors only at the expansion
20//! site reports twenty clones of something nobody wrote twice and nobody can
21//! remove — the labelled corpora call that shape something other than
22//! duplication, consistently. Keeping the definition site lets a group say "one
23//! definition, twenty expansions" instead.
24//!
25//! # Auxiliary semantic evidence
26//!
27//! [`EffectSummary`] reports only closed, compiler-confirmed resource
28//! interactions. Its empty list is never a purity claim. [`DataFlowSummary`]
29//! records only bounded compiler-confirmed operation flows; absent evidence
30//! never changes which semantic findings exist.
31
32use std::path::Path;
33
34use serde::{Deserialize, Serialize};
35
36/// The compiler-IR schema identifier.
37///
38/// The product has not been released, so the complete current shape is the
39/// only supported wire contract.
40pub const COMPILER_IR_SCHEMA_VERSION: &str = "compiler-ir-v1";
41
42/// A half-open byte range in one file, with the line its start falls on.
43///
44/// Byte offsets rather than lines alone: a line number cannot say where inside
45/// a line something begins, and clone fragments regularly do.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct SourceRange {
48    /// Path as the analysis spells it — see [`CompilerIr::anchored_at`] for
49    /// what it is spelled against.
50    pub file: String,
51    /// First byte covered.
52    pub start_byte: u64,
53    /// One past the last byte covered.
54    pub end_byte: u64,
55    /// Line the first byte falls on, counting from one.
56    pub start_line: u32,
57}
58
59/// Where a node is, and where it was written.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct Anchor {
62    /// Where the code sits in the file being read. Syntax fragments are cut
63    /// from here, so this is what a node anchors to.
64    pub expansion: SourceRange,
65    /// Where the code was written, when that is somewhere else — inside a
66    /// macro body, or in the template this instantiation came from.
67    ///
68    /// `None` for code that is where it was written.
69    pub definition: Option<SourceRange>,
70}
71
72impl Anchor {
73    /// An anchor for code that is where it was written.
74    #[must_use]
75    pub const fn written_here(range: SourceRange) -> Self {
76        Self {
77            expansion: range,
78            definition: None,
79        }
80    }
81
82    /// Whether this node was produced somewhere other than where it reads.
83    #[must_use]
84    pub const fn is_expanded(&self) -> bool {
85        self.definition.is_some()
86    }
87}
88
89/// The normalized kind of a type.
90///
91/// Deliberately coarse. Two languages do not agree on what a type *is*, and a
92/// similarity measure that compares spelled type names compares vocabularies
93/// rather than programs. What survives translation is the shape: whether a
94/// value is a number, a sequence, a handle to something else.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum TypeCategory {
98    /// Any integer width or signedness.
99    Integer,
100    /// Any floating-point width.
101    Float,
102    /// A boolean.
103    Boolean,
104    /// A character or code point.
105    Character,
106    /// A string or string slice.
107    Text,
108    /// A raw pointer or a reference.
109    Handle,
110    /// A contiguous sequence: array, slice, vector.
111    Sequence,
112    /// An associative container.
113    Mapping,
114    /// A fixed heterogeneous group: tuple, pair.
115    Tuple,
116    /// A record with named fields.
117    Record,
118    /// A closed set of alternatives.
119    Enumeration,
120    /// An interface: trait, abstract base, concept.
121    Interface,
122    /// Something callable: function, method, closure.
123    Callable,
124    /// A type parameter not yet substituted.
125    Parameter,
126    /// The absence of a value: unit, void.
127    Nothing,
128    /// A type the helper could not resolve.
129    Unresolved,
130}
131
132impl TypeCategory {
133    /// Stable lowercase identifier, the same spelling this serializes as.
134    #[must_use]
135    pub const fn name(self) -> &'static str {
136        match self {
137            Self::Integer => "integer",
138            Self::Float => "float",
139            Self::Boolean => "boolean",
140            Self::Character => "character",
141            Self::Text => "text",
142            Self::Handle => "handle",
143            Self::Sequence => "sequence",
144            Self::Mapping => "mapping",
145            Self::Tuple => "tuple",
146            Self::Record => "record",
147            Self::Enumeration => "enumeration",
148            Self::Interface => "interface",
149            Self::Callable => "callable",
150            Self::Parameter => "parameter",
151            Self::Nothing => "nothing",
152            Self::Unresolved => "unresolved",
153        }
154    }
155}
156
157/// A name the compiler resolved to a definition.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ResolvedSymbol {
160    /// Stable identity of the definition within this build, as the compiler
161    /// spells it — a path, a mangled name, a USR.
162    pub id: String,
163    /// The name a reader would use.
164    pub name: String,
165    /// What kind of thing it is.
166    pub kind: SymbolKind,
167    /// Where the use is, and where the definition was written.
168    pub anchor: Anchor,
169    /// Its type, as an index into [`CompilerIr::types`].
170    pub type_index: Option<u32>,
171    /// Whether the definition is outside the code being scanned.
172    ///
173    /// The difference matters to normalization: a call into a library names an
174    /// interface two fragments genuinely share, while a call to a local
175    /// function names something one of them happens to have called.
176    pub external: bool,
177}
178
179/// What kind of definition a symbol names.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum SymbolKind {
183    /// A function, method, or closure.
184    Function,
185    /// A type definition.
186    Type,
187    /// A field or member.
188    Field,
189    /// A variant of an enumeration.
190    Variant,
191    /// A local binding or parameter.
192    Binding,
193    /// A constant or static.
194    Constant,
195    /// A module, namespace, or crate.
196    Namespace,
197    /// A compiler fact that has no more specific symbol category.
198    Other,
199}
200
201impl SymbolKind {
202    /// Stable lowercase identifier, the same spelling this serializes as.
203    #[must_use]
204    pub const fn name(self) -> &'static str {
205        match self {
206            Self::Function => "function",
207            Self::Type => "type",
208            Self::Field => "field",
209            Self::Variant => "variant",
210            Self::Binding => "binding",
211            Self::Constant => "constant",
212            Self::Namespace => "namespace",
213            Self::Other => "other",
214        }
215    }
216}
217
218/// A type as the compiler resolved it.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct ResolvedType {
221    /// The type as the compiler spells it, for display.
222    pub display: String,
223    /// Its normalized category.
224    pub category: TypeCategory,
225    /// Types it is built from: element, key and value, parameters.
226    pub arguments: Vec<u32>,
227    /// The symbol defining it, when it has one.
228    pub definition: Option<String>,
229}
230
231/// One call, and what it was found to call.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct CallSite {
234    /// Where the call is.
235    pub anchor: Anchor,
236    /// What it calls.
237    pub target: CallTarget,
238    /// A compiler-confirmed standard-library API name, when the helper can
239    /// establish it without deriving it from the stable target identifier.
240    ///
241    /// `None` for calls outside the deliberately small API vocabulary used by
242    /// restricted semantic rules.
243    pub api_name: Option<String>,
244}
245
246/// One compiler-confirmed construct that a restricted semantic rule may
247/// normalize without reconstructing syntax in the analysis process.
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct SemanticConstruct {
250    /// Where the construct occurs, preserving macro expansion provenance.
251    pub anchor: Anchor,
252    /// The closed meaning the helper established for this construct.
253    pub kind: SemanticConstructKind,
254    /// The standard fallible container the compiler resolved, when this
255    /// construct operates on one.
256    ///
257    /// `None` when the construct does not operate on a fallible container.
258    pub fallible_kind: Option<FallibleKind>,
259    /// A closed form that makes this propagation directly comparable to a
260    /// different spelling without general equivalence reasoning.
261    pub direct_propagation: Option<DirectPropagation>,
262    /// Closed resource category for a compiler-confirmed acquire or release.
263    /// It is absent for every other construct.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub resource_kind: Option<String>,
266}
267
268/// A standard library fallible container established by the compiler.
269///
270/// This is deliberately narrower than a general algebraic-data-type category:
271/// registered rules must not treat a project enum with similarly named arms as
272/// a `Result` or `Option`.
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "snake_case")]
275pub enum FallibleKind {
276    /// `core::option::Option` or its standard-library re-export.
277    Option,
278    /// `core::result::Result` or its standard-library re-export.
279    Result,
280}
281
282impl FallibleKind {
283    /// Stable storage spelling.
284    #[must_use]
285    pub const fn name(self) -> &'static str {
286        match self {
287            Self::Option => "option",
288            Self::Result => "result",
289        }
290    }
291
292    /// Parse the stable storage spelling.
293    #[must_use]
294    pub fn parse(name: &str) -> Option<Self> {
295        match name {
296            "option" => Some(Self::Option),
297            "result" => Some(Self::Result),
298            _ => None,
299        }
300    }
301}
302
303/// A compiler-confirmed direct spelling of a fallible propagation operation.
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub enum DirectPropagation {
307    /// `Ok(value?)` or an identity `Result` match.
308    ResultAdapter,
309    /// `Some(value?)` or an identity `Option` match.
310    OptionAdapter,
311}
312
313impl DirectPropagation {
314    /// Stable storage spelling.
315    #[must_use]
316    pub const fn name(self) -> &'static str {
317        match self {
318            Self::ResultAdapter => "result_adapter",
319            Self::OptionAdapter => "option_adapter",
320        }
321    }
322
323    /// Parse the stable storage spelling.
324    #[must_use]
325    pub fn parse(name: &str) -> Option<Self> {
326        match name {
327            "result_adapter" => Some(Self::ResultAdapter),
328            "option_adapter" => Some(Self::OptionAdapter),
329            _ => None,
330        }
331    }
332}
333
334/// Closed semantic constructs that compiler helpers may report.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum SemanticConstructKind {
338    /// A compiler-verified sequence consumed by a restricted explicit loop.
339    Source,
340    /// A compiler-verified explicit loop materialized one element per iteration.
341    Collect,
342    /// A compiler-verified explicit loop accumulated every sequence element.
343    Reduce,
344    /// Rust `?` propagated an error-like value to the surrounding caller.
345    PropagateError,
346    /// Rust selected a branch after checking a fallible or optional value.
347    Validate,
348    /// A compiler-confirmed standard operation acquired a tracked resource.
349    AcquireResource,
350    /// A lexical scope ended and released a tracked resource.
351    ReleaseResource,
352}
353
354impl SemanticConstructKind {
355    /// Stable storage spelling.
356    #[must_use]
357    pub const fn name(self) -> &'static str {
358        match self {
359            Self::Source => "source",
360            Self::Collect => "collect",
361            Self::Reduce => "reduce",
362            Self::PropagateError => "propagate_error",
363            Self::Validate => "validate",
364            Self::AcquireResource => "acquire_resource",
365            Self::ReleaseResource => "release_resource",
366        }
367    }
368
369    /// Parse the stable storage spelling.
370    #[must_use]
371    pub fn parse(name: &str) -> Option<Self> {
372        match name {
373            "source" => Some(Self::Source),
374            "collect" => Some(Self::Collect),
375            "reduce" => Some(Self::Reduce),
376            "propagate_error" => Some(Self::PropagateError),
377            "validate" => Some(Self::Validate),
378            "acquire_resource" => Some(Self::AcquireResource),
379            "release_resource" => Some(Self::ReleaseResource),
380            _ => None,
381        }
382    }
383}
384
385/// The type the compiler resolved for an expression with no physical source
386/// token of its own, such as the body a declarative macro generated.
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct ResolvedExpression {
389    /// Where the expression is observable and where it was written.
390    pub anchor: Anchor,
391    /// Its entry in [`CompilerIr::types`].
392    pub type_index: u32,
393}
394
395/// A macro invocation the helper deliberately did not expand.
396///
397/// This is coverage information, not a failed unit. A procedural macro can
398/// leave the surrounding crate meaningful while its generated declarations
399/// remain unavailable; recording the invocation prevents that thin answer
400/// from reading as a complete one.
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct UnexpandedMacro {
403    /// The invocation written in the analysed source.
404    pub invocation: SourceRange,
405    /// Why its expansion is absent.
406    pub reason: UnexpandedMacroReason,
407}
408
409/// Why an individual macro invocation was not expanded.
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
411#[serde(rename_all = "snake_case")]
412pub enum UnexpandedMacroReason {
413    /// Expanding the invocation would execute a procedural macro.
414    RequiresExecution,
415    /// The analysis engine could not resolve the invocation to a macro.
416    Unresolved,
417    /// A declarative macro was known but its expansion was unavailable.
418    ExpansionUnavailable,
419}
420
421impl UnexpandedMacroReason {
422    /// Stable spelling for storage and reporting.
423    #[must_use]
424    pub const fn name(self) -> &'static str {
425        match self {
426            Self::RequiresExecution => "requires_execution",
427            Self::Unresolved => "unresolved",
428            Self::ExpansionUnavailable => "expansion_unavailable",
429        }
430    }
431
432    /// Parse the stable storage spelling.
433    #[must_use]
434    pub fn parse(name: &str) -> Option<Self> {
435        match name {
436            "requires_execution" => Some(Self::RequiresExecution),
437            "unresolved" => Some(Self::Unresolved),
438            "expansion_unavailable" => Some(Self::ExpansionUnavailable),
439            _ => None,
440        }
441    }
442}
443
444/// What a call resolves to.
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(tag = "resolution", rename_all = "snake_case")]
447pub enum CallTarget {
448    /// Exactly one definition.
449    Static {
450        /// The symbol called.
451        symbol: String,
452    },
453    /// One of several, chosen at run time.
454    ///
455    /// Kept as the candidate set rather than collapsed to "dynamic": two calls
456    /// that dispatch over the same small set of implementations are doing the
457    /// same thing, and that is invisible once the set is thrown away.
458    Dynamic {
459        /// Every definition the compiler admits as possible.
460        candidates: Vec<String>,
461    },
462    /// The compiler could not say.
463    Unresolved,
464}
465
466/// A control-flow graph, as the compiler built it.
467#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
468pub struct ControlFlowGraph {
469    /// Blocks, in the order the compiler numbered them.
470    pub blocks: Vec<BasicBlock>,
471    /// Edges between blocks, by index into `blocks`.
472    pub edges: Vec<Edge>,
473}
474
475/// One straight-line run of a control-flow graph.
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
477pub struct BasicBlock {
478    /// Where the block's code sits.
479    pub anchor: Anchor,
480    /// How many statements or instructions it holds.
481    pub length: u32,
482}
483
484/// A transfer of control between two blocks.
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486pub struct Edge {
487    /// Index of the block control leaves.
488    pub from: u32,
489    /// Index of the block control reaches.
490    pub to: u32,
491    /// Why control moves.
492    pub kind: EdgeKind,
493}
494
495/// Why control moves along an edge.
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(rename_all = "snake_case")]
498pub enum EdgeKind {
499    /// Control falls through or jumps unconditionally.
500    Flow,
501    /// A condition held.
502    Taken,
503    /// A condition did not hold.
504    NotTaken,
505    /// Control left by unwinding or an exception.
506    Unwind,
507    /// Control returned to the caller.
508    Return,
509}
510
511impl EdgeKind {
512    /// Stable lowercase identifier, the same spelling this serializes as.
513    #[must_use]
514    pub const fn name(self) -> &'static str {
515        match self {
516            Self::Flow => "flow",
517            Self::Taken => "taken",
518            Self::NotTaken => "not_taken",
519            Self::Unwind => "unwind",
520            Self::Return => "return",
521        }
522    }
523}
524
525/// Where an instantiated body came from.
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
527pub struct Instantiation {
528    /// Where the instantiated code sits.
529    pub anchor: Anchor,
530    /// The generic or template it was instantiated from.
531    pub definition: String,
532    /// One-based final line of the generic or template definition when the
533    /// compiler reported a complete source range.
534    ///
535    /// This is a source anchor used to contain nested members of a class
536    /// template during artifact correlation. It is not an identity.
537    pub definition_end_line: Option<u32>,
538    /// Optional compiler-produced spelling used only to correlate a source
539    /// specialization with a demangled artifact symbol.
540    ///
541    /// This is comparison evidence, not a stable identity or a replacement
542    /// for [`Self::definition`] or [`Self::instantiation_key`].
543    pub artifact_match_key: Option<String>,
544    /// What groups every instantiation of that definition together.
545    ///
546    /// Two bodies with the same key are the same source text with different
547    /// substitutions — one thing written once, not two things that agree.
548    pub instantiation_key: String,
549    /// The type arguments substituted, as indices into [`CompilerIr::types`].
550    pub arguments: Vec<u32>,
551}
552
553/// What a unit does beyond computing a value.
554#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
555pub struct EffectSummary {
556    /// Whether the helper attempted this analysis at all.
557    ///
558    /// Distinguishes "computed, and there are no effects" from "not computed",
559    /// which are the same empty summary and very different claims.
560    pub computed: bool,
561    /// Symbols whose state the unit writes.
562    pub writes: Vec<String>,
563    /// Closed external interactions observed in the unit.
564    ///
565    /// An empty list is not a proof that the unit is pure; helpers report only
566    /// interactions they can establish from their deliberately narrow
567    /// vocabulary.
568    pub interactions: Vec<String>,
569}
570
571/// How values move through a unit.
572///
573/// The deliberately small initial vocabulary records only direct, resolved
574/// `filter`/`map` receiver chains.  Each endpoint is a helper-local operation
575/// reference in the form `start_byte:end_byte:resolved_api_name`; it is not a
576/// stable identifier and is meaningful only beside this unit's source and
577/// schema version.  This is evidence that one operation's output is the next
578/// operation's receiver, not a general data-flow result.
579#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
580pub struct DataFlowSummary {
581    /// Whether the helper attempted this analysis at all.
582    pub computed: bool,
583    /// Pairs of operation references where the first directly feeds the
584    /// second. The references are intentionally local to this compiler IR.
585    pub flows: Vec<(String, String)>,
586}
587
588/// Which piece of a project an analysis is about.
589#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
590pub struct UnitRef {
591    /// The translation unit or crate, as the build system names it.
592    pub unit: String,
593    /// The file being analyzed within it.
594    ///
595    /// A header analyzed from two translation units is two analyses of one
596    /// file, which is why the unit is part of the identity and the file alone
597    /// is not.
598    pub file: String,
599    /// The build variant this analysis belongs to.
600    pub variant: String,
601}
602
603/// Everything one helper found in one unit.
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605pub struct CompilerIr {
606    /// The schema this was written against.
607    pub schema_version: String,
608    /// What it is about.
609    pub unit: UnitRef,
610    /// The directory the file paths in this analysis are spelled against.
611    ///
612    /// A helper reports paths the way the project spells them, which is
613    /// relative to the root it read the project from — not to whatever
614    /// directory a scan was started in. Saying which root that was is what lets
615    /// a reader turn a file it knows about into the name this analysis filed it
616    /// under. Without it the two spellings can only be compared by hoping they
617    /// agree, and comparing on a shared suffix instead would let one file's
618    /// answers be counted for another's, since two files can end the same way.
619    ///
620    /// `None` when the paths stand on their own, which is what an analysis
621    /// with no project root to speak of reports.
622    ///
623    pub anchored_at: Option<String>,
624    /// Names resolved to definitions.
625    pub symbols: Vec<ResolvedSymbol>,
626    /// Types, referred to by index from everything else.
627    pub types: Vec<ResolvedType>,
628    /// Calls and what they call.
629    pub calls: Vec<CallSite>,
630    /// Compiler-confirmed constructs available to restricted semantic rules.
631    pub semantic_constructs: Vec<SemanticConstruct>,
632    /// Expression types whose anchor may cover an invocation rather than one
633    /// source token.
634    pub expressions: Vec<ResolvedExpression>,
635    /// Macro invocations that were not expanded, and why.
636    pub unexpanded_macros: Vec<UnexpandedMacro>,
637    /// Control flow, when the helper offers it.
638    pub cfg: Option<ControlFlowGraph>,
639    /// Generic and template instantiations.
640    pub instantiations: Vec<Instantiation>,
641    /// What the unit does. Declared, not yet computed.
642    pub effects: EffectSummary,
643    /// How values move. Declared, not yet computed.
644    pub data_flow: DataFlowSummary,
645}
646
647impl CompilerIr {
648    /// An empty result for `unit`, written against this build's schema.
649    #[must_use]
650    pub fn empty(unit: UnitRef) -> Self {
651        Self {
652            schema_version: COMPILER_IR_SCHEMA_VERSION.to_owned(),
653            unit,
654            anchored_at: None,
655            symbols: Vec::new(),
656            types: Vec::new(),
657            calls: Vec::new(),
658            semantic_constructs: Vec::new(),
659            expressions: Vec::new(),
660            unexpanded_macros: Vec::new(),
661            cfg: None,
662            instantiations: Vec::new(),
663            effects: EffectSummary::default(),
664            data_flow: DataFlowSummary::default(),
665        }
666    }
667
668    /// Whether this was written against a schema this build reads.
669    #[must_use]
670    pub fn is_readable(&self) -> bool {
671        self.schema_version == COMPILER_IR_SCHEMA_VERSION
672    }
673
674    /// How this analysis spells `absolute`, so a caller holding a file can
675    /// look up what was said about it.
676    #[must_use]
677    pub fn spelling(&self, absolute: &Path) -> String {
678        spell(self.anchored_at.as_ref().map(Path::new), absolute)
679    }
680}
681
682/// How a path is spelled against `root`.
683///
684/// One function for both sides of the wire. A helper writes its anchors with
685/// it and a reader looks them up with it, so the two spellings agree because
686/// they are the same rule rather than because they were written to match.
687///
688/// A path made relative has its components separated by `/` whatever the
689/// platform separates them with. Where a file sits inside the project is a
690/// value on the wire, in the audit database and in every exported report, so
691/// it has to read as one name rather than one per operating system — and
692/// Windows opens a path spelled that way as readily as its own.
693///
694/// A path outside `root` keeps its own name, exactly as it stands: made
695/// relative it would climb out of the project with `..`, which says less than
696/// the path it started as, and respelled it would stop being what the
697/// filesystem answered — a Windows path reached past the ordinary rules is
698/// spelled one way only.
699#[must_use]
700pub fn spell(root: Option<&Path>, path: &Path) -> String {
701    let Some(relative) = root.and_then(|root| relative_to(root, path)) else {
702        return path.display().to_string();
703    };
704    separated_by(&relative.display().to_string(), std::path::MAIN_SEPARATOR)
705}
706
707/// Where `path` sits under `root`, if it sits under it at all.
708///
709/// The two sides of this question are resolved by two different programs, and
710/// on Windows resolving a path can produce the *verbatim* form — the `\\?\`
711/// spelling that exists so paths the ordinary rules cannot express are still
712/// reachable. One side arriving in that form and the other not is a difference
713/// in how the two were written down, not in which directory they name, so the
714/// prefix is read past on both sides before they are compared.
715fn relative_to<'a>(root: &Path, path: &'a Path) -> Option<&'a Path> {
716    ordinary(path).strip_prefix(ordinary(root)).ok()
717}
718
719/// A Windows verbatim path read as the path it stands for, and anything else
720/// unchanged.
721pub(crate) fn ordinary(path: &Path) -> &Path {
722    path.to_str()
723        .and_then(|text| text.strip_prefix(r"\\?\"))
724        .map_or(path, Path::new)
725}
726
727/// Restate a path that was written with `separator` so its components are
728/// separated by `/`.
729///
730/// Takes the separator rather than reading it, so that the rewrite Windows
731/// needs can be exercised on any machine. A rule only one operating system can
732/// run is a rule only that operating system can find a mistake in.
733fn separated_by(displayed: &str, separator: char) -> String {
734    if separator == '/' {
735        return displayed.to_owned();
736    }
737    displayed.replace(separator, "/")
738}
739
740/// Why a unit has no compiler IR.
741///
742/// A first-class outcome rather than an error: a scan of a real project will
743/// have units nobody can analyze — a crate whose build script would have to run,
744/// a file no compile command mentions — and reporting less about those is the
745/// correct result, not a failed run.
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
747#[serde(rename_all = "snake_case")]
748pub enum Unavailability {
749    /// Analyzing it would mean running code from the project.
750    RequiresExecution,
751    /// Cargo cannot resolve dependencies without network access or a lockfile change.
752    MetadataUnavailable,
753    /// Nothing says how the file is compiled.
754    NoBuildInformation,
755    /// The helper was built for a different toolchain than the project uses.
756    ToolchainMismatch,
757    /// The helper took too long and was given up on.
758    HelperTimedOut,
759    /// The helper stopped before answering.
760    HelperDied,
761    /// The helper answered, but not in a schema this build reads.
762    UnreadableSchema,
763    /// The helper produced an IR response over the protocol frame ceiling.
764    ResponseTooLarge,
765    /// Consecutive helper crashes exhausted the restart budget for this run.
766    RestartBudgetExhausted,
767    /// The helper does not analyze this kind of input.
768    NotSupported,
769}
770
771impl Unavailability {
772    /// Stable lowercase identifier, the same spelling this serializes as.
773    #[must_use]
774    pub const fn name(self) -> &'static str {
775        match self {
776            Self::RequiresExecution => "requires_execution",
777            Self::MetadataUnavailable => "metadata_unavailable",
778            Self::NoBuildInformation => "no_build_information",
779            Self::ToolchainMismatch => "toolchain_mismatch",
780            Self::HelperTimedOut => "helper_timed_out",
781            Self::HelperDied => "helper_died",
782            Self::UnreadableSchema => "unreadable_schema",
783            Self::ResponseTooLarge => "response_too_large",
784            Self::RestartBudgetExhausted => "restart_budget_exhausted",
785            Self::NotSupported => "not_supported",
786        }
787    }
788
789    /// Whether trying the same unit again could plausibly go differently.
790    ///
791    /// A helper that died might have died on this input in particular, and
792    /// retrying costs one more crash to find out. A helper that says the input
793    /// needs execution will say so every time, and retrying is only slower.
794    #[must_use]
795    pub const fn worth_retrying(self) -> bool {
796        matches!(self, Self::HelperTimedOut | Self::HelperDied)
797    }
798}
799
800#[cfg(test)]
801#[allow(clippy::expect_used, clippy::unwrap_used)]
802mod tests {
803    use super::*;
804
805    fn range(file: &str) -> SourceRange {
806        SourceRange {
807            file: file.into(),
808            start_byte: 10,
809            end_byte: 40,
810            start_line: 2,
811        }
812    }
813
814    #[test]
815    fn code_written_where_it_reads_has_no_second_place() {
816        let anchor = Anchor::written_here(range("src/lib.rs"));
817        assert!(!anchor.is_expanded());
818    }
819
820    #[test]
821    fn expanded_code_keeps_both_places() {
822        let anchor = Anchor {
823            expansion: range("src/uses.rs"),
824            definition: Some(range("src/macros.rs")),
825        };
826        assert!(anchor.is_expanded());
827        // The expansion site is what a fragment can be cut from, so it is the
828        // one a node anchors to.
829        assert_eq!(anchor.expansion.file, "src/uses.rs");
830    }
831
832    #[test]
833    fn an_empty_result_still_says_which_schema_it_is() {
834        let ir = CompilerIr::empty(UnitRef {
835            unit: "crate".into(),
836            file: "src/lib.rs".into(),
837            variant: "v1".into(),
838        });
839        assert!(ir.is_readable());
840        assert_eq!(ir.schema_version, COMPILER_IR_SCHEMA_VERSION);
841    }
842
843    #[test]
844    fn a_result_from_another_schema_is_not_read_as_if_it_were_current() {
845        let mut ir = CompilerIr::empty(UnitRef {
846            unit: "crate".into(),
847            file: "src/lib.rs".into(),
848            variant: "v1".into(),
849        });
850        ir.schema_version = "compiler-ir-unsupported".into();
851        assert!(!ir.is_readable());
852    }
853
854    #[test]
855    fn an_empty_summary_says_whether_anyone_looked() {
856        let summary = EffectSummary::default();
857        assert!(!summary.computed);
858        assert!(summary.writes.is_empty());
859        // "Nothing was found" and "nothing was attempted" are the same empty
860        // list and must not read the same.
861        let looked = EffectSummary {
862            computed: true,
863            ..EffectSummary::default()
864        };
865        assert_ne!(summary, looked);
866    }
867
868    #[test]
869    fn only_a_helper_that_broke_is_worth_asking_twice() {
870        assert!(Unavailability::HelperDied.worth_retrying());
871        assert!(Unavailability::HelperTimedOut.worth_retrying());
872        for settled in [
873            Unavailability::RequiresExecution,
874            Unavailability::MetadataUnavailable,
875            Unavailability::NoBuildInformation,
876            Unavailability::ToolchainMismatch,
877            Unavailability::UnreadableSchema,
878            Unavailability::NotSupported,
879        ] {
880            assert!(!settled.worth_retrying(), "{settled:?}");
881        }
882    }
883
884    #[test]
885    fn a_dynamic_call_keeps_the_candidates_rather_than_the_word_dynamic() {
886        let target = CallTarget::Dynamic {
887            candidates: vec!["a::run".into(), "b::run".into()],
888        };
889        let text = serde_json::to_string(&target).unwrap();
890        let back: CallTarget = serde_json::from_str(&text).unwrap();
891        assert_eq!(back, target);
892        assert!(text.contains("a::run") && text.contains("b::run"));
893    }
894
895    #[test]
896    fn an_unknown_symbol_kind_is_rejected() {
897        assert!(serde_json::from_str::<SymbolKind>("\"something_new\"").is_err());
898    }
899
900    /// Built the way the platform builds one, so that on Windows the parts are
901    /// joined by the separator this rule has to answer for.
902    fn native(parts: &[&str]) -> std::path::PathBuf {
903        parts.iter().collect()
904    }
905
906    /// A file inside the project is named by where it sits in the project,
907    /// with the separator every reader of this value expects — and it is the
908    /// same string wherever the file was read, because the value travels on
909    /// the wire, into the audit database and out into every report.
910    #[test]
911    fn a_file_under_the_root_is_named_relative_to_it() {
912        let root = native(&["home", "project"]);
913        let nested = native(&["home", "project", "src", "inner", "mod.rs"]);
914        assert_eq!(spell(Some(&root), &nested), "src/inner/mod.rs");
915    }
916
917    /// The rewrite Windows depends on, run here whatever this machine is.
918    #[test]
919    fn a_path_written_with_backslashes_is_named_with_slashes() {
920        assert_eq!(separated_by(r"src\inner\mod.rs", '\\'), "src/inner/mod.rs");
921        assert_eq!(separated_by(r"C:\home\project", '\\'), "C:/home/project");
922        // Already spelled that way, from a caller that wrote it by hand.
923        assert_eq!(separated_by("src/lib.rs", '\\'), "src/lib.rs");
924        // On a platform whose separator is already the one wanted, a name
925        // containing a backslash is a name, not a separator.
926        assert_eq!(separated_by(r"src/odd\name.rs", '/'), r"src/odd\name.rs");
927    }
928
929    /// The same path written the way Windows writes one it had to reach past
930    /// the ordinary rules for. Built from a native path so that the rule can be
931    /// exercised on any machine: what is under test is reading past the prefix,
932    /// and a prefix nothing here can produce is still a prefix that arrives.
933    fn verbatim(path: &Path) -> std::path::PathBuf {
934        std::path::PathBuf::from(format!(r"\\?\{}", path.display()))
935    }
936
937    /// Two programs resolved these paths, and only one of them need have come
938    /// back in the verbatim form for the file to look like it sits somewhere
939    /// else entirely.
940    #[test]
941    fn a_root_and_a_file_written_in_different_forms_still_meet() {
942        let root = native(&["home", "project"]);
943        let file = native(&["home", "project", "src", "lib.rs"]);
944        let expected = native(&["src", "lib.rs"]);
945        for (root, file) in [
946            (root.clone(), verbatim(&file)),
947            (verbatim(&root), file.clone()),
948            (verbatim(&root), verbatim(&file)),
949            (root.clone(), file.clone()),
950        ] {
951            assert_eq!(
952                relative_to(&root, &file),
953                Some(expected.as_path()),
954                "{} under {}",
955                file.display(),
956                root.display()
957            );
958        }
959    }
960
961    /// Reading past the prefix is for comparing, not for deciding a file is
962    /// somewhere it is not.
963    #[test]
964    fn reading_past_the_prefix_does_not_put_a_file_under_the_wrong_root() {
965        let root = verbatim(&native(&["home", "project"]));
966        for elsewhere in [
967            native(&["home", "other", "x.rs"]),
968            verbatim(&native(&["home", "other", "x.rs"])),
969        ] {
970            assert_eq!(
971                relative_to(&root, &elsewhere),
972                None,
973                "{}",
974                elsewhere.display()
975            );
976        }
977    }
978
979    /// Outside the root there is nothing to be relative to, so the path keeps
980    /// its own name rather than climbing out of the project to reach it.
981    #[test]
982    fn a_file_outside_the_root_keeps_its_own_name() {
983        let root = native(&["home", "project"]);
984        let elsewhere = native(&["home", "elsewhere", "vendor.rs"]);
985        assert_eq!(
986            spell(Some(&root), &elsewhere),
987            elsewhere.display().to_string()
988        );
989        assert_eq!(
990            spell(None, &elsewhere),
991            spell(Some(&root), &elsewhere),
992            "an unrooted analysis names the file the same way"
993        );
994    }
995
996    /// A path the ordinary Windows rules cannot express is reachable by one
997    /// spelling only. Respelling it, as a path inside the project is respelled,
998    /// would produce a name that reaches nothing at all.
999    #[test]
1000    fn a_file_named_the_only_way_it_can_be_keeps_that_name() {
1001        let root = native(&["home", "project"]);
1002        let elsewhere = verbatim(&native(&["home", "elsewhere", "vendor.rs"]));
1003        assert_eq!(
1004            spell(Some(&root), &elsewhere),
1005            elsewhere.display().to_string()
1006        );
1007    }
1008
1009    /// What a caller holding an absolute path looks up, written by the same
1010    /// rule the helper wrote the anchor with.
1011    #[test]
1012    fn a_reader_looks_a_file_up_the_way_the_helper_wrote_it() {
1013        let root = native(&["home", "project"]);
1014        let mut ir = CompilerIr::empty(UnitRef {
1015            unit: "crate".into(),
1016            file: "src/lib.rs".into(),
1017            variant: "v1".into(),
1018        });
1019        ir.anchored_at = Some(root.display().to_string());
1020        assert_eq!(
1021            ir.spelling(&native(&["home", "project", "src", "lib.rs"])),
1022            "src/lib.rs"
1023        );
1024    }
1025}