use std::path::Path;
use serde::{Deserialize, Serialize};
pub const COMPILER_IR_SCHEMA_VERSION: &str = "compiler-ir-v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceRange {
pub file: String,
pub start_byte: u64,
pub end_byte: u64,
pub start_line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Anchor {
pub expansion: SourceRange,
pub definition: Option<SourceRange>,
}
impl Anchor {
#[must_use]
pub const fn written_here(range: SourceRange) -> Self {
Self {
expansion: range,
definition: None,
}
}
#[must_use]
pub const fn is_expanded(&self) -> bool {
self.definition.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TypeCategory {
Integer,
Float,
Boolean,
Character,
Text,
Handle,
Sequence,
Mapping,
Tuple,
Record,
Enumeration,
Interface,
Callable,
Parameter,
Nothing,
Unresolved,
}
impl TypeCategory {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Integer => "integer",
Self::Float => "float",
Self::Boolean => "boolean",
Self::Character => "character",
Self::Text => "text",
Self::Handle => "handle",
Self::Sequence => "sequence",
Self::Mapping => "mapping",
Self::Tuple => "tuple",
Self::Record => "record",
Self::Enumeration => "enumeration",
Self::Interface => "interface",
Self::Callable => "callable",
Self::Parameter => "parameter",
Self::Nothing => "nothing",
Self::Unresolved => "unresolved",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedSymbol {
pub id: String,
pub name: String,
pub kind: SymbolKind,
pub anchor: Anchor,
pub type_index: Option<u32>,
pub external: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Type,
Field,
Variant,
Binding,
Constant,
Namespace,
Other,
}
impl SymbolKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Function => "function",
Self::Type => "type",
Self::Field => "field",
Self::Variant => "variant",
Self::Binding => "binding",
Self::Constant => "constant",
Self::Namespace => "namespace",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedType {
pub display: String,
pub category: TypeCategory,
pub arguments: Vec<u32>,
pub definition: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallSite {
pub anchor: Anchor,
pub target: CallTarget,
pub api_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticConstruct {
pub anchor: Anchor,
pub kind: SemanticConstructKind,
pub fallible_kind: Option<FallibleKind>,
pub direct_propagation: Option<DirectPropagation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_kind: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FallibleKind {
Option,
Result,
}
impl FallibleKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Option => "option",
Self::Result => "result",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"option" => Some(Self::Option),
"result" => Some(Self::Result),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DirectPropagation {
ResultAdapter,
OptionAdapter,
}
impl DirectPropagation {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::ResultAdapter => "result_adapter",
Self::OptionAdapter => "option_adapter",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"result_adapter" => Some(Self::ResultAdapter),
"option_adapter" => Some(Self::OptionAdapter),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticConstructKind {
Source,
Collect,
Reduce,
PropagateError,
Validate,
AcquireResource,
ReleaseResource,
}
impl SemanticConstructKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Source => "source",
Self::Collect => "collect",
Self::Reduce => "reduce",
Self::PropagateError => "propagate_error",
Self::Validate => "validate",
Self::AcquireResource => "acquire_resource",
Self::ReleaseResource => "release_resource",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"source" => Some(Self::Source),
"collect" => Some(Self::Collect),
"reduce" => Some(Self::Reduce),
"propagate_error" => Some(Self::PropagateError),
"validate" => Some(Self::Validate),
"acquire_resource" => Some(Self::AcquireResource),
"release_resource" => Some(Self::ReleaseResource),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedExpression {
pub anchor: Anchor,
pub type_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnexpandedMacro {
pub invocation: SourceRange,
pub reason: UnexpandedMacroReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UnexpandedMacroReason {
RequiresExecution,
Unresolved,
ExpansionUnavailable,
}
impl UnexpandedMacroReason {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::RequiresExecution => "requires_execution",
Self::Unresolved => "unresolved",
Self::ExpansionUnavailable => "expansion_unavailable",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"requires_execution" => Some(Self::RequiresExecution),
"unresolved" => Some(Self::Unresolved),
"expansion_unavailable" => Some(Self::ExpansionUnavailable),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "resolution", rename_all = "snake_case")]
pub enum CallTarget {
Static {
symbol: String,
},
Dynamic {
candidates: Vec<String>,
},
Unresolved,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlFlowGraph {
pub blocks: Vec<BasicBlock>,
pub edges: Vec<Edge>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BasicBlock {
pub anchor: Anchor,
pub length: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edge {
pub from: u32,
pub to: u32,
pub kind: EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeKind {
Flow,
Taken,
NotTaken,
Unwind,
Return,
}
impl EdgeKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Flow => "flow",
Self::Taken => "taken",
Self::NotTaken => "not_taken",
Self::Unwind => "unwind",
Self::Return => "return",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Instantiation {
pub anchor: Anchor,
pub definition: String,
pub definition_end_line: Option<u32>,
pub artifact_match_key: Option<String>,
pub instantiation_key: String,
pub arguments: Vec<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EffectSummary {
pub computed: bool,
pub writes: Vec<String>,
pub interactions: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataFlowSummary {
pub computed: bool,
pub flows: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct UnitRef {
pub unit: String,
pub file: String,
pub variant: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerIr {
pub schema_version: String,
pub unit: UnitRef,
pub anchored_at: Option<String>,
pub symbols: Vec<ResolvedSymbol>,
pub types: Vec<ResolvedType>,
pub calls: Vec<CallSite>,
pub semantic_constructs: Vec<SemanticConstruct>,
pub expressions: Vec<ResolvedExpression>,
pub unexpanded_macros: Vec<UnexpandedMacro>,
pub cfg: Option<ControlFlowGraph>,
pub instantiations: Vec<Instantiation>,
pub effects: EffectSummary,
pub data_flow: DataFlowSummary,
}
impl CompilerIr {
#[must_use]
pub fn empty(unit: UnitRef) -> Self {
Self {
schema_version: COMPILER_IR_SCHEMA_VERSION.to_owned(),
unit,
anchored_at: None,
symbols: Vec::new(),
types: Vec::new(),
calls: Vec::new(),
semantic_constructs: Vec::new(),
expressions: Vec::new(),
unexpanded_macros: Vec::new(),
cfg: None,
instantiations: Vec::new(),
effects: EffectSummary::default(),
data_flow: DataFlowSummary::default(),
}
}
#[must_use]
pub fn is_readable(&self) -> bool {
self.schema_version == COMPILER_IR_SCHEMA_VERSION
}
#[must_use]
pub fn spelling(&self, absolute: &Path) -> String {
spell(self.anchored_at.as_ref().map(Path::new), absolute)
}
}
#[must_use]
pub fn spell(root: Option<&Path>, path: &Path) -> String {
let Some(relative) = root.and_then(|root| relative_to(root, path)) else {
return path.display().to_string();
};
separated_by(&relative.display().to_string(), std::path::MAIN_SEPARATOR)
}
fn relative_to<'a>(root: &Path, path: &'a Path) -> Option<&'a Path> {
ordinary(path).strip_prefix(ordinary(root)).ok()
}
pub(crate) fn ordinary(path: &Path) -> &Path {
path.to_str()
.and_then(|text| text.strip_prefix(r"\\?\"))
.map_or(path, Path::new)
}
fn separated_by(displayed: &str, separator: char) -> String {
if separator == '/' {
return displayed.to_owned();
}
displayed.replace(separator, "/")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Unavailability {
RequiresExecution,
MetadataUnavailable,
NoBuildInformation,
ToolchainMismatch,
HelperTimedOut,
HelperDied,
UnreadableSchema,
ResponseTooLarge,
RestartBudgetExhausted,
NotSupported,
}
impl Unavailability {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::RequiresExecution => "requires_execution",
Self::MetadataUnavailable => "metadata_unavailable",
Self::NoBuildInformation => "no_build_information",
Self::ToolchainMismatch => "toolchain_mismatch",
Self::HelperTimedOut => "helper_timed_out",
Self::HelperDied => "helper_died",
Self::UnreadableSchema => "unreadable_schema",
Self::ResponseTooLarge => "response_too_large",
Self::RestartBudgetExhausted => "restart_budget_exhausted",
Self::NotSupported => "not_supported",
}
}
#[must_use]
pub const fn worth_retrying(self) -> bool {
matches!(self, Self::HelperTimedOut | Self::HelperDied)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
fn range(file: &str) -> SourceRange {
SourceRange {
file: file.into(),
start_byte: 10,
end_byte: 40,
start_line: 2,
}
}
#[test]
fn code_written_where_it_reads_has_no_second_place() {
let anchor = Anchor::written_here(range("src/lib.rs"));
assert!(!anchor.is_expanded());
}
#[test]
fn expanded_code_keeps_both_places() {
let anchor = Anchor {
expansion: range("src/uses.rs"),
definition: Some(range("src/macros.rs")),
};
assert!(anchor.is_expanded());
assert_eq!(anchor.expansion.file, "src/uses.rs");
}
#[test]
fn an_empty_result_still_says_which_schema_it_is() {
let ir = CompilerIr::empty(UnitRef {
unit: "crate".into(),
file: "src/lib.rs".into(),
variant: "v1".into(),
});
assert!(ir.is_readable());
assert_eq!(ir.schema_version, COMPILER_IR_SCHEMA_VERSION);
}
#[test]
fn a_result_from_another_schema_is_not_read_as_if_it_were_current() {
let mut ir = CompilerIr::empty(UnitRef {
unit: "crate".into(),
file: "src/lib.rs".into(),
variant: "v1".into(),
});
ir.schema_version = "compiler-ir-unsupported".into();
assert!(!ir.is_readable());
}
#[test]
fn an_empty_summary_says_whether_anyone_looked() {
let summary = EffectSummary::default();
assert!(!summary.computed);
assert!(summary.writes.is_empty());
let looked = EffectSummary {
computed: true,
..EffectSummary::default()
};
assert_ne!(summary, looked);
}
#[test]
fn only_a_helper_that_broke_is_worth_asking_twice() {
assert!(Unavailability::HelperDied.worth_retrying());
assert!(Unavailability::HelperTimedOut.worth_retrying());
for settled in [
Unavailability::RequiresExecution,
Unavailability::MetadataUnavailable,
Unavailability::NoBuildInformation,
Unavailability::ToolchainMismatch,
Unavailability::UnreadableSchema,
Unavailability::NotSupported,
] {
assert!(!settled.worth_retrying(), "{settled:?}");
}
}
#[test]
fn a_dynamic_call_keeps_the_candidates_rather_than_the_word_dynamic() {
let target = CallTarget::Dynamic {
candidates: vec!["a::run".into(), "b::run".into()],
};
let text = serde_json::to_string(&target).unwrap();
let back: CallTarget = serde_json::from_str(&text).unwrap();
assert_eq!(back, target);
assert!(text.contains("a::run") && text.contains("b::run"));
}
#[test]
fn an_unknown_symbol_kind_is_rejected() {
assert!(serde_json::from_str::<SymbolKind>("\"something_new\"").is_err());
}
fn native(parts: &[&str]) -> std::path::PathBuf {
parts.iter().collect()
}
#[test]
fn a_file_under_the_root_is_named_relative_to_it() {
let root = native(&["home", "project"]);
let nested = native(&["home", "project", "src", "inner", "mod.rs"]);
assert_eq!(spell(Some(&root), &nested), "src/inner/mod.rs");
}
#[test]
fn a_path_written_with_backslashes_is_named_with_slashes() {
assert_eq!(separated_by(r"src\inner\mod.rs", '\\'), "src/inner/mod.rs");
assert_eq!(separated_by(r"C:\home\project", '\\'), "C:/home/project");
assert_eq!(separated_by("src/lib.rs", '\\'), "src/lib.rs");
assert_eq!(separated_by(r"src/odd\name.rs", '/'), r"src/odd\name.rs");
}
fn verbatim(path: &Path) -> std::path::PathBuf {
std::path::PathBuf::from(format!(r"\\?\{}", path.display()))
}
#[test]
fn a_root_and_a_file_written_in_different_forms_still_meet() {
let root = native(&["home", "project"]);
let file = native(&["home", "project", "src", "lib.rs"]);
let expected = native(&["src", "lib.rs"]);
for (root, file) in [
(root.clone(), verbatim(&file)),
(verbatim(&root), file.clone()),
(verbatim(&root), verbatim(&file)),
(root.clone(), file.clone()),
] {
assert_eq!(
relative_to(&root, &file),
Some(expected.as_path()),
"{} under {}",
file.display(),
root.display()
);
}
}
#[test]
fn reading_past_the_prefix_does_not_put_a_file_under_the_wrong_root() {
let root = verbatim(&native(&["home", "project"]));
for elsewhere in [
native(&["home", "other", "x.rs"]),
verbatim(&native(&["home", "other", "x.rs"])),
] {
assert_eq!(
relative_to(&root, &elsewhere),
None,
"{}",
elsewhere.display()
);
}
}
#[test]
fn a_file_outside_the_root_keeps_its_own_name() {
let root = native(&["home", "project"]);
let elsewhere = native(&["home", "elsewhere", "vendor.rs"]);
assert_eq!(
spell(Some(&root), &elsewhere),
elsewhere.display().to_string()
);
assert_eq!(
spell(None, &elsewhere),
spell(Some(&root), &elsewhere),
"an unrooted analysis names the file the same way"
);
}
#[test]
fn a_file_named_the_only_way_it_can_be_keeps_that_name() {
let root = native(&["home", "project"]);
let elsewhere = verbatim(&native(&["home", "elsewhere", "vendor.rs"]));
assert_eq!(
spell(Some(&root), &elsewhere),
elsewhere.display().to_string()
);
}
#[test]
fn a_reader_looks_a_file_up_the_way_the_helper_wrote_it() {
let root = native(&["home", "project"]);
let mut ir = CompilerIr::empty(UnitRef {
unit: "crate".into(),
file: "src/lib.rs".into(),
variant: "v1".into(),
});
ir.anchored_at = Some(root.display().to_string());
assert_eq!(
ir.spelling(&native(&["home", "project", "src", "lib.rs"])),
"src/lib.rs"
);
}
}