Skip to main content

stav/check/
host.rs

1//! The interface between the checker and the program that embeds it.
2
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::path::{Component, Path, PathBuf};
7
8use crate::value::Value;
9
10/// Reads source files for the checker.
11///
12/// The library never touches the filesystem itself. Every source file it
13/// checks, other than the sources a [`Host`] passes in directly, comes from
14/// a loader. A loader that reads from disk is expected to resolve symbolic
15/// links and to refuse files whose real path leaves the import root.
16pub trait Loader {
17    /// Returns the source of the module `std/<name>`, or `None` if the host
18    /// has no such module.
19    fn std_module(&self, name: &str) -> Option<Cow<'static, str>>;
20
21    /// Reads the file `rel` inside the import root `root`.
22    ///
23    /// # Errors
24    ///
25    /// Returns a [`LoadError`] when the file does not exist, is not a
26    /// regular file, leaves the import root once symbolic links are
27    /// resolved, or cannot be read.
28    fn read(&self, root: &ImportRoot, rel: &RelPath) -> Result<String, LoadError>;
29}
30
31/// Why a [`Loader`] could not read a file.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum LoadError {
34    /// No file exists at the path.
35    NotFound,
36    /// The path names something other than a regular file.
37    NotAFile,
38    /// The real path of the file, with symbolic links resolved, leaves the
39    /// import root.
40    OutsideRoot,
41    /// The file is larger than the checker accepts.
42    TooLarge,
43    /// The file could not be read for another reason, described here.
44    Other(String),
45}
46
47/// A directory that the files of a root may reference.
48///
49/// Imports, part files and path literals of a file cannot leave the import
50/// root the file belongs to.
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct ImportRoot {
53    path: PathBuf,
54}
55
56impl ImportRoot {
57    /// Returns an import root for the directory `path`.
58    pub fn new(path: impl Into<PathBuf>) -> Self {
59        Self { path: path.into() }
60    }
61
62    /// Returns the directory of the import root.
63    pub fn path(&self) -> &Path {
64        &self.path
65    }
66}
67
68/// A normalized path inside an import root, such as `lib/net.stav`.
69///
70/// It consists of one or more `/`-separated components, none of which is
71/// empty, `.` or `..`.
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct RelPath(String);
74
75impl RelPath {
76    /// Returns the path for the components of `path`, normalizing `.` and
77    /// `..` lexically. Returns `None` if the path is absolute, not UTF-8,
78    /// empty, or leaves its starting directory.
79    pub fn new(path: impl AsRef<Path>) -> Option<Self> {
80        let mut parts: Vec<&str> = Vec::new();
81        for component in path.as_ref().components() {
82            match component {
83                Component::Normal(part) => parts.push(part.to_str()?),
84                Component::CurDir => {}
85                Component::ParentDir => {
86                    parts.pop()?;
87                }
88                Component::RootDir | Component::Prefix(_) => return None,
89            }
90        }
91        Self::from_parts(parts)
92    }
93
94    /// Returns the path with the components `parts`, or `None` when there
95    /// are none or one is empty, `.`, `..` or contains a `/`.
96    pub(crate) fn from_parts<'a>(parts: impl IntoIterator<Item = &'a str>) -> Option<Self> {
97        let parts: Vec<&str> = parts.into_iter().collect();
98        let valid = |part: &&str| !matches!(*part, "" | "." | "..") && !part.contains('/');
99        (!parts.is_empty() && parts.iter().all(valid)).then(|| Self(parts.join("/")))
100    }
101
102    /// Returns the path as text, with `/` between components.
103    pub fn as_str(&self) -> &str {
104        &self.0
105    }
106
107    /// Returns the path as a relative [`Path`].
108    pub fn to_path(&self) -> PathBuf {
109        self.0.split('/').collect()
110    }
111
112    /// Returns the components of the directory that contains the file.
113    pub(crate) fn dir(&self) -> Vec<&str> {
114        let mut parts: Vec<&str> = self.0.split('/').collect();
115        parts.pop();
116        parts
117    }
118}
119
120impl fmt::Display for RelPath {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str(&self.0)
123    }
124}
125
126/// A source text the host passes in directly, such as a prelude file.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct Source {
129    /// The name the file has in diagnostics, for example `prelude/system.stav`.
130    pub name: String,
131    /// The source text.
132    pub text: String,
133}
134
135impl Source {
136    /// Returns a source with the given name and text.
137    pub fn new(name: impl Into<String>, text: impl Into<String>) -> Self {
138        Self {
139            name: name.into(),
140            text: text.into(),
141        }
142    }
143}
144
145/// A domain type whose values the host validates after evaluation, such as
146/// a package reference.
147///
148/// Where a symbol type is expected, an identifier is a literal of that type.
149/// Symbol words and string literals are literals of it when the host allows
150/// them. The constructor converts any string value.
151#[derive(Debug, Clone)]
152pub struct SymbolType {
153    /// The type name, which starts with an uppercase letter.
154    pub name: String,
155    /// Whether symbol words such as `name@1.2:static` are literals of the
156    /// type.
157    pub words: bool,
158    /// Whether string literals are literals of the type.
159    pub strings: bool,
160    /// The name of the built-in function that converts a `Str` into a value
161    /// of the type, for example `pkg`.
162    pub constructor: String,
163    /// Checks the text of a literal when it is evaluated, and returns its
164    /// canonical text or a message that says what is wrong.
165    pub parse: fn(&str) -> Result<String, String>,
166}
167
168/// A module whose source the host supplies for one evaluation, imported as
169/// `"<namespace>/<module>"`.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct HostModule {
172    /// The import path, a namespace and a module name such as `ns/target`.
173    pub path: String,
174    /// The source text, which may contain declarations only.
175    pub source: String,
176}
177
178/// Labels that a host lets references to a struct name, in addition to
179/// the labels of the struct's target map.
180///
181/// The host derives them from the merged value of the root, for example a
182/// group for every user that has no explicit group. The function must be
183/// deterministic and total. It runs after merging and before references
184/// are checked, and cannot change the value.
185#[derive(Debug, Clone)]
186pub struct ImplicitRefs {
187    /// The name of the struct the labels are targets for, for example
188    /// `Group`.
189    pub target: String,
190    /// Returns the labels, given the merged value of the root.
191    pub labels: fn(&Value) -> Vec<String>,
192}
193
194/// Everything a host program contributes to the checking of a root.
195pub struct Host {
196    /// Files whose declarations are in scope, unqualified, in every file.
197    pub prelude: Vec<Source>,
198    /// Part files included at the start of the root body, before every entry
199    /// file.
200    pub implicit_parts: Vec<Source>,
201    /// The domain types the host validates after evaluation.
202    pub symbols: Vec<SymbolType>,
203    /// The modules the host supplies for this evaluation.
204    pub host_modules: Vec<HostModule>,
205    /// Labels that references may name although no block declares them.
206    pub implicit_refs: Vec<ImplicitRefs>,
207    /// Reads every other source file.
208    pub loader: Box<dyn Loader>,
209}
210
211impl Host {
212    /// Returns a host without prelude, implicit parts, symbol types, host
213    /// modules or implicit reference targets, which reads files through
214    /// `loader`.
215    pub fn new(loader: impl Loader + 'static) -> Self {
216        Self {
217            prelude: Vec::new(),
218            implicit_parts: Vec::new(),
219            symbols: Vec::new(),
220            host_modules: Vec::new(),
221            implicit_refs: Vec::new(),
222            loader: Box::new(loader),
223        }
224    }
225}
226
227impl fmt::Debug for Host {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_struct("Host")
230            .field("prelude", &self.prelude)
231            .field("implicit_parts", &self.implicit_parts)
232            .field("symbols", &self.symbols)
233            .field("host_modules", &self.host_modules)
234            .field("implicit_refs", &self.implicit_refs)
235            .finish_non_exhaustive()
236    }
237}
238
239/// What to check: the root struct, the entry files and the import roots.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct RootSpec {
242    /// The name of the root struct, declared in the prelude or in an entry
243    /// file.
244    pub root_struct: String,
245    /// The entry files, each a body of the root struct, in evaluation
246    /// order. Each must lie inside one of the import roots.
247    pub entries: Vec<PathBuf>,
248    /// The directories that files of the root may reference.
249    pub import_roots: Vec<ImportRoot>,
250}
251
252/// A [`Loader`] that serves files and standard modules from memory.
253///
254/// It suits tests and hosts that embed their sources. Files are keyed by
255/// the path of their import root joined with their relative path.
256///
257/// # Examples
258///
259/// ```
260/// use stav::{Host, ImportRoot, MemoryLoader, RootSpec};
261///
262/// let loader = MemoryLoader::new()
263///     .with_file("app/main.stav", "struct Root { port: Int = 80 }\nport = 8080\n");
264/// let root = RootSpec {
265///     root_struct: "Root".to_owned(),
266///     entries: vec!["app/main.stav".into()],
267///     import_roots: vec![ImportRoot::new("app")],
268/// };
269/// let checked = stav::check(&Host::new(loader), &root);
270/// assert!(checked.is_ok());
271/// ```
272#[derive(Debug, Clone, Default)]
273pub struct MemoryLoader {
274    files: BTreeMap<PathBuf, String>,
275    std: BTreeMap<String, String>,
276}
277
278impl MemoryLoader {
279    /// Returns a loader without files.
280    pub fn new() -> Self {
281        Self::default()
282    }
283
284    /// Returns the loader with a file at `path`, replacing any file at the
285    /// same path.
286    pub fn with_file(mut self, path: impl Into<PathBuf>, text: impl Into<String>) -> Self {
287        self.files.insert(path.into(), text.into());
288        self
289    }
290
291    /// Returns the loader with the module `std/<name>`.
292    pub fn with_std(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
293        self.std.insert(name.into(), text.into());
294        self
295    }
296}
297
298impl Loader for MemoryLoader {
299    fn std_module(&self, name: &str) -> Option<Cow<'static, str>> {
300        self.std.get(name).map(|text| Cow::Owned(text.clone()))
301    }
302
303    fn read(&self, root: &ImportRoot, rel: &RelPath) -> Result<String, LoadError> {
304        self.files
305            .get(&root.path().join(rel.to_path()))
306            .cloned()
307            .ok_or(LoadError::NotFound)
308    }
309}