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")]
112 pub clause_heads: BTreeMap<FunArity, Vec<Vec<ArgShape>>>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub path: Option<String>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub app: Option<ApplicationName>,
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub records: Vec<RecordDecl>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub test_only_exports: Vec<TestOnlyExport>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub ifdef_macros: Vec<IfdefMacro>,
138 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub variant_c_blocks: Vec<VariantCBlock>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub versioned_machine_version: Option<VersionedMachineVersion>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub wire_constants: Vec<WireConstantBinding>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
159pub struct TestOnlyExport {
160 pub function: FunctionName,
161 pub arity: Arity,
162 pub export_line: usize,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub body_line: Option<usize>,
170 pub variant: TestExportVariant,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum TestExportVariant {
176 A,
180 B,
183}
184
185impl TestExportVariant {
186 pub fn as_label(self) -> &'static str {
187 match self {
188 Self::A => "a",
189 Self::B => "b",
190 }
191 }
192
193 pub fn from_label(s: &str) -> Option<Self> {
194 match s {
195 "a" => Some(Self::A),
196 "b" => Some(Self::B),
197 _ => None,
198 }
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
203pub struct IfdefMacro {
204 pub name: String,
205 pub line: usize,
207 pub guard_kind: IfdefGuardKind,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum IfdefGuardKind {
213 Test,
215 NotTest,
217 Other,
219}
220
221impl IfdefGuardKind {
222 pub fn as_label(self) -> &'static str {
223 match self {
224 Self::Test => "test",
225 Self::NotTest => "not_test",
226 Self::Other => "other",
227 }
228 }
229
230 pub fn from_label(s: &str) -> Option<Self> {
231 match s {
232 "test" => Some(Self::Test),
233 "not_test" => Some(Self::NotTest),
234 "other" => Some(Self::Other),
235 _ => None,
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
241pub struct VariantCBlock {
242 pub guard: String,
244 pub start_line: usize,
246 pub end_line: usize,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub else_line: Option<usize>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
258pub struct VersionedMachineVersion {
259 pub function: FunctionName,
260 pub arity: Arity,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub value: Option<u64>,
266 pub provenance: Provenance,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case", tag = "kind")]
273pub enum Provenance {
274 Literal,
276 MacroBody {
280 macro_name: MacroName,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 defined_in: Option<String>,
283 },
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
290pub struct WireConstantBinding {
291 pub macro_name: MacroName,
292 pub value: WireValue,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub defined_in: Option<String>,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
302#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
303pub enum WireValue {
304 U64(u64),
306 Bytes(Vec<u8>),
311 Opaque(String),
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
318pub struct VendoredDep {
319 pub name: DependencyName,
320 pub version: DependencyVersion,
321 pub source: VendoredDepSource,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case")]
328pub enum VendoredDepSource {
329 Hex,
331 Git,
333 GitRmq,
335}
336
337impl VendoredDepSource {
338 pub fn as_label(self) -> &'static str {
339 match self {
340 Self::Hex => "hex",
341 Self::Git => "git",
342 Self::GitRmq => "git_rmq",
343 }
344 }
345
346 pub fn from_label(s: &str) -> Option<Self> {
347 match s {
348 "hex" => Some(Self::Hex),
349 "git" => Some(Self::Git),
350 "git_rmq" => Some(Self::GitRmq),
351 _ => None,
352 }
353 }
354}
355
356impl Module {
357 pub fn new(name: ModuleName) -> Self {
358 Self {
359 name,
360 visibility: Visibility::Public,
361 behaviours: Vec::new(),
362 exports: Vec::new(),
363 export_types: Vec::new(),
364 callbacks: Vec::new(),
365 optional_callbacks: Vec::new(),
366 specs: Vec::new(),
367 types: Vec::new(),
368 opaques: Vec::new(),
369 deprecations: Vec::new(),
370 clause_heads: BTreeMap::new(),
371 path: None,
372 app: None,
373 records: Vec::new(),
374 test_only_exports: Vec::new(),
375 ifdef_macros: Vec::new(),
376 variant_c_blocks: Vec::new(),
377 versioned_machine_version: None,
378 wire_constants: Vec::new(),
379 }
380 }
381
382 pub fn missing_clause_bodies(&self) -> Vec<FunArity> {
386 if self.clause_heads.is_empty() {
387 return Vec::new();
388 }
389 self.exports
390 .iter()
391 .filter(|fa| !self.clause_heads.contains_key(fa))
392 .cloned()
393 .collect()
394 }
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398pub struct HrlFile {
399 pub path: String,
400 pub types: Vec<TypeDecl>,
401 pub opaques: Vec<TypeArity>,
402 pub records: Vec<RecordDecl>,
403}
404
405impl HrlFile {
406 pub fn new(path: impl Into<String>) -> Self {
407 Self {
408 path: path.into(),
409 types: Vec::new(),
410 opaques: Vec::new(),
411 records: Vec::new(),
412 }
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
417pub struct FunArity {
418 pub name: FunctionName,
419 pub arity: Arity,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
423pub struct TypeArity {
424 pub name: TypeName,
425 pub arity: Arity,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct CallbackSig {
430 pub name: FunctionName,
431 pub arity: Arity,
432 pub signature: String,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct SpecSig {
437 pub name: FunctionName,
438 pub arity: Arity,
439 pub signature: String,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443pub struct TypeDecl {
444 pub name: TypeName,
445 pub arity: Arity,
446 pub rhs: String,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct RecordDecl {
451 pub name: RecordName,
452 pub fields: Vec<RecordField>,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456pub struct RecordField {
457 pub name: FieldName,
458 pub type_repr: Option<String>,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct Deprecation {
463 pub function: Option<FunctionName>,
464 pub arity_match: ArityMatch,
465 pub since: Option<TagName>,
466 pub replacement: Option<DeprecationReplacement>,
467 pub reason: Option<String>,
468 pub module_wide: bool,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case", tag = "kind")]
473pub enum ArityMatch {
474 Exact { arity: Arity },
475 Any,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479pub struct DeprecationReplacement {
480 pub function: FunctionName,
481 pub arity: Arity,
482}
483
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(bound = "")]
486pub struct Snapshot<S = state::Unsorted> {
487 pub header: SnapshotHeader,
488 pub modules: Vec<Module>,
489 pub headers: Vec<HrlFile>,
490 #[serde(skip)]
491 _state: PhantomData<S>,
492}
493
494impl Snapshot<state::Unsorted> {
495 pub fn from_extracted(
496 header: SnapshotHeader,
497 modules: Vec<Module>,
498 headers: Vec<HrlFile>,
499 ) -> Self {
500 Self {
501 header,
502 modules,
503 headers,
504 _state: PhantomData,
505 }
506 }
507
508 #[must_use]
509 pub fn into_canonical(mut self) -> Snapshot<state::Canonical> {
510 canonicalize(&mut self.modules, &mut self.headers);
511 self.header.apps_scanned.sort();
512 self.header.apps_scanned.dedup();
513 self.header.dep_pins.sort_by(|a, b| a.name.cmp(&b.name));
514 self.header.dep_pins.dedup_by(|a, b| a.name == b.name);
515 Snapshot {
516 header: self.header,
517 modules: self.modules,
518 headers: self.headers,
519 _state: PhantomData,
520 }
521 }
522}
523
524impl<S> Snapshot<S> {
525 pub fn header(&self) -> &SnapshotHeader {
526 &self.header
527 }
528
529 pub fn modules(&self) -> &[Module] {
530 &self.modules
531 }
532
533 pub fn headers(&self) -> &[HrlFile] {
534 &self.headers
535 }
536
537 pub fn records(&self) -> impl Iterator<Item = &RecordDecl> {
539 self.headers
540 .iter()
541 .flat_map(|h| h.records.iter())
542 .chain(self.modules.iter().flat_map(|m| m.records.iter()))
543 }
544}
545
546impl Snapshot<state::Canonical> {
547 pub fn lookup_export(
550 &self,
551 module: &ModuleName,
552 function: &FunctionName,
553 arity: Arity,
554 ) -> bool {
555 let Some(m) = self.module_named(module) else {
556 return false;
557 };
558 let target = FunArity {
559 name: function.clone(),
560 arity,
561 };
562 m.exports.binary_search(&target).is_ok()
563 }
564
565 pub fn module_named(&self, name: &ModuleName) -> Option<&Module> {
568 self.modules
569 .binary_search_by(|m| m.name.cmp(name))
570 .ok()
571 .map(|idx| &self.modules[idx])
572 }
573
574 pub fn implementers_of(&self, behaviour: &ModuleName) -> Vec<&Module> {
579 self.modules
580 .iter()
581 .filter(|m| m.behaviours.contains(behaviour))
582 .collect()
583 }
584
585 pub(crate) fn from_canonical_parts(
586 header: SnapshotHeader,
587 modules: Vec<Module>,
588 headers: Vec<HrlFile>,
589 ) -> Self {
590 Self {
591 header,
592 modules,
593 headers,
594 _state: PhantomData,
595 }
596 }
597}
598
599impl Default for Snapshot<state::Unsorted> {
600 fn default() -> Self {
601 Self {
602 header: SnapshotHeader {
603 project: ProjectName::new("placeholder").expect("valid"),
604 tag: TagName::new("v0.0.0").expect("valid"),
605 branch: None,
606 commit: CommitSha::new("0".repeat(40)).expect("valid"),
607 scanned_paths: Vec::new(),
608 apps_scanned: Vec::new(),
609 generated_by: format!("backhopper {}", env!("CARGO_PKG_VERSION")),
610 generated_at: OffsetDateTime::UNIX_EPOCH,
611 extractor_version: String::new(),
612 dep_pins: Vec::new(),
613 },
614 modules: Vec::new(),
615 headers: Vec::new(),
616 _state: PhantomData,
617 }
618 }
619}