Skip to main content

vize_l2/summary/
global.rs

1//! Project summary for facts no single SFC owns (P5-3).
2//!
3//! Global components are the P4-3b project group. App-level provide/inject
4//! pairs are the P4-3f project group. Dialect-wide directives sit beside
5//! them. Each fact has its own fingerprint, in a domain that is not the
6//! per-file summary's, so a project edit cannot move an `SfcSummary`.
7
8use alloc::vec::Vec;
9
10use vize_l0::String;
11
12use super::{Fingerprint, digest};
13use vize_l0::fact::ids::{COMPONENT_USAGES, PROVIDE_INJECT};
14
15const DOMAIN: &[u8] = b"vize.global-summary.v2\0";
16
17const _: () = {
18    assert!(COMPONENT_USAGES.index() == 4);
19    assert!(PROVIDE_INJECT.index() == 8);
20};
21
22/// One project-level fact group.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[repr(u8)]
25pub enum GlobalFacet {
26    /// A component registered for the whole project (P4-3b).
27    Component = 0,
28    /// An app-level provide (P4-3f).
29    Provide = 1,
30    /// A directive the dialect defines for every file.
31    Directive = 2,
32}
33
34impl GlobalFacet {
35    const fn group(self) -> &'static str {
36        match self {
37            Self::Component => "global-components",
38            Self::Provide => "app-provide-inject",
39            Self::Directive => "dialect-directives",
40        }
41    }
42
43    const fn schema(self) -> u16 {
44        1
45    }
46}
47
48/// One orphan fact: identity plus the contract spelling, not a body.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct GlobalEntry {
51    /// Fact identity.
52    pub name: String,
53    /// Contract spelling. Not a function body.
54    pub contract: String,
55}
56
57/// The three orphan groups, still empty when the project has none.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct GlobalFacts {
60    /// P4-3b global components.
61    pub components: Vec<GlobalEntry>,
62    /// P4-3f app-level provides.
63    pub provides: Vec<GlobalEntry>,
64    /// Dialect-wide directives.
65    pub directives: Vec<GlobalEntry>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69struct Fact {
70    facet: GlobalFacet,
71    name: String,
72    contract: String,
73}
74
75impl Fact {
76    fn fingerprint(&self) -> Fingerprint {
77        digest(
78            DOMAIN,
79            self.facet.group(),
80            self.facet.schema(),
81            &self.name,
82            &self.contract,
83        )
84    }
85}
86
87/// The project summary. It does not store per-file declarations.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct GlobalSummary {
90    facts: Vec<Fact>,
91}
92
93impl GlobalSummary {
94    /// Build the summary from the three orphan groups.
95    ///
96    /// # Errors
97    ///
98    /// A bad name, a contract that contains a newline, or two facts of the
99    /// same facet with the same name.
100    pub fn from_facts(facts: GlobalFacts) -> Result<Self, GlobalError> {
101        let mut stored = Vec::new();
102        insert_all(&mut stored, GlobalFacet::Component, facts.components)?;
103        insert_all(&mut stored, GlobalFacet::Provide, facts.provides)?;
104        insert_all(&mut stored, GlobalFacet::Directive, facts.directives)?;
105        stored.sort_unstable_by(|lhs, rhs| {
106            (lhs.facet, lhs.name.as_str()).cmp(&(rhs.facet, rhs.name.as_str()))
107        });
108        Ok(Self { facts: stored })
109    }
110
111    /// The fingerprint of one project fact, if this summary exports it.
112    #[must_use]
113    pub fn fingerprint(&self, facet: GlobalFacet, name: &str) -> Option<Fingerprint> {
114        let index = self
115            .facts
116            .binary_search_by(|fact| (fact.facet, fact.name.as_str()).cmp(&(facet, name)))
117            .ok()?;
118        self.facts.get(index).map(|fact| fact.fingerprint())
119    }
120
121    /// Files in `resolutions` order whose recorded project facts are not all
122    /// still this summary's. Everyone else is left out.
123    #[must_use]
124    pub fn invalidated<'a>(&self, resolutions: &'a [GlobalResolution]) -> Vec<&'a str> {
125        resolutions
126            .iter()
127            .filter(|resolution| !resolution.is_fresh(self))
128            .map(GlobalResolution::file)
129            .collect()
130    }
131}
132
133/// The project facts one file resolved, each with the fingerprint it saw.
134///
135/// A name this summary did not export is stored with no fingerprint. Adding
136/// that fact later makes the file stale. A name the file did not resolve
137/// cannot invalidate it.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct GlobalResolution {
140    file: String,
141    resolved: Vec<(GlobalFacet, String, Option<Fingerprint>)>,
142}
143
144impl GlobalResolution {
145    /// Record `resolved` as read from `summary` by `file`.
146    ///
147    /// # Errors
148    ///
149    /// An empty file name, or the same fact twice.
150    pub fn record(
151        file: &str,
152        summary: &GlobalSummary,
153        resolved: &[(GlobalFacet, &str)],
154    ) -> Result<Self, GlobalError> {
155        if file.is_empty() {
156            return Err(GlobalError::EmptyFile);
157        }
158        let mut stored = Vec::with_capacity(resolved.len());
159        for &(facet, name) in resolved {
160            if stored
161                .iter()
162                .any(|(seen_facet, seen_name, _)| *seen_facet == facet && seen_name == name)
163            {
164                return Err(GlobalError::Duplicate {
165                    facet,
166                    name: String::from(name),
167                });
168            }
169            stored.push((facet, String::from(name), summary.fingerprint(facet, name)));
170        }
171        Ok(Self {
172            file: String::from(file),
173            resolved: stored,
174        })
175    }
176
177    /// The file's name.
178    #[must_use]
179    pub fn file(&self) -> &str {
180        &self.file
181    }
182
183    /// Whether every recorded fingerprint is unchanged in `next`.
184    #[must_use]
185    pub fn is_fresh(&self, next: &GlobalSummary) -> bool {
186        self.resolved
187            .iter()
188            .all(|(facet, name, seen)| next.fingerprint(*facet, name) == *seen)
189    }
190}
191
192/// Why a project summary could not be built, or a resolution could not be recorded.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum GlobalError {
195    /// The fact name is empty or contains whitespace or `=`.
196    BadName { facet: GlobalFacet, name: String },
197    /// The contract contains a newline.
198    BadContract { facet: GlobalFacet, name: String },
199    /// Two facts of `facet` share `name`.
200    Duplicate { facet: GlobalFacet, name: String },
201    /// A resolution named no file.
202    EmptyFile,
203}
204
205fn insert_all(
206    out: &mut Vec<Fact>,
207    facet: GlobalFacet,
208    entries: Vec<GlobalEntry>,
209) -> Result<(), GlobalError> {
210    for entry in entries {
211        insert(out, facet, entry.name, entry.contract)?;
212    }
213    Ok(())
214}
215
216fn insert(
217    out: &mut Vec<Fact>,
218    facet: GlobalFacet,
219    name: String,
220    contract: String,
221) -> Result<(), GlobalError> {
222    if name.is_empty() || name.chars().any(|ch| ch.is_whitespace() || ch == '=') {
223        return Err(GlobalError::BadName { facet, name });
224    }
225    if contract.chars().any(|ch| ch == '\n' || ch == '\r') {
226        return Err(GlobalError::BadContract { facet, name });
227    }
228    if out
229        .iter()
230        .any(|fact| fact.facet == facet && fact.name == name)
231    {
232        return Err(GlobalError::Duplicate { facet, name });
233    }
234    out.push(Fact {
235        facet,
236        name,
237        contract,
238    });
239    Ok(())
240}