use std::{collections::BTreeSet, path::PathBuf};
use serde::{Deserialize, Serialize};
use rllvm_core::{
catalog::{CatalogOrigin, CatalogScope, DigestOrigin},
error::Error,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FunctionId {
pub module_id: String,
pub symbol: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CallSiteId {
pub function: FunctionId,
pub block_index: u32,
pub instruction_index: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceStatus {
Current,
Modified,
Missing,
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardKind {
Compare,
Clamp,
Saturate,
Mask,
Modulo,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLocation {
pub file: PathBuf,
pub directory: Option<PathBuf>,
pub line: u32,
pub column: u32,
pub source_status: SourceStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_basis: Option<DigestOrigin>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inlined_at: Vec<SourceLocation>,
}
impl SourceLocation {
pub(crate) fn resolved_path(&self) -> PathBuf {
match &self.directory {
Some(directory) => directory.join(&self.file),
None => self.file.clone(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Linkage {
External,
Internal,
Odr,
Weak,
AvailableExternally,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Language {
Rust,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LanguageBasis {
DebugInfo,
Producer,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLanguage {
pub name: Language,
pub basis: LanguageBasis,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FunctionFact {
pub id: FunctionId,
pub is_definition: bool,
pub linkage: Linkage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<SourceLanguage>,
pub signature: String,
pub location: Option<SourceLocation>,
pub mapped_lines: BTreeSet<(PathBuf, u32)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias_of: Option<FunctionId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FieldRef {
pub record: String,
pub offset: u64,
}
impl std::fmt::Display for FieldRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.record, self.offset)
}
}
impl std::str::FromStr for FieldRef {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let invalid = || {
Error::InvalidArguments(format!(
"field `{text}` is not `record@offset`, such as `ops@8`"
))
};
let (record, offset) = text.rsplit_once('@').ok_or_else(invalid)?;
if record.is_empty() {
return Err(invalid());
}
Ok(Self {
record: record.to_string(),
offset: offset.parse().map_err(|_| invalid())?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldBasis {
StructGep,
Tbaa,
Initializer,
DebugInfo,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Slot {
Global {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
name: String,
},
Param { function: FunctionId, index: u32 },
Returned { function: FunctionId },
}
impl std::fmt::Display for Slot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Slot::Global { name, .. } => write!(f, "global {name}"),
Slot::Param { function, index } => write!(f, "param {index} of {}", function.symbol),
Slot::Returned { function } => write!(f, "*{}()", function.symbol),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReturnedAddress {
pub function: FunctionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub global: Option<Slot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<FieldEvidence>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlotFlow {
pub from: Slot,
pub into: Slot,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldFlow {
pub from: Slot,
pub into: FieldEvidence,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldEvidence {
pub field: FieldRef,
pub basis: FieldBasis,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CallTarget {
Direct {
callee: FunctionId,
},
Indirect {
signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
llvm_target_bound: Option<Vec<FunctionId>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
via_field: Option<FieldEvidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
via_slot: Option<Slot>,
},
Intrinsic {
name: String,
},
InlineAsm,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CallSiteFact {
pub id: CallSiteId,
pub location: Option<SourceLocation>,
pub target: CallTarget,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UseKind {
StoredToMemory,
PassedAsArgument,
GlobalInitializer,
ReturnedValue,
Other,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UseFact {
pub used: FunctionId,
pub in_function: Option<FunctionId>,
pub in_global: Option<String>,
pub location: Option<SourceLocation>,
pub kind: UseKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<FieldEvidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub into_slot: Option<Slot>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModuleAnalysis {
Verified,
Analyzed,
Changed,
Missing,
Failed,
Unsupported,
NotBuilt,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModuleReport {
pub id: String,
pub status: ModuleAnalysis,
pub ir_stage: Option<String>,
pub debug_info: Option<bool>,
pub compiler: Option<String>,
pub configuration_id: Option<String>,
pub content_sha256: Option<String>,
pub target_triple: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub producers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProgramFacts {
pub functions: Vec<FunctionFact>,
pub call_sites: Vec<CallSiteFact>,
pub uses: Vec<UseFact>,
pub slot_flows: Vec<SlotFlow>,
pub field_flows: Vec<FieldFlow>,
pub returned_addresses: Vec<ReturnedAddress>,
pub scope: CatalogScope,
pub origin: CatalogOrigin,
pub modules: Vec<ModuleReport>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_symbol_is_not_a_function_identity() {
let left = FunctionId {
module_id: "a".into(),
symbol: "f".into(),
};
let right = FunctionId {
module_id: "b".into(),
symbol: "f".into(),
};
assert_ne!(left, right, "same symbol in two modules must not collide");
}
#[test]
fn a_field_reads_back_from_its_display() {
let field = FieldRef {
record: "n::S".into(),
offset: 0,
};
assert_eq!(field.to_string(), "n::S@0");
assert_eq!(field.to_string().parse::<FieldRef>().unwrap(), field);
assert_eq!(
"a@b@8".parse::<FieldRef>().unwrap(),
FieldRef {
record: "a@b".into(),
offset: 8
},
"the offset follows the last `@`"
);
for invalid in ["ops@", "@8", "ops", "ops@-8", "ops@x"] {
assert!(
matches!(invalid.parse::<FieldRef>(), Err(Error::InvalidArguments(_))),
"{invalid}"
);
}
}
}