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}