1use std::collections::BTreeMap;
13use std::marker::PhantomData;
14
15use serde::{Deserialize, Serialize};
16use time::OffsetDateTime;
17
18use crate::compat::arg_shape::ArgShape;
19use crate::model::names::{
20 ApplicationName, Arity, CommitSha, DependencyName, DependencyVersion, FieldName, FunctionName,
21 MacroName, ModuleName, ProjectName, RecordName, TagName, TypeName,
22};
23use crate::snapshot::sort::canonicalize;
24
25pub mod state {
26 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27 pub struct Unsorted;
28
29 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30 pub struct Canonical;
31}
32
33pub const FORMAT_VERSION: u32 = 4;
34
35pub const SUPPORTED_FORMAT_VERSIONS: &[u32] = &[1, 2, 3, 4];
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct SnapshotHeader {
42 pub project: ProjectName,
43 pub tag: TagName,
44 pub branch: Option<String>,
45 pub commit: CommitSha,
46 pub scanned_paths: Vec<String>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub apps_scanned: Vec<ApplicationName>,
51 pub generated_by: String,
52 #[serde(with = "time::serde::rfc3339")]
53 pub generated_at: OffsetDateTime,
54 #[serde(default)]
58 pub extractor_version: String,
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub dep_pins: Vec<VendoredDep>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum Visibility {
70 Public,
71 Hidden,
72 TestOnly,
73}
74
75impl Visibility {
76 pub fn keyword(self) -> &'static str {
77 match self {
78 Self::Public => "public",
79 Self::Hidden => "hidden",
80 Self::TestOnly => "test_only",
81 }
82 }
83
84 pub fn from_keyword(s: &str) -> Option<Self> {
85 match s {
86 "public" => Some(Self::Public),
87 "hidden" => Some(Self::Hidden),
88 "test_only" => Some(Self::TestOnly),
89 _ => None,
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct Module {
96 pub name: ModuleName,
97 pub visibility: Visibility,
98 pub behaviours: Vec<ModuleName>,
99 pub exports: Vec<FunArity>,
100 pub export_types: Vec<TypeArity>,
101 pub callbacks: Vec<CallbackSig>,
102 pub optional_callbacks: Vec<FunArity>,
103 pub specs: Vec<SpecSig>,
104 pub types: Vec<TypeDecl>,
105 pub opaques: Vec<TypeArity>,
106 pub deprecations: Vec<Deprecation>,
107 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
111 pub clause_heads: BTreeMap<FunArity, Vec<Vec<ArgShape>>>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub path: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub app: Option<ApplicationName>,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub records: Vec<RecordDecl>,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 pub test_only_exports: Vec<TestOnlyExport>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub ifdef_macros: Vec<IfdefMacro>,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
141 pub variant_c_blocks: Vec<VariantCBlock>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub versioned_machine_version: Option<VersionedMachineVersion>,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
154 pub wire_constants: Vec<WireConstantBinding>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
158pub struct TestOnlyExport {
159 pub function: FunctionName,
160 pub arity: Arity,
161 pub export_line: usize,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub body_line: Option<usize>,
169 pub variant: TestExportVariant,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub enum TestExportVariant {
175 A,
179 B,
182}
183
184impl TestExportVariant {
185 pub fn as_label(self) -> &'static str {
186 match self {
187 Self::A => "a",
188 Self::B => "b",
189 }
190 }
191
192 pub fn from_label(s: &str) -> Option<Self> {
193 match s {
194 "a" => Some(Self::A),
195 "b" => Some(Self::B),
196 _ => None,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
202pub struct IfdefMacro {
203 pub name: String,
204 pub line: usize,
206 pub guard_kind: IfdefGuardKind,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum IfdefGuardKind {
212 Test,
214 NotTest,
216 Other,
218}
219
220impl IfdefGuardKind {
221 pub fn as_label(self) -> &'static str {
222 match self {
223 Self::Test => "test",
224 Self::NotTest => "not_test",
225 Self::Other => "other",
226 }
227 }
228
229 pub fn from_label(s: &str) -> Option<Self> {
230 match s {
231 "test" => Some(Self::Test),
232 "not_test" => Some(Self::NotTest),
233 "other" => Some(Self::Other),
234 _ => None,
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
240pub struct VariantCBlock {
241 pub guard: String,
243 pub start_line: usize,
245 pub end_line: usize,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub else_line: Option<usize>,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
257pub struct VersionedMachineVersion {
258 pub function: FunctionName,
259 pub arity: Arity,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub value: Option<u64>,
265 pub provenance: Provenance,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case", tag = "kind")]
272pub enum Provenance {
273 Literal,
275 MacroBody {
279 macro_name: MacroName,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 defined_in: Option<String>,
282 },
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
289pub struct WireConstantBinding {
290 pub macro_name: MacroName,
291 pub value: WireValue,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub defined_in: Option<String>,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
301#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
302pub enum WireValue {
303 U64(u64),
305 Bytes(Vec<u8>),
310 Opaque(String),
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
317pub struct VendoredDep {
318 pub name: DependencyName,
319 pub version: DependencyVersion,
320 pub source: VendoredDepSource,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
326#[serde(rename_all = "snake_case")]
327pub enum VendoredDepSource {
328 Hex,
330 Git,
332 GitRmq,
334}
335
336impl VendoredDepSource {
337 pub fn as_label(self) -> &'static str {
338 match self {
339 Self::Hex => "hex",
340 Self::Git => "git",
341 Self::GitRmq => "git_rmq",
342 }
343 }
344
345 pub fn from_label(s: &str) -> Option<Self> {
346 match s {
347 "hex" => Some(Self::Hex),
348 "git" => Some(Self::Git),
349 "git_rmq" => Some(Self::GitRmq),
350 _ => None,
351 }
352 }
353}
354
355impl Module {
356 pub fn new(name: ModuleName) -> Self {
357 Self {
358 name,
359 visibility: Visibility::Public,
360 behaviours: Vec::new(),
361 exports: Vec::new(),
362 export_types: Vec::new(),
363 callbacks: Vec::new(),
364 optional_callbacks: Vec::new(),
365 specs: Vec::new(),
366 types: Vec::new(),
367 opaques: Vec::new(),
368 deprecations: Vec::new(),
369 clause_heads: BTreeMap::new(),
370 path: None,
371 app: None,
372 records: Vec::new(),
373 test_only_exports: Vec::new(),
374 ifdef_macros: Vec::new(),
375 variant_c_blocks: Vec::new(),
376 versioned_machine_version: None,
377 wire_constants: Vec::new(),
378 }
379 }
380
381 pub fn missing_clause_bodies(&self) -> Vec<FunArity> {
385 if self.clause_heads.is_empty() {
386 return Vec::new();
387 }
388 self.exports
389 .iter()
390 .filter(|fa| !self.clause_heads.contains_key(fa))
391 .cloned()
392 .collect()
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct HrlFile {
398 pub path: String,
399 pub types: Vec<TypeDecl>,
400 pub opaques: Vec<TypeArity>,
401 pub records: Vec<RecordDecl>,
402}
403
404impl HrlFile {
405 pub fn new(path: impl Into<String>) -> Self {
406 Self {
407 path: path.into(),
408 types: Vec::new(),
409 opaques: Vec::new(),
410 records: Vec::new(),
411 }
412 }
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
416pub struct FunArity {
417 pub name: FunctionName,
418 pub arity: Arity,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
422pub struct TypeArity {
423 pub name: TypeName,
424 pub arity: Arity,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct CallbackSig {
429 pub name: FunctionName,
430 pub arity: Arity,
431 pub signature: String,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct SpecSig {
436 pub name: FunctionName,
437 pub arity: Arity,
438 pub signature: String,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct TypeDecl {
443 pub name: TypeName,
444 pub arity: Arity,
445 pub rhs: String,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449pub struct RecordDecl {
450 pub name: RecordName,
451 pub fields: Vec<RecordField>,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455pub struct RecordField {
456 pub name: FieldName,
457 pub type_repr: Option<String>,
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
461pub struct Deprecation {
462 pub function: Option<FunctionName>,
463 pub arity_match: ArityMatch,
464 pub since: Option<TagName>,
465 pub replacement: Option<DeprecationReplacement>,
466 pub reason: Option<String>,
467 pub module_wide: bool,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471#[serde(rename_all = "snake_case", tag = "kind")]
472pub enum ArityMatch {
473 Exact { arity: Arity },
474 Any,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
478pub struct DeprecationReplacement {
479 pub function: FunctionName,
480 pub arity: Arity,
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484#[serde(bound = "")]
485pub struct Snapshot<S = state::Unsorted> {
486 pub header: SnapshotHeader,
487 pub modules: Vec<Module>,
488 pub headers: Vec<HrlFile>,
489 #[serde(skip)]
490 _state: PhantomData<S>,
491}
492
493impl Snapshot<state::Unsorted> {
494 pub fn from_extracted(
495 header: SnapshotHeader,
496 modules: Vec<Module>,
497 headers: Vec<HrlFile>,
498 ) -> Self {
499 Self {
500 header,
501 modules,
502 headers,
503 _state: PhantomData,
504 }
505 }
506
507 #[must_use]
508 pub fn into_canonical(mut self) -> Snapshot<state::Canonical> {
509 canonicalize(&mut self.modules, &mut self.headers);
510 self.header.apps_scanned.sort();
511 self.header.apps_scanned.dedup();
512 self.header.dep_pins.sort_by(|a, b| a.name.cmp(&b.name));
513 self.header.dep_pins.dedup_by(|a, b| a.name == b.name);
514 Snapshot {
515 header: self.header,
516 modules: self.modules,
517 headers: self.headers,
518 _state: PhantomData,
519 }
520 }
521}
522
523impl<S> Snapshot<S> {
524 pub fn header(&self) -> &SnapshotHeader {
525 &self.header
526 }
527
528 pub fn modules(&self) -> &[Module] {
529 &self.modules
530 }
531
532 pub fn headers(&self) -> &[HrlFile] {
533 &self.headers
534 }
535
536 pub fn records(&self) -> impl Iterator<Item = &RecordDecl> {
538 self.headers
539 .iter()
540 .flat_map(|h| h.records.iter())
541 .chain(self.modules.iter().flat_map(|m| m.records.iter()))
542 }
543}
544
545impl Snapshot<state::Canonical> {
546 pub fn lookup_export(
549 &self,
550 module: &ModuleName,
551 function: &FunctionName,
552 arity: Arity,
553 ) -> bool {
554 let Some(m) = self.module_named(module) else {
555 return false;
556 };
557 let target = FunArity {
558 name: function.clone(),
559 arity,
560 };
561 m.exports.binary_search(&target).is_ok()
562 }
563
564 pub fn module_named(&self, name: &ModuleName) -> Option<&Module> {
567 self.modules
568 .binary_search_by(|m| m.name.cmp(name))
569 .ok()
570 .map(|idx| &self.modules[idx])
571 }
572
573 pub fn implementers_of(&self, behaviour: &ModuleName) -> Vec<&Module> {
578 self.modules
579 .iter()
580 .filter(|m| m.behaviours.contains(behaviour))
581 .collect()
582 }
583
584 pub(crate) fn from_canonical_parts(
585 header: SnapshotHeader,
586 modules: Vec<Module>,
587 headers: Vec<HrlFile>,
588 ) -> Self {
589 Self {
590 header,
591 modules,
592 headers,
593 _state: PhantomData,
594 }
595 }
596}
597
598impl Default for Snapshot<state::Unsorted> {
599 fn default() -> Self {
600 Self {
601 header: SnapshotHeader {
602 project: ProjectName::new("placeholder").expect("valid"),
603 tag: TagName::new("v0.0.0").expect("valid"),
604 branch: None,
605 commit: CommitSha::new("0".repeat(40)).expect("valid"),
606 scanned_paths: Vec::new(),
607 apps_scanned: Vec::new(),
608 generated_by: format!("backhopper {}", env!("CARGO_PKG_VERSION")),
609 generated_at: OffsetDateTime::UNIX_EPOCH,
610 extractor_version: String::new(),
611 dep_pins: Vec::new(),
612 },
613 modules: Vec::new(),
614 headers: Vec::new(),
615 _state: PhantomData,
616 }
617 }
618}