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}