use std::collections::BTreeMap;
use std::marker::PhantomData;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::compat::arg_shape::ArgShape;
use crate::model::names::{
ApplicationName, Arity, CommitSha, DependencyName, DependencyVersion, FieldName, FunctionName,
MacroName, ModuleName, ProjectName, RecordName, TagName, TypeName,
};
use crate::snapshot::sort::canonicalize;
pub mod state {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Unsorted;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Canonical;
}
pub const FORMAT_VERSION: u32 = 4;
pub const SUPPORTED_FORMAT_VERSIONS: &[u32] = &[1, 2, 3, 4];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotHeader {
pub project: ProjectName,
pub tag: TagName,
pub branch: Option<String>,
pub commit: CommitSha,
pub scanned_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub apps_scanned: Vec<ApplicationName>,
pub generated_by: String,
#[serde(with = "time::serde::rfc3339")]
pub generated_at: OffsetDateTime,
#[serde(default)]
pub extractor_version: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dep_pins: Vec<VendoredDep>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Hidden,
TestOnly,
}
impl Visibility {
pub fn keyword(self) -> &'static str {
match self {
Self::Public => "public",
Self::Hidden => "hidden",
Self::TestOnly => "test_only",
}
}
pub fn from_keyword(s: &str) -> Option<Self> {
match s {
"public" => Some(Self::Public),
"hidden" => Some(Self::Hidden),
"test_only" => Some(Self::TestOnly),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Module {
pub name: ModuleName,
pub visibility: Visibility,
pub behaviours: Vec<ModuleName>,
pub exports: Vec<FunArity>,
pub export_types: Vec<TypeArity>,
pub callbacks: Vec<CallbackSig>,
pub optional_callbacks: Vec<FunArity>,
pub specs: Vec<SpecSig>,
pub types: Vec<TypeDecl>,
pub opaques: Vec<TypeArity>,
pub deprecations: Vec<Deprecation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub clause_heads: BTreeMap<FunArity, Vec<Vec<ArgShape>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app: Option<ApplicationName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub records: Vec<RecordDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub test_only_exports: Vec<TestOnlyExport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ifdef_macros: Vec<IfdefMacro>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variant_c_blocks: Vec<VariantCBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub versioned_machine_version: Option<VersionedMachineVersion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub wire_constants: Vec<WireConstantBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TestOnlyExport {
pub function: FunctionName,
pub arity: Arity,
pub export_line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_line: Option<usize>,
pub variant: TestExportVariant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestExportVariant {
A,
B,
}
impl TestExportVariant {
pub fn as_label(self) -> &'static str {
match self {
Self::A => "a",
Self::B => "b",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"a" => Some(Self::A),
"b" => Some(Self::B),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IfdefMacro {
pub name: String,
pub line: usize,
pub guard_kind: IfdefGuardKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IfdefGuardKind {
Test,
NotTest,
Other,
}
impl IfdefGuardKind {
pub fn as_label(self) -> &'static str {
match self {
Self::Test => "test",
Self::NotTest => "not_test",
Self::Other => "other",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"test" => Some(Self::Test),
"not_test" => Some(Self::NotTest),
"other" => Some(Self::Other),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VariantCBlock {
pub guard: String,
pub start_line: usize,
pub end_line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub else_line: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VersionedMachineVersion {
pub function: FunctionName,
pub arity: Arity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<u64>,
pub provenance: Provenance,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Provenance {
Literal,
MacroBody {
macro_name: MacroName,
#[serde(default, skip_serializing_if = "Option::is_none")]
defined_in: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WireConstantBinding {
pub macro_name: MacroName,
pub value: WireValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub defined_in: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
pub enum WireValue {
U64(u64),
Bytes(Vec<u8>),
Opaque(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VendoredDep {
pub name: DependencyName,
pub version: DependencyVersion,
pub source: VendoredDepSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VendoredDepSource {
Hex,
Git,
GitRmq,
}
impl VendoredDepSource {
pub fn as_label(self) -> &'static str {
match self {
Self::Hex => "hex",
Self::Git => "git",
Self::GitRmq => "git_rmq",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"hex" => Some(Self::Hex),
"git" => Some(Self::Git),
"git_rmq" => Some(Self::GitRmq),
_ => None,
}
}
}
impl Module {
pub fn new(name: ModuleName) -> Self {
Self {
name,
visibility: Visibility::Public,
behaviours: Vec::new(),
exports: Vec::new(),
export_types: Vec::new(),
callbacks: Vec::new(),
optional_callbacks: Vec::new(),
specs: Vec::new(),
types: Vec::new(),
opaques: Vec::new(),
deprecations: Vec::new(),
clause_heads: BTreeMap::new(),
path: None,
app: None,
records: Vec::new(),
test_only_exports: Vec::new(),
ifdef_macros: Vec::new(),
variant_c_blocks: Vec::new(),
versioned_machine_version: None,
wire_constants: Vec::new(),
}
}
pub fn missing_clause_bodies(&self) -> Vec<FunArity> {
if self.clause_heads.is_empty() {
return Vec::new();
}
self.exports
.iter()
.filter(|fa| !self.clause_heads.contains_key(fa))
.cloned()
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HrlFile {
pub path: String,
pub types: Vec<TypeDecl>,
pub opaques: Vec<TypeArity>,
pub records: Vec<RecordDecl>,
}
impl HrlFile {
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
types: Vec::new(),
opaques: Vec::new(),
records: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FunArity {
pub name: FunctionName,
pub arity: Arity,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TypeArity {
pub name: TypeName,
pub arity: Arity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallbackSig {
pub name: FunctionName,
pub arity: Arity,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpecSig {
pub name: FunctionName,
pub arity: Arity,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeDecl {
pub name: TypeName,
pub arity: Arity,
pub rhs: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordDecl {
pub name: RecordName,
pub fields: Vec<RecordField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordField {
pub name: FieldName,
pub type_repr: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deprecation {
pub function: Option<FunctionName>,
pub arity_match: ArityMatch,
pub since: Option<TagName>,
pub replacement: Option<DeprecationReplacement>,
pub reason: Option<String>,
pub module_wide: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum ArityMatch {
Exact { arity: Arity },
Any,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeprecationReplacement {
pub function: FunctionName,
pub arity: Arity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct Snapshot<S = state::Unsorted> {
pub header: SnapshotHeader,
pub modules: Vec<Module>,
pub headers: Vec<HrlFile>,
#[serde(skip)]
_state: PhantomData<S>,
}
impl Snapshot<state::Unsorted> {
pub fn from_extracted(
header: SnapshotHeader,
modules: Vec<Module>,
headers: Vec<HrlFile>,
) -> Self {
Self {
header,
modules,
headers,
_state: PhantomData,
}
}
#[must_use]
pub fn into_canonical(mut self) -> Snapshot<state::Canonical> {
canonicalize(&mut self.modules, &mut self.headers);
self.header.apps_scanned.sort();
self.header.apps_scanned.dedup();
self.header.dep_pins.sort_by(|a, b| a.name.cmp(&b.name));
self.header.dep_pins.dedup_by(|a, b| a.name == b.name);
Snapshot {
header: self.header,
modules: self.modules,
headers: self.headers,
_state: PhantomData,
}
}
}
impl<S> Snapshot<S> {
pub fn header(&self) -> &SnapshotHeader {
&self.header
}
pub fn modules(&self) -> &[Module] {
&self.modules
}
pub fn headers(&self) -> &[HrlFile] {
&self.headers
}
pub fn records(&self) -> impl Iterator<Item = &RecordDecl> {
self.headers
.iter()
.flat_map(|h| h.records.iter())
.chain(self.modules.iter().flat_map(|m| m.records.iter()))
}
}
impl Snapshot<state::Canonical> {
pub fn lookup_export(
&self,
module: &ModuleName,
function: &FunctionName,
arity: Arity,
) -> bool {
let Some(m) = self.module_named(module) else {
return false;
};
let target = FunArity {
name: function.clone(),
arity,
};
m.exports.binary_search(&target).is_ok()
}
pub fn module_named(&self, name: &ModuleName) -> Option<&Module> {
self.modules
.binary_search_by(|m| m.name.cmp(name))
.ok()
.map(|idx| &self.modules[idx])
}
pub fn implementers_of(&self, behaviour: &ModuleName) -> Vec<&Module> {
self.modules
.iter()
.filter(|m| m.behaviours.contains(behaviour))
.collect()
}
pub(crate) fn from_canonical_parts(
header: SnapshotHeader,
modules: Vec<Module>,
headers: Vec<HrlFile>,
) -> Self {
Self {
header,
modules,
headers,
_state: PhantomData,
}
}
}
impl Default for Snapshot<state::Unsorted> {
fn default() -> Self {
Self {
header: SnapshotHeader {
project: ProjectName::new("placeholder").expect("valid"),
tag: TagName::new("v0.0.0").expect("valid"),
branch: None,
commit: CommitSha::new("0".repeat(40)).expect("valid"),
scanned_paths: Vec::new(),
apps_scanned: Vec::new(),
generated_by: format!("backhopper {}", env!("CARGO_PKG_VERSION")),
generated_at: OffsetDateTime::UNIX_EPOCH,
extractor_version: String::new(),
dep_pins: Vec::new(),
},
modules: Vec::new(),
headers: Vec::new(),
_state: PhantomData,
}
}
}