Skip to main content

aura_lang/
script.rs

1//! The embedding API (D33): a loaded script a Rust host can call.
2//!
3//! A `Script` is loaded once — lexed, parsed, analysed and evaluated — and then
4//! called as often as the host likes, from as many threads as it likes. Every
5//! guarantee the CLI gives holds here too, because loading goes through the same
6//! loader, analysis and capability model:
7//!
8//! - **hermetic by default**: with no grants, loading proves statically that the
9//!   script reads no file, variable or clock (`E0505` otherwise);
10//! - **a budget per call**, in steps and bytes, never in time (`E0325`/`E0326`);
11//! - **constant memory per call**: each call runs in its own interpreter, whose
12//!   environments are released when it returns;
13//! - **a checked boundary**: a value Aura cannot hold exactly is refused at the
14//!   door (`E0005`), and a result of the wrong shape names the path (`E0006`).
15//!
16//! What a call costs: an `Arc` clone of the shared method registry, a fresh
17//! interpreter around it, and the conversion of arguments and result. Parsing,
18//! analysis and module evaluation are never repeated.
19
20use std::cell::RefCell;
21use std::collections::HashMap;
22use std::fmt;
23use std::marker::PhantomData;
24use std::path::{Path, PathBuf};
25use std::sync::{mpsc, Arc, Mutex, RwLock};
26use std::time::{Duration, Instant};
27
28use indexmap::IndexMap;
29use serde::de::DeserializeOwned;
30use serde::Serialize;
31
32use crate::error::Severity;
33use crate::eval::budget::Budget;
34use crate::eval::methods::MethodRegistry;
35use crate::eval::value::Value;
36use crate::eval::{DenyFs, EnvCap, FileAccess, FileError, Interpreter, MemFs, Options, RealFs};
37use crate::facade::{to_report, Report};
38use crate::host::{HostCall, HostFn, HostKind, HostOutcome, HostTable};
39use crate::source::SourceCache;
40use crate::span::Span;
41use crate::vfs::loader::Loader;
42use crate::vfs::lockfile::Lockfile;
43use crate::vfs::{FileResolver, ImportSpec, LocalFsResolver, MemoryResolver, ModuleId};
44
45// ---------------------------------------------------------------------------
46// Errors
47// ---------------------------------------------------------------------------
48
49/// Everything that can go wrong loading or calling a script. It carries the
50/// same reports the CLI prints: a code, a position when there is one, and the
51/// help line.
52#[derive(Debug, Clone)]
53pub struct Error {
54    reports: Vec<Report>,
55}
56
57impl Error {
58    /// The code of the reason the operation stopped — `"E0505"`, `"E0325"`, …
59    pub fn code(&self) -> &'static str {
60        self.reports.first().map_or("E0001", |r| r.code)
61    }
62
63    /// Every report, the reason first.
64    pub fn reports(&self) -> &[Report] {
65        &self.reports
66    }
67
68    fn host(code: &'static str, message: impl Into<String>) -> Self {
69        Error {
70            reports: vec![Report {
71                code,
72                severity: Severity::Error,
73                message: message.into(),
74                file: String::new(),
75                line: 0,
76                column: 0,
77                help: None,
78            }],
79        }
80    }
81}
82
83impl From<Vec<Report>> for Error {
84    fn from(reports: Vec<Report>) -> Self {
85        Error { reports }
86    }
87}
88
89impl fmt::Display for Error {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        for (i, r) in self.reports.iter().enumerate() {
92            if i > 0 {
93                writeln!(f)?;
94            }
95            write!(f, "{r}")?;
96        }
97        Ok(())
98    }
99}
100
101impl std::error::Error for Error {}
102
103// ---------------------------------------------------------------------------
104// Builder
105// ---------------------------------------------------------------------------
106
107/// Configures a script before it is loaded. Grants have the names and meaning
108/// of the CLI's flags.
109#[derive(Default)]
110pub struct ScriptBuilder {
111    sources: Vec<(String, String)>,
112    entry: Option<String>,
113    file: Option<PathBuf>,
114    env: Option<Vec<String>>,
115    env_values: HashMap<String, String>,
116    read: Vec<PathBuf>,
117    strict: bool,
118    budget: Budget,
119    host: Vec<HostFn>,
120    host_for_imports: Vec<String>,
121    host_call_steps: Option<u64>,
122    watch: Option<(Duration, OnReload)>,
123    settle: Option<Duration>,
124}
125
126impl ScriptBuilder {
127    /// A source held in memory. Several may be given; imports resolve among them.
128    pub fn source(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
129        self.sources.push((name.into(), text.into()));
130        self
131    }
132
133    /// Which in-memory source to start from. Defaults to the first one given.
134    pub fn entry(mut self, name: impl Into<String>) -> Self {
135        self.entry = Some(name.into());
136        self
137    }
138
139    /// Load from disk instead; imports resolve beside it, as in `aura eval`.
140    pub fn file(mut self, path: impl AsRef<Path>) -> Self {
141        self.file = Some(path.as_ref().to_path_buf());
142        self
143    }
144
145    /// `--allow-env=A,B`: the named variables only.
146    pub fn allow_env<I, S>(mut self, names: I) -> Self
147    where
148        I: IntoIterator<Item = S>,
149        S: Into<String>,
150    {
151        self.env
152            .get_or_insert_with(Vec::new)
153            .extend(names.into_iter().map(Into::into));
154        self
155    }
156
157    /// Grant these variables with these values. `env()` reads them from here
158    /// and never from the process, so a host's own environment cannot leak
159    /// into a script that was handed other values - and a test can give a
160    /// script any environment without changing the process's.
161    pub fn env_values<I, K, V>(mut self, values: I) -> Self
162    where
163        I: IntoIterator<Item = (K, V)>,
164        K: Into<String>,
165        V: Into<String>,
166    {
167        for (k, v) in values {
168            let k = k.into();
169            self.env.get_or_insert_with(Vec::new).push(k.clone());
170            self.env_values.insert(k, v.into());
171        }
172        self
173    }
174
175    /// `--allow-read=<dir>`.
176    pub fn allow_read(mut self, dir: impl AsRef<Path>) -> Self {
177        self.read.push(dir.as_ref().to_path_buf());
178        self
179    }
180
181    /// `--strict`: warnings fail the load.
182    pub fn strict(mut self) -> Self {
183        self.strict = true;
184        self
185    }
186
187    /// The budget each call gets. Loading is held to the same budget.
188    pub fn budget(mut self, budget: Budget) -> Self {
189        self.budget = budget;
190        self
191    }
192
193    /// D35: a function the script may call as `import "host:geo" as geo` …
194    /// `geo.region(ip)`, named `"geo.region"` here. *Pure* is the host's
195    /// promise that the function is deterministic - the language cannot check
196    /// it - and it is what lets the script stay hermetic. Return `Option` for a
197    /// value that may be absent: `None` reaches the script as `null`.
198    pub fn pure<Args, F: PureFn<Args>>(mut self, name: impl Into<String>, f: F) -> Self {
199        self.host.push(HostFn {
200            name: name.into(),
201            kind: HostKind::Pure,
202            arity: F::ARITY,
203            for_imports: false,
204            call: f.erase(),
205        });
206        self
207    }
208
209    /// D35: a function with an effect - it reads or changes something outside
210    /// the script. Registering it is the grant, like `allow_env`, so the load
211    /// is no longer hermetic. `Err` stops the call with `E0008`.
212    pub fn effect<Args, F: EffectFn<Args>>(mut self, name: impl Into<String>, f: F) -> Self {
213        self.host.push(HostFn {
214            name: name.into(),
215            kind: HostKind::Effect,
216            arity: F::ARITY,
217            for_imports: false,
218            call: f.erase(),
219        });
220        self
221    }
222
223    /// D35 §3: let imported modules call this host function too. By default
224    /// only the root module can - a package the script imports does not reach
225    /// the host's services unless the host says so, one function at a time.
226    pub fn grant_to_imports(mut self, name: impl Into<String>) -> Self {
227        self.host_for_imports.push(name.into());
228        self
229    }
230
231    /// D35 §5: what one call to a host function costs, in steps. The default
232    /// is 100. Time spent inside the host is not measured; this is what bounds
233    /// a loop of host calls.
234    pub fn host_call_steps(mut self, steps: u64) -> Self {
235        self.host_call_steps = Some(steps);
236        self
237    }
238
239    /// D36: watch the script's files, and reload when their content changes.
240    /// Every `interval` a thread owned by the script looks at the files; a
241    /// change is loaded once the files have been left alone for the settle
242    /// period ([`ScriptBuilder::settle`]), and
243    /// passes every check the first load did. `on_reload` is told of every
244    /// automatic reload - the new version, or why it was refused while the old
245    /// one keeps serving - so a failure cannot pass unnoticed. It runs on the
246    /// watcher's thread. Only a script loaded from a file can be watched.
247    pub fn watch(
248        mut self,
249        interval: Duration,
250        on_reload: impl Fn(Result<ScriptVersion, Error>) + Send + 'static,
251    ) -> Self {
252        self.watch = Some((interval, Box::new(on_reload)));
253        self
254    }
255
256    /// D36 §6: how long the files must stay unchanged before a watched script
257    /// reloads them - no write, no change of size or modification time. A
258    /// file being written is never at rest that long, so it is never loaded
259    /// half done. The default is two intervals and at least a second; raise it
260    /// for a slow network share. It may not be shorter than the interval.
261    pub fn settle(mut self, settle: Duration) -> Self {
262        self.settle = Some(settle);
263        self
264    }
265
266    pub fn load(self) -> Result<Script, Error> {
267        Script::build(self)
268    }
269}
270
271// ---------------------------------------------------------------------------
272// Script
273// ---------------------------------------------------------------------------
274
275/// What a call needs and the load produced: the module's exports and the
276/// interpreter that evaluated them. That interpreter is never used again; it is
277/// kept because it owns the environments the exported functions close over.
278struct State<'a> {
279    exports: IndexMap<String, Value<'a>>,
280    registry: Arc<MethodRegistry<'a>>,
281    fs: Arc<dyn FileAccess>,
282    env_cap: EnvCap,
283    env_values: Arc<HashMap<String, String>>,
284    host_call_steps: u64,
285    strict: bool,
286    _closures: Interpreter<'a>,
287}
288
289self_cell::self_cell!(
290    struct Loaded {
291        owner: SourceCache,
292        #[not_covariant]
293        dependent: State,
294    }
295);
296
297/// Everything a load uses, taken from the builder once and kept, so that a
298/// reload runs with exactly the grants the first load had (D36 §3).
299struct Config {
300    sources: Vec<(String, String)>,
301    entry: Option<String>,
302    file: Option<PathBuf>,
303    read: Vec<PathBuf>,
304    env_cap: EnvCap,
305    env_values: Arc<HashMap<String, String>>,
306    granted: bool,
307    strict: bool,
308    budget: Budget,
309    host: Arc<HostTable>,
310}
311
312/// Which version of a script is serving (D36 §8): the generation counts
313/// successful loads from 1, and the hash covers every file the load read.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct ScriptVersion {
316    pub generation: u64,
317    pub hash: String,
318}
319
320/// One loaded version: what calls run against, and the files it came from.
321struct Version {
322    loaded: Loaded,
323    id: ScriptVersion,
324    /// Every local file the load read, with what it read (D36 §5).
325    files: Vec<(PathBuf, String)>,
326}
327
328/// What a version promises the host: each export's name, and the number of
329/// arguments if it is a function (D36 §2).
330type Contract = Vec<(String, Option<usize>)>;
331
332/// The state every handle on a script shares, the watcher included.
333struct Shared {
334    config: Config,
335    /// The lock as the first load left it. Every reload is frozen to it, so a
336    /// reload cannot pull in a package the first load did not pin (D36 §4).
337    lock: Lockfile,
338    /// Fixed at the first load (D36 §2).
339    contract: Contract,
340    current: RwLock<Arc<Version>>,
341    /// One install at a time, so generations follow each other.
342    installing: Mutex<()>,
343}
344
345/// A load that was tried: its outcome, and the files it read on the way - kept
346/// even when it failed, so the watcher can tell a half-written file from a
347/// broken one (D36 §6).
348struct Attempt {
349    result: Result<(Loaded, Lockfile), Error>,
350    files: Vec<(PathBuf, String)>,
351    /// The generation it was loaded against; installed only onto that one.
352    base: u64,
353}
354
355/// A loaded script. `Send + Sync`: one instance serves every thread, and a
356/// call takes `&self`.
357pub struct Script {
358    shared: Arc<Shared>,
359    watcher: Option<Watcher>,
360}
361
362impl Script {
363    /// Load a file with no grants. Hermetic: the load fails if the script
364    /// could read anything at all.
365    pub fn load(path: impl AsRef<Path>) -> Result<Script, Error> {
366        Self::builder().file(path).load()
367    }
368
369    pub fn builder() -> ScriptBuilder {
370        ScriptBuilder::default()
371    }
372
373    fn build(mut b: ScriptBuilder) -> Result<Script, Error> {
374        let host = Arc::new(host_table(&mut b)?);
375        // An effect is a grant like any other (D35 §2); a pure function is not.
376        let granted = b.env.is_some() || !b.read.is_empty() || host.has_effects();
377        let env_cap = match &b.env {
378            None => EnvCap::Deny,
379            Some(names) => EnvCap::Allow(names.clone()),
380        };
381        let watch = b.watch.take();
382        if let (Some((interval, _)), Some(settle)) = (&watch, b.settle) {
383            if settle < *interval {
384                return Err(Error::host(
385                    "E0001",
386                    format!(
387                        "settle ({settle:?}) is shorter than the watch interval ({interval:?}): \
388                         the files would be read before anyone looked at them twice"
389                    ),
390                ));
391            }
392        }
393        if watch.is_some() && b.file.is_none() {
394            return Err(Error::host(
395                "E0001",
396                "only a script loaded from a file can be watched; an in-memory source has no file",
397            ));
398        }
399        let config = Config {
400            sources: std::mem::take(&mut b.sources),
401            entry: b.entry.take(),
402            file: b.file.take(),
403            read: std::mem::take(&mut b.read),
404            env_cap,
405            env_values: Arc::new(std::mem::take(&mut b.env_values)),
406            granted,
407            strict: b.strict,
408            budget: b.budget,
409            host,
410        };
411
412        let lock = match &config.file {
413            Some(path) => read_lock(path)?,
414            None => Lockfile::default(),
415        };
416        let attempt = load_version(&config, lock, false, 0);
417        let (loaded, lock) = attempt.result?;
418        let contract = contract_of(&loaded);
419        let version = Version {
420            loaded,
421            id: ScriptVersion {
422                generation: 1,
423                hash: hash_files(&attempt.files),
424            },
425            files: attempt.files,
426        };
427        let shared = Arc::new(Shared {
428            config,
429            lock,
430            contract,
431            current: RwLock::new(Arc::new(version)),
432            installing: Mutex::new(()),
433        });
434        let settle = b.settle;
435        let watcher = watch.map(|(interval, on_reload)| {
436            let settle = settle.unwrap_or_else(|| default_settle(interval));
437            Watcher::start(&shared, interval, settle, on_reload)
438        });
439        Ok(Script { shared, watcher })
440    }
441
442    /// The version serving now.
443    pub fn version(&self) -> ScriptVersion {
444        self.shared.current().id.clone()
445    }
446
447    /// Read the script's files again and, if they changed, load them as a new
448    /// version (D36). The new version passes every check the first load did,
449    /// with the same grants, against the same lock, and keeps every export the
450    /// host may have bound; if anything fails, the old version keeps serving
451    /// and the error says why. Content decides: files rewritten with the same
452    /// bytes are not a new version.
453    pub fn reload(&self) -> Result<ScriptVersion, Error> {
454        if self.shared.config.file.is_none() {
455            return Err(Error::host(
456                "E0001",
457                "a script built from in-memory sources has no files to reload; build a new Script",
458            ));
459        }
460        let attempt = self.shared.attempt();
461        Ok(self
462            .shared
463            .install(attempt)?
464            .unwrap_or_else(|| self.version()))
465    }
466
467    /// The whole manifest, as `aura eval` prints it - properties and blocks,
468    /// not bindings, functions, schemas or enums - as any `Deserialize` type.
469    pub fn output<T: DeserializeOwned>(&self) -> Result<T, Error> {
470        refuse_reentry()?;
471        let current = self.shared.current();
472        let json = current.loaded.with_dependent(|cache, state| {
473            let module = Value::Object(Arc::new(state.exports.clone()));
474            crate::serialize::to_json(&module).map_err(|d| Error::from(vec![to_report(&d, cache)]))
475        })?;
476        from_json(json)
477    }
478
479    /// A value the script exports, as any `Deserialize` type.
480    pub fn get<T: DeserializeOwned>(&self, name: &str) -> Result<T, Error> {
481        refuse_reentry()?;
482        let current = self.shared.current();
483        let json = current.loaded.with_dependent(|cache, state| {
484            let v = state.exports.get(name).ok_or_else(|| no_export(name))?;
485            crate::serialize::to_json(v).map_err(|d| Error::from(vec![to_report(&d, cache)]))
486        })?;
487        from_json(json)
488    }
489
490    /// Call a function by name. For a function called often, bind it once with
491    /// [`Script::function`] instead: the lookup and the checks then happen once.
492    pub fn call<A: Args, R: DeserializeOwned>(&self, name: &str, args: A) -> Result<R, Error> {
493        self.function::<A, R>(name)?.call(args)
494    }
495
496    /// Bind a function, checking now - not on the first request - that the name
497    /// exists (`E0002`), names a function (`E0003`), and takes as many
498    /// arguments as `A` has (`E0004`). A reload keeps all three true (D36 §2).
499    pub fn function<A: Args, R: DeserializeOwned>(
500        &self,
501        name: &str,
502    ) -> Result<Function<'_, A, R>, Error> {
503        self.shared.current().loaded.with_dependent(|_, state| {
504            let value = state.exports.get(name).ok_or_else(|| no_export(name))?;
505            let Value::Function(def) = value else {
506                return Err(Error::host(
507                    "E0003",
508                    format!("'{name}' is a {}, not a function", value.type_name()),
509                ));
510            };
511            if def.params.len() != A::ARITY {
512                return Err(Error::host(
513                    "E0004",
514                    format!(
515                        "'{name}' takes {} argument{}, but the host passes {}",
516                        def.params.len(),
517                        if def.params.len() == 1 { "" } else { "s" },
518                        A::ARITY
519                    ),
520                ));
521            }
522            Ok(())
523        })?;
524        Ok(Function {
525            script: self,
526            name: name.to_string(),
527            _types: PhantomData,
528        })
529    }
530
531    /// One call, start to finish, in its own interpreter, on the version that
532    /// was current when it started (D36 §9).
533    fn invoke<A: Args>(&self, name: &str, args: A) -> Result<serde_json::Value, Error> {
534        refuse_reentry()?;
535        let budget = self.shared.config.budget;
536        let current = self.shared.current();
537        current.loaded.with_dependent(|cache, state| {
538            // Found by name, not by position: a reload may reorder the exports
539            // (D36 §2), and the contract guarantees the name is still there.
540            let f = state.exports.get(name).ok_or_else(|| no_export(name))?;
541            let mut it = Interpreter::with_registry(
542                state.registry.clone(),
543                Options {
544                    strict: state.strict,
545                    dry_run: false,
546                },
547            );
548            it.fs = Box::new(SharedFs(state.fs.clone()));
549            it.env_cap = state.env_cap.clone();
550            it.env_overrides = (*state.env_values).clone();
551            it.host_call_steps = state.host_call_steps;
552            let _meter = budget.begin(&mut it);
553
554            let args = args.to_values().map_err(|e| Error::host("E0005", e.0))?;
555            let span = Span::new(0, 0, 0);
556            let result = it
557                .call_value(f, &args, span)
558                .map_err(|d| Error::from(vec![to_report(&d, cache)]))?;
559            // Converted before `it` is dropped, which is what releases every
560            // environment the call created.
561            crate::serialize::to_json(&result).map_err(|d| {
562                let mut e = Error::from(vec![to_report(&d, cache)]);
563                e.reports[0].code = "E0006";
564                e
565            })
566        })
567    }
568}
569
570impl Drop for Script {
571    fn drop(&mut self) {
572        if let Some(w) = self.watcher.take() {
573            w.stop();
574        }
575    }
576}
577
578impl Shared {
579    fn current(&self) -> Arc<Version> {
580        // A poisoned lock means a thread panicked while swapping; the Arc it
581        // holds is always a whole version, so it is safe to keep serving it.
582        self.current
583            .read()
584            .unwrap_or_else(|e| e.into_inner())
585            .clone()
586    }
587
588    /// Load the files as they are on disk now, frozen to the first lock.
589    fn attempt(&self) -> Attempt {
590        let base = self.current().id.generation;
591        load_version(&self.config, self.lock.clone(), true, base)
592    }
593
594    /// Make an attempt the current version. `Ok(None)` when its content is the
595    /// content already serving, or another reload got there first.
596    fn install(&self, attempt: Attempt) -> Result<Option<ScriptVersion>, Error> {
597        let (loaded, _) = attempt.result?;
598        let _one = self.installing.lock().unwrap_or_else(|e| e.into_inner());
599        let current = self.current();
600        let hash = hash_files(&attempt.files);
601        if hash == current.id.hash || current.id.generation != attempt.base {
602            return Ok(None);
603        }
604        check_contract(&self.contract, &contract_of(&loaded))?;
605        let id = ScriptVersion {
606            generation: current.id.generation + 1,
607            hash,
608        };
609        let version = Arc::new(Version {
610            loaded,
611            id: id.clone(),
612            files: attempt.files,
613        });
614        *self.current.write().unwrap_or_else(|e| e.into_inner()) = version;
615        Ok(Some(id))
616    }
617}
618
619/// `aura.lock` beside the entry file, if there is one.
620fn read_lock(path: &Path) -> Result<Lockfile, Error> {
621    let entry = std::fs::canonicalize(path)
622        .map_err(|e| Error::host("E0001", format!("cannot open {}: {e}", path.display())))?;
623    let root = entry.parent().unwrap_or(Path::new("."));
624    match std::fs::read_to_string(root.join("aura.lock")) {
625        Ok(text) => Lockfile::parse(&text)
626            .map_err(|e| Error::host("E0001", format!("invalid aura.lock: {e}"))),
627        Err(_) => Ok(Lockfile::default()),
628    }
629}
630
631/// One load, start to finish: resolve, analyse, check the host functions,
632/// evaluate under the budget. The same function serves the first load and
633/// every reload, which is what makes a reload no weaker than a load (D36 §3).
634fn load_version(config: &Config, lock: Lockfile, frozen: bool, base: u64) -> Attempt {
635    let mut files = Vec::new();
636    let mut lock_after = None;
637    let result = Loaded::try_new(SourceCache::new(), |cache| -> Result<State<'_>, Error> {
638        // Where modules and file reads come from: memory or disk.
639        let (resolver, fs, entry): (Box<dyn FileResolver>, Arc<dyn FileAccess>, String) =
640            match &config.file {
641                Some(path) => {
642                    let entry = std::fs::canonicalize(path).map_err(|e| {
643                        Error::host("E0001", format!("cannot open {}: {e}", path.display()))
644                    })?;
645                    let root = entry.parent().unwrap_or(Path::new(".")).to_path_buf();
646                    let registry_dir = std::env::var_os("USERPROFILE")
647                        .or_else(|| std::env::var_os("HOME"))
648                        .map(PathBuf::from)
649                        .unwrap_or_default()
650                        .join(".aura")
651                        .join("registry");
652                    let fs: Arc<dyn FileAccess> = if config.read.is_empty() {
653                        Arc::new(DenyFs)
654                    } else {
655                        Arc::new(RealFs {
656                            allowed: config.read.clone(),
657                        })
658                    };
659                    let name = entry
660                        .file_name()
661                        .unwrap_or_default()
662                        .to_string_lossy()
663                        .to_string();
664                    (Box::new(LocalFsResolver { root, registry_dir }), fs, name)
665                }
666                None => {
667                    let entry = config
668                        .entry
669                        .clone()
670                        .or_else(|| config.sources.first().map(|(n, _)| n.clone()))
671                        .ok_or_else(|| Error::host("E0001", "no source was given"))?;
672                    let files: HashMap<String, String> = config.sources.iter().cloned().collect();
673                    let fs: Arc<dyn FileAccess> = if config.read.is_empty() {
674                        Arc::new(DenyFs)
675                    } else {
676                        Arc::new(MemFs(files.clone()))
677                    };
678                    (Box::new(MemoryResolver { files }), fs, entry)
679                }
680            };
681        let recording = Recording {
682            inner: resolver.as_ref(),
683            seen: RefCell::new(Vec::new()),
684        };
685
686        let mut loader = Loader::new(cache, &recording);
687        // D33 §1: nothing granted, so prove nothing is used.
688        loader.hermetic = !config.granted;
689        loader.lock = lock;
690        loader.frozen = frozen;
691        loader.host = config.host.clone();
692
693        let registry = Arc::new(MethodRegistry::builtin());
694        let mut interp = Interpreter::with_registry(
695            registry.clone(),
696            Options {
697                strict: config.strict,
698                dry_run: false,
699            },
700        );
701        interp.fs = Box::new(SharedFs(fs.clone()));
702        interp.env_cap = config.env_cap.clone();
703        interp.env_overrides = (*config.env_values).clone();
704        interp.host_call_steps = config.host.call_steps;
705        // Loading is held to the call budget: a script that cannot finish
706        // evaluating its own module must not hang the host's startup, nor a
707        // reload (D36 §10).
708        let _meter = config.budget.begin(&mut interp);
709
710        let value = loader.eval_entry(&mut interp, &ImportSpec::File(&entry));
711        files = recording.seen.take();
712        let mut reports: Vec<Report> = loader.diags.iter().map(|d| to_report(d, cache)).collect();
713        let value = match value {
714            Ok(v) => v,
715            Err(d) => {
716                reports.insert(0, to_report(&d, cache));
717                return Err(reports.into());
718            }
719        };
720        if crate::analysis::has_blocking(&loader.diags, config.strict) {
721            return Err(reports.into());
722        }
723        let Value::Object(exports) = value else {
724            return Err(Error::host("E0001", "a module evaluates to an object"));
725        };
726        lock_after = Some(std::mem::take(&mut loader.lock));
727
728        Ok(State {
729            exports: (*exports).clone(),
730            registry,
731            fs,
732            env_cap: config.env_cap.clone(),
733            env_values: config.env_values.clone(),
734            host_call_steps: config.host.call_steps,
735            strict: config.strict,
736            _closures: interp,
737        })
738    });
739    Attempt {
740        result: result.map(|loaded| (loaded, lock_after.unwrap_or_default())),
741        files,
742        base,
743    }
744}
745
746/// Passes resolution through, and remembers every local file it was asked to
747/// load and what it read: the file set of a version (D36 §5).
748struct Recording<'r> {
749    inner: &'r dyn FileResolver,
750    seen: RefCell<Vec<(PathBuf, String)>>,
751}
752
753impl FileResolver for Recording<'_> {
754    fn resolve(
755        &self,
756        spec: &ImportSpec<'_>,
757        importer: Option<&ModuleId>,
758    ) -> Result<ModuleId, String> {
759        self.inner.resolve(spec, importer)
760    }
761
762    fn load(&self, id: &ModuleId) -> Result<String, String> {
763        let text = self.inner.load(id)?;
764        if let ModuleId::Local(path) = id {
765            self.seen.borrow_mut().push((path.clone(), text.clone()));
766        }
767        Ok(text)
768    }
769
770    fn dir_of(&self, id: &ModuleId) -> Option<PathBuf> {
771        self.inner.dir_of(id)
772    }
773}
774
775/// SHA-256 over every file's path and content, in path order.
776fn hash_files(files: &[(PathBuf, String)]) -> String {
777    use sha2::{Digest, Sha256};
778    let mut sorted: Vec<_> = files.iter().collect();
779    sorted.sort_by(|a, b| a.0.cmp(&b.0));
780    let mut h = Sha256::new();
781    for (path, text) in sorted {
782        h.update(path.to_string_lossy().as_bytes());
783        h.update([0]);
784        h.update(text.as_bytes());
785        h.update([0]);
786    }
787    h.finalize().iter().map(|b| format!("{b:02x}")).collect()
788}
789
790fn contract_of(loaded: &Loaded) -> Contract {
791    loaded.with_dependent(|_, s| {
792        s.exports
793            .iter()
794            .map(|(k, v)| {
795                let arity = match v {
796                    Value::Function(d) => Some(d.params.len()),
797                    _ => None,
798                };
799                (k.clone(), arity)
800            })
801            .collect()
802    })
803}
804
805/// D36 §2: everything the first version exported is still there, of the same
806/// kind and arity. New exports are fine - nothing the host bound can use them.
807fn check_contract(fixed: &Contract, new: &Contract) -> Result<(), Error> {
808    let describe = |k: &Option<usize>| match k {
809        Some(n) => format!(
810            "a function of {n} argument{}",
811            if *n == 1 { "" } else { "s" }
812        ),
813        None => "a value".to_string(),
814    };
815    for (name, kind) in fixed {
816        let msg = match new.iter().find(|(n, _)| n == name) {
817            None => format!("the new version no longer exports '{name}'"),
818            Some((_, k)) if k != kind => format!(
819                "'{name}' was {}, and the new version makes it {}",
820                describe(kind),
821                describe(k)
822            ),
823            Some(_) => continue,
824        };
825        let mut e = Error::host("E0011", msg);
826        e.reports[0].help = Some(
827            "the host may have bound it, so the reload was refused and the old version keeps \
828             serving; restart the application to change what the script exports"
829                .to_string(),
830        );
831        return Err(e);
832    }
833    Ok(())
834}
835
836/// The thread that watches a script's files (D36 §1). It holds the script only
837/// weakly and stops when the `Script` is dropped.
838struct Watcher {
839    stop: mpsc::Sender<()>,
840    thread: std::thread::JoinHandle<()>,
841}
842
843/// The handler `watch` requires (D36 §12).
844type OnReload = Box<dyn Fn(Result<ScriptVersion, Error>) + Send>;
845
846impl Watcher {
847    fn start(
848        shared: &Arc<Shared>,
849        interval: Duration,
850        settle: Duration,
851        on_reload: OnReload,
852    ) -> Watcher {
853        let (stop, stopped) = mpsc::channel();
854        let weak = Arc::downgrade(shared);
855        let thread = std::thread::Builder::new()
856            .name("aura-watch".into())
857            .spawn(move || watch_loop(weak, interval, settle, stopped, on_reload))
858            .expect("spawn the watcher thread");
859        Watcher { stop, thread }
860    }
861
862    fn stop(self) {
863        drop(self.stop);
864        // The handler may own the last handle on the script, and so drop it on
865        // the watcher's own thread: joining there would wait for itself.
866        if self.thread.thread().id() != std::thread::current().id() {
867            let _ = self.thread.join();
868        }
869    }
870}
871
872/// Waits one interval; false when the script is gone and the watcher must end.
873fn tick(stopped: &mpsc::Receiver<()>, interval: Duration) -> bool {
874    matches!(
875        stopped.recv_timeout(interval),
876        Err(mpsc::RecvTimeoutError::Timeout)
877    )
878}
879
880/// What the file system says about the files without reading them: a cheap
881/// reason to look, never a reason to reload (D36 §1).
882type Stats = Vec<Option<(u64, Option<std::time::SystemTime>)>>;
883
884fn stats(files: &[(PathBuf, String)]) -> Stats {
885    files
886        .iter()
887        .map(|(p, _)| {
888            std::fs::metadata(p)
889                .ok()
890                .map(|m| (m.len(), m.modified().ok()))
891        })
892        .collect()
893}
894
895/// Every file read again, with the file system's word that nothing wrote to
896/// them while they were read: size and modification time the same before and
897/// after. `None` if one can no longer be read, or something wrote meanwhile.
898fn read_still(files: &[(PathBuf, String)]) -> Option<(Stats, Vec<(PathBuf, String)>)> {
899    let before = stats(files);
900    let read = reread(files)?;
901    (stats(files) == before).then_some((before, read))
902}
903
904/// Every file read again; `None` if one can no longer be read.
905fn reread(files: &[(PathBuf, String)]) -> Option<Vec<(PathBuf, String)>> {
906    files
907        .iter()
908        .map(|(p, _)| std::fs::read_to_string(p).ok().map(|t| (p.clone(), t)))
909        .collect()
910}
911
912/// D36 §6: when the files may be read for a reload. Pure - no clock, no file
913/// system - so the rule is tested with made-up times, not with the luck of a
914/// scheduler.
915///
916/// The rule is a quiet period: the file system has reported the same size and
917/// modification time for every file for `settle`. Two reads that agree are not
918/// enough - a writer that stalls between truncating a file and filling it
919/// leaves one that reads the same, empty, for as long as it stalls; a writer
920/// cut off at the end of a rule leaves one that is valid and loads. A quiet
921/// period longer than any such stall is what keeps a half-written file out.
922#[derive(Debug)]
923struct Settle {
924    settle: Duration,
925    /// What the files looked like, and since when.
926    seen: Option<(Stats, Instant)>,
927}
928
929#[derive(Debug, PartialEq, Eq)]
930enum Step {
931    /// Something changed, or has not been still for long enough.
932    Wait,
933    /// Still for `settle`: read the files.
934    Look,
935}
936
937impl Settle {
938    fn new(settle: Duration) -> Self {
939        Settle { settle, seen: None }
940    }
941
942    fn observe(&mut self, now: Instant, stats: &Stats) -> Step {
943        match &self.seen {
944            Some((s, since)) if s == stats => {
945                if now.saturating_duration_since(*since) >= self.settle {
946                    Step::Look
947                } else {
948                    Step::Wait
949                }
950            }
951            _ => {
952                self.seen = Some((stats.clone(), now));
953                Step::Wait
954            }
955        }
956    }
957
958    /// Start counting again, from the next observation.
959    fn reset(&mut self) {
960        self.seen = None;
961    }
962}
963
964/// The settle period when the host does not set one: two intervals, and never
965/// less than a second - longer than any pause a writer takes between two
966/// writes on a busy machine, and short beside how often rules are edited.
967fn default_settle(interval: Duration) -> Duration {
968    (interval * 2).max(Duration::from_secs(1))
969}
970
971fn watch_loop(
972    shared: std::sync::Weak<Shared>,
973    interval: Duration,
974    settle: Duration,
975    stopped: mpsc::Receiver<()>,
976    on_reload: OnReload,
977) {
978    let mut gate = Settle::new(settle);
979    // The files and the state the serving version was read from: nothing to do
980    // while they look like this.
981    let mut quiet: Option<Stats> = None;
982    // Content already reported as failing, by the state it was read in.
983    let mut failed: Option<Stats> = None;
984    // The files to watch: the serving version's, or - once a load found new
985    // imports - the set that load read, so they settle too.
986    let mut tracked: Option<Vec<(PathBuf, String)>> = None;
987    loop {
988        if !tick(&stopped, interval) {
989            return;
990        }
991        let Some(script) = shared.upgrade() else {
992            return;
993        };
994        let current = script.current();
995        let files = tracked.clone().unwrap_or_else(|| current.files.clone());
996        let now = stats(&files);
997        if quiet.as_ref() == Some(&now) || failed.as_ref() == Some(&now) {
998            gate.reset();
999            continue;
1000        }
1001        if gate.observe(Instant::now(), &now) == Step::Wait {
1002            continue;
1003        }
1004        // Still for the whole settle period. Only content counts now: a touch
1005        // leaves the hash as it was, and nothing happens.
1006        let Some((read_stats, read)) = read_still(&files) else {
1007            gate.reset();
1008            continue;
1009        };
1010        if read_stats != now {
1011            gate.reset();
1012            continue;
1013        }
1014        if tracked.is_none() && hash_files(&read) == current.id.hash {
1015            quiet = Some(now);
1016            gate.reset();
1017            continue;
1018        }
1019        let attempt = script.attempt();
1020        drop(current);
1021
1022        // The load read the files once more. If it found a different set -
1023        // a new import - those files have not settled yet: watch the new set
1024        // and try again once it has.
1025        let same_set = attempt.files.len() == files.len()
1026            && attempt.files.iter().zip(&files).all(|(a, b)| a.0 == b.0);
1027        if !same_set {
1028            tracked = Some(attempt.files.clone());
1029            gate.reset();
1030            continue;
1031        }
1032        // And it must have read what was still - nothing written meanwhile.
1033        if attempt.files != read || stats(&attempt.files) != now {
1034            gate.reset();
1035            continue;
1036        }
1037        tracked = None;
1038        match script.install(attempt) {
1039            Ok(Some(v)) => {
1040                failed = None;
1041                quiet = None;
1042                on_reload(Ok(v));
1043            }
1044            Ok(None) => quiet = Some(now),
1045            Err(e) => {
1046                failed = Some(now);
1047                on_reload(Err(e));
1048            }
1049        }
1050        gate.reset();
1051    }
1052}
1053
1054/// D35 §6: a host function may not call back into a script. Re-entrancy across
1055/// the boundary is the class of defect behind the 2016 DAO exploit; a closure
1056/// can capture the script, so the type system cannot rule it out.
1057fn refuse_reentry() -> Result<(), Error> {
1058    if crate::host::inside_host() {
1059        return Err(Error::host(
1060            "E0010",
1061            "a script was called from inside one of its host functions",
1062        ));
1063    }
1064    Ok(())
1065}
1066
1067/// The functions the builder was given, checked as a whole: each named
1068/// `module.function`, none twice, none shadowed by a built-in object method,
1069/// and every name granted to imports one that exists.
1070fn host_table(b: &mut ScriptBuilder) -> Result<HostTable, Error> {
1071    let registry = MethodRegistry::builtin();
1072    let mut table = HostTable::new();
1073    if let Some(steps) = b.host_call_steps {
1074        table.call_steps = steps;
1075    }
1076    let ident = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1077    for mut f in std::mem::take(&mut b.host) {
1078        let Some((module, func)) = f.name.split_once('.').filter(|(m, n)| ident(m) && ident(n))
1079        else {
1080            return Err(Error::host(
1081                "E0001",
1082                format!(
1083                    "host function '{}' must be named module.function, like \"geo.region\"",
1084                    f.name
1085                ),
1086            ));
1087        };
1088        if table.get(module, func).is_some() {
1089            return Err(Error::host(
1090                "E0001",
1091                format!("host function '{}' is provided twice", f.name),
1092            ));
1093        }
1094        // Builtin methods win over an object's fields (D12), so `geo.keys()`
1095        // would never reach the host. Refused here rather than shadowed silently.
1096        if registry
1097            .get(crate::eval::value::TypeTag::Object, func)
1098            .is_some()
1099        {
1100            return Err(Error::host(
1101                "E0001",
1102                format!(
1103                    "host function '{}' is named like the built-in object method '{func}'",
1104                    f.name
1105                ),
1106            ));
1107        }
1108        f.for_imports = b.host_for_imports.contains(&f.name);
1109        table.insert(f);
1110    }
1111    for n in &b.host_for_imports {
1112        if !table.fns().any(|f| f.name == *n) {
1113            return Err(Error::host(
1114                "E0001",
1115                format!("grant_to_imports names '{n}', which is not provided"),
1116            ));
1117        }
1118    }
1119    Ok(table)
1120}
1121
1122/// A Rust closure that can be given to [`ScriptBuilder::pure`]: up to six
1123/// arguments that `Deserialize`, and a result that `Serialize`s.
1124pub trait PureFn<Args>: Send + Sync + 'static {
1125    const ARITY: usize;
1126    #[doc(hidden)]
1127    fn erase(self) -> Arc<HostCall>;
1128}
1129
1130/// A Rust closure that can be given to [`ScriptBuilder::effect`]: as
1131/// [`PureFn`], returning `Result<R, E>` where `E` explains the failure.
1132pub trait EffectFn<Args>: Send + Sync + 'static {
1133    const ARITY: usize;
1134    #[doc(hidden)]
1135    fn erase(self) -> Arc<HostCall>;
1136}
1137
1138/// A host result into an Aura value, by the same exact rules as arguments.
1139fn host_result<R: Serialize>(r: R) -> HostOutcome {
1140    match r.serialize(ToValue(PhantomData)) {
1141        Ok(v) => HostOutcome::Value(v),
1142        Err(e) => HostOutcome::BadArgument(format!("its result: {e}")),
1143    }
1144}
1145
1146macro_rules! host_fn {
1147    ($n:expr $(, $t:ident $v:ident $i:tt)*) => {
1148        impl<F, R, $($t),*> PureFn<($($t,)*)> for F
1149        where
1150            F: Fn($($t),*) -> R + Send + Sync + 'static,
1151            R: Serialize,
1152            $($t: DeserializeOwned,)*
1153        {
1154            const ARITY: usize = $n;
1155            fn erase(self) -> Arc<HostCall> {
1156                Arc::new(move |args: &[serde_json::Value]| {
1157                    $(
1158                        let $v: $t = match serde_json::from_value(args[$i].clone()) {
1159                            Ok(v) => v,
1160                            Err(e) => {
1161                                return HostOutcome::BadArgument(format!("argument {}: {e}", $i + 1))
1162                            }
1163                        };
1164                    )*
1165                    let _ = args;
1166                    host_result(self($($v),*))
1167                })
1168            }
1169        }
1170
1171        impl<F, R, Er, $($t),*> EffectFn<($($t,)*)> for F
1172        where
1173            F: Fn($($t),*) -> Result<R, Er> + Send + Sync + 'static,
1174            R: Serialize,
1175            Er: fmt::Display,
1176            $($t: DeserializeOwned,)*
1177        {
1178            const ARITY: usize = $n;
1179            fn erase(self) -> Arc<HostCall> {
1180                Arc::new(move |args: &[serde_json::Value]| {
1181                    $(
1182                        let $v: $t = match serde_json::from_value(args[$i].clone()) {
1183                            Ok(v) => v,
1184                            Err(e) => {
1185                                return HostOutcome::BadArgument(format!("argument {}: {e}", $i + 1))
1186                            }
1187                        };
1188                    )*
1189                    let _ = args;
1190                    match self($($v),*) {
1191                        Ok(r) => host_result(r),
1192                        Err(e) => HostOutcome::Failed(e.to_string()),
1193                    }
1194                })
1195            }
1196        }
1197    };
1198}
1199
1200host_fn!(0);
1201host_fn!(1, A a 0);
1202host_fn!(2, A a 0, B b 1);
1203host_fn!(3, A a 0, B b 1, C c 2);
1204host_fn!(4, A a 0, B b 1, C c 2, D d 3);
1205host_fn!(5, A a 0, B b 1, C c 2, D d 3, E e 4);
1206host_fn!(6, A a 0, B b 1, C c 2, D d 3, E e 4, G g 5);
1207
1208fn no_export(name: &str) -> Error {
1209    Error::host(
1210        "E0002",
1211        format!("the script exports nothing named '{name}'"),
1212    )
1213}
1214
1215fn from_json<T: DeserializeOwned>(json: serde_json::Value) -> Result<T, Error> {
1216    serde_path_to_error::deserialize(json).map_err(|e| {
1217        let path = e.path().to_string();
1218        Error::host(
1219            "E0006",
1220            format!(
1221                "the result does not match the host's type at {path}: {}",
1222                e.inner()
1223            ),
1224        )
1225    })
1226}
1227
1228/// A bound function: the checks are done, so a call only converts, runs and
1229/// converts back.
1230pub struct Function<'s, A, R> {
1231    script: &'s Script,
1232    name: String,
1233    _types: PhantomData<fn(A) -> R>,
1234}
1235
1236impl<A: Args, R: DeserializeOwned> Function<'_, A, R> {
1237    pub fn call(&self, args: A) -> Result<R, Error> {
1238        from_json(self.script.invoke(&self.name, args)?)
1239    }
1240}
1241
1242impl<A, R> fmt::Debug for Function<'_, A, R> {
1243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1244        f.debug_struct("Function")
1245            .field("name", &self.name)
1246            .finish()
1247    }
1248}
1249
1250impl fmt::Debug for Script {
1251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1252        let current = self.shared.current();
1253        let exports: Vec<String> = current
1254            .loaded
1255            .with_dependent(|_, s| s.exports.keys().cloned().collect());
1256        f.debug_struct("Script")
1257            .field("exports", &exports)
1258            .field("version", &current.id)
1259            .field("budget", &self.shared.config.budget)
1260            .finish()
1261    }
1262}
1263
1264/// Reads through a grant shared by every call of one script.
1265struct SharedFs(Arc<dyn FileAccess>);
1266
1267impl FileAccess for SharedFs {
1268    fn read(&self, path: &str) -> Result<String, FileError> {
1269        self.0.read(path)
1270    }
1271}
1272
1273// ---------------------------------------------------------------------------
1274// Arguments
1275// ---------------------------------------------------------------------------
1276
1277/// The arguments of a call: a tuple of `Serialize` values. The arity is a
1278/// constant, so binding a function can check it before any call is made.
1279pub trait Args {
1280    const ARITY: usize;
1281    #[doc(hidden)]
1282    fn to_values<'a>(&self) -> Result<Vec<Value<'a>>, BoundaryError>;
1283}
1284
1285macro_rules! args_tuple {
1286    ($n:expr $(, $t:ident $i:tt)*) => {
1287        impl<$($t: Serialize),*> Args for ($($t,)*) {
1288            const ARITY: usize = $n;
1289            #[allow(unused_variables)]
1290            fn to_values<'a>(&self) -> Result<Vec<Value<'a>>, BoundaryError> {
1291                Ok(vec![$(self.$i.serialize(ToValue(PhantomData))?),*])
1292            }
1293        }
1294    };
1295}
1296
1297args_tuple!(0);
1298args_tuple!(1, A 0);
1299args_tuple!(2, A 0, B 1);
1300args_tuple!(3, A 0, B 1, C 2);
1301args_tuple!(4, A 0, B 1, C 2, D 3);
1302args_tuple!(5, A 0, B 1, C 2, D 3, E 4);
1303args_tuple!(6, A 0, B 1, C 2, D 3, E 4, F 5);
1304
1305/// A host value Aura cannot hold exactly. Refused rather than coerced: an
1306/// integer wrapped, or a NaN turned into `null`, would be a different input
1307/// than the host meant to give.
1308#[derive(Debug)]
1309pub struct BoundaryError(String);
1310
1311impl fmt::Display for BoundaryError {
1312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1313        f.write_str(&self.0)
1314    }
1315}
1316
1317impl std::error::Error for BoundaryError {}
1318
1319impl serde::ser::Error for BoundaryError {
1320    fn custom<T: fmt::Display>(msg: T) -> Self {
1321        BoundaryError(msg.to_string())
1322    }
1323}
1324
1325/// Serialises a host value straight into an Aura value — no JSON detour, so no
1326/// precision is lost on the way and no NaN quietly becomes `null`.
1327struct ToValue<'a>(PhantomData<Value<'a>>);
1328
1329fn refuse<T>(what: impl fmt::Display) -> Result<T, BoundaryError> {
1330    Err(BoundaryError(format!("{what} has no exact Aura value")))
1331}
1332
1333impl<'a> serde::Serializer for ToValue<'a> {
1334    type Ok = Value<'a>;
1335    type Error = BoundaryError;
1336    type SerializeSeq = SeqToValue<'a>;
1337    type SerializeTuple = SeqToValue<'a>;
1338    type SerializeTupleStruct = SeqToValue<'a>;
1339    type SerializeTupleVariant = VariantToValue<SeqToValue<'a>>;
1340    type SerializeMap = MapToValue<'a>;
1341    type SerializeStruct = MapToValue<'a>;
1342    type SerializeStructVariant = VariantToValue<MapToValue<'a>>;
1343
1344    fn serialize_bool(self, v: bool) -> Result<Value<'a>, BoundaryError> {
1345        Ok(Value::Bool(v))
1346    }
1347    fn serialize_i8(self, v: i8) -> Result<Value<'a>, BoundaryError> {
1348        Ok(Value::Int(v.into()))
1349    }
1350    fn serialize_i16(self, v: i16) -> Result<Value<'a>, BoundaryError> {
1351        Ok(Value::Int(v.into()))
1352    }
1353    fn serialize_i32(self, v: i32) -> Result<Value<'a>, BoundaryError> {
1354        Ok(Value::Int(v.into()))
1355    }
1356    fn serialize_i64(self, v: i64) -> Result<Value<'a>, BoundaryError> {
1357        Ok(Value::Int(v))
1358    }
1359    fn serialize_i128(self, v: i128) -> Result<Value<'a>, BoundaryError> {
1360        i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1361    }
1362    fn serialize_u8(self, v: u8) -> Result<Value<'a>, BoundaryError> {
1363        Ok(Value::Int(v.into()))
1364    }
1365    fn serialize_u16(self, v: u16) -> Result<Value<'a>, BoundaryError> {
1366        Ok(Value::Int(v.into()))
1367    }
1368    fn serialize_u32(self, v: u32) -> Result<Value<'a>, BoundaryError> {
1369        Ok(Value::Int(v.into()))
1370    }
1371    fn serialize_u64(self, v: u64) -> Result<Value<'a>, BoundaryError> {
1372        i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1373    }
1374    fn serialize_u128(self, v: u128) -> Result<Value<'a>, BoundaryError> {
1375        i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1376    }
1377    fn serialize_f32(self, v: f32) -> Result<Value<'a>, BoundaryError> {
1378        self.serialize_f64(v.into())
1379    }
1380    fn serialize_f64(self, v: f64) -> Result<Value<'a>, BoundaryError> {
1381        if v.is_finite() {
1382            Ok(Value::Float(v))
1383        } else {
1384            refuse(v)
1385        }
1386    }
1387    fn serialize_char(self, v: char) -> Result<Value<'a>, BoundaryError> {
1388        Ok(Value::str(v.encode_utf8(&mut [0; 4])))
1389    }
1390    fn serialize_str(self, v: &str) -> Result<Value<'a>, BoundaryError> {
1391        Ok(Value::str(v))
1392    }
1393    fn serialize_bytes(self, _: &[u8]) -> Result<Value<'a>, BoundaryError> {
1394        refuse("a byte string")
1395    }
1396    fn serialize_none(self) -> Result<Value<'a>, BoundaryError> {
1397        Ok(Value::Null)
1398    }
1399    fn serialize_some<T: ?Sized + Serialize>(self, v: &T) -> Result<Value<'a>, BoundaryError> {
1400        v.serialize(self)
1401    }
1402    fn serialize_unit(self) -> Result<Value<'a>, BoundaryError> {
1403        Ok(Value::Null)
1404    }
1405    fn serialize_unit_struct(self, _: &'static str) -> Result<Value<'a>, BoundaryError> {
1406        Ok(Value::Null)
1407    }
1408    fn serialize_unit_variant(
1409        self,
1410        _: &'static str,
1411        _: u32,
1412        variant: &'static str,
1413    ) -> Result<Value<'a>, BoundaryError> {
1414        Ok(Value::str(variant))
1415    }
1416    fn serialize_newtype_struct<T: ?Sized + Serialize>(
1417        self,
1418        _: &'static str,
1419        v: &T,
1420    ) -> Result<Value<'a>, BoundaryError> {
1421        v.serialize(self)
1422    }
1423    fn serialize_newtype_variant<T: ?Sized + Serialize>(
1424        self,
1425        _: &'static str,
1426        _: u32,
1427        variant: &'static str,
1428        v: &T,
1429    ) -> Result<Value<'a>, BoundaryError> {
1430        let inner = v.serialize(ToValue(PhantomData))?;
1431        Ok(Value::object(IndexMap::from([(
1432            variant.to_string(),
1433            inner,
1434        )])))
1435    }
1436    fn serialize_seq(self, len: Option<usize>) -> Result<SeqToValue<'a>, BoundaryError> {
1437        Ok(SeqToValue(Vec::with_capacity(len.unwrap_or(0))))
1438    }
1439    fn serialize_tuple(self, len: usize) -> Result<SeqToValue<'a>, BoundaryError> {
1440        self.serialize_seq(Some(len))
1441    }
1442    fn serialize_tuple_struct(
1443        self,
1444        _: &'static str,
1445        len: usize,
1446    ) -> Result<SeqToValue<'a>, BoundaryError> {
1447        self.serialize_seq(Some(len))
1448    }
1449    fn serialize_tuple_variant(
1450        self,
1451        _: &'static str,
1452        _: u32,
1453        variant: &'static str,
1454        len: usize,
1455    ) -> Result<Self::SerializeTupleVariant, BoundaryError> {
1456        Ok(VariantToValue(variant, self.serialize_seq(Some(len))?))
1457    }
1458    fn serialize_map(self, len: Option<usize>) -> Result<MapToValue<'a>, BoundaryError> {
1459        Ok(MapToValue {
1460            map: IndexMap::with_capacity(len.unwrap_or(0)),
1461            key: None,
1462        })
1463    }
1464    fn serialize_struct(
1465        self,
1466        _: &'static str,
1467        len: usize,
1468    ) -> Result<MapToValue<'a>, BoundaryError> {
1469        self.serialize_map(Some(len))
1470    }
1471    fn serialize_struct_variant(
1472        self,
1473        _: &'static str,
1474        _: u32,
1475        variant: &'static str,
1476        len: usize,
1477    ) -> Result<Self::SerializeStructVariant, BoundaryError> {
1478        Ok(VariantToValue(variant, self.serialize_map(Some(len))?))
1479    }
1480}
1481
1482struct SeqToValue<'a>(Vec<Value<'a>>);
1483
1484impl<'a> SeqToValue<'a> {
1485    fn push<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1486        self.0.push(v.serialize(ToValue(PhantomData))?);
1487        Ok(())
1488    }
1489}
1490
1491impl<'a> serde::ser::SerializeSeq for SeqToValue<'a> {
1492    type Ok = Value<'a>;
1493    type Error = BoundaryError;
1494    fn serialize_element<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1495        self.push(v)
1496    }
1497    fn end(self) -> Result<Value<'a>, BoundaryError> {
1498        Ok(Value::list(self.0))
1499    }
1500}
1501
1502impl<'a> serde::ser::SerializeTuple for SeqToValue<'a> {
1503    type Ok = Value<'a>;
1504    type Error = BoundaryError;
1505    fn serialize_element<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1506        self.push(v)
1507    }
1508    fn end(self) -> Result<Value<'a>, BoundaryError> {
1509        Ok(Value::list(self.0))
1510    }
1511}
1512
1513impl<'a> serde::ser::SerializeTupleStruct for SeqToValue<'a> {
1514    type Ok = Value<'a>;
1515    type Error = BoundaryError;
1516    fn serialize_field<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1517        self.push(v)
1518    }
1519    fn end(self) -> Result<Value<'a>, BoundaryError> {
1520        Ok(Value::list(self.0))
1521    }
1522}
1523
1524struct MapToValue<'a> {
1525    map: IndexMap<String, Value<'a>>,
1526    key: Option<String>,
1527}
1528
1529impl<'a> MapToValue<'a> {
1530    fn finish(self) -> Value<'a> {
1531        Value::object(self.map)
1532    }
1533}
1534
1535impl<'a> serde::ser::SerializeMap for MapToValue<'a> {
1536    type Ok = Value<'a>;
1537    type Error = BoundaryError;
1538    fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), BoundaryError> {
1539        match key.serialize(ToValue(PhantomData))? {
1540            Value::Str(s) => {
1541                self.key = Some(s.to_string());
1542                Ok(())
1543            }
1544            other => refuse(format!("a map key of type {}", other.type_name())),
1545        }
1546    }
1547    fn serialize_value<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1548        let key = self.key.take().expect("serde calls serialize_key first");
1549        self.map.insert(key, v.serialize(ToValue(PhantomData))?);
1550        Ok(())
1551    }
1552    fn end(self) -> Result<Value<'a>, BoundaryError> {
1553        Ok(self.finish())
1554    }
1555}
1556
1557impl<'a> serde::ser::SerializeStruct for MapToValue<'a> {
1558    type Ok = Value<'a>;
1559    type Error = BoundaryError;
1560    fn serialize_field<T: ?Sized + Serialize>(
1561        &mut self,
1562        key: &'static str,
1563        v: &T,
1564    ) -> Result<(), BoundaryError> {
1565        self.map
1566            .insert(key.to_string(), v.serialize(ToValue(PhantomData))?);
1567        Ok(())
1568    }
1569    fn end(self) -> Result<Value<'a>, BoundaryError> {
1570        Ok(self.finish())
1571    }
1572}
1573
1574/// An enum variant carrying data: `{ variant: data }`, as serde's externally
1575/// tagged default.
1576struct VariantToValue<S>(&'static str, S);
1577
1578impl<'a> serde::ser::SerializeTupleVariant for VariantToValue<SeqToValue<'a>> {
1579    type Ok = Value<'a>;
1580    type Error = BoundaryError;
1581    fn serialize_field<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1582        self.1.push(v)
1583    }
1584    fn end(self) -> Result<Value<'a>, BoundaryError> {
1585        Ok(Value::object(IndexMap::from([(
1586            self.0.to_string(),
1587            Value::list(self.1 .0),
1588        )])))
1589    }
1590}
1591
1592impl<'a> serde::ser::SerializeStructVariant for VariantToValue<MapToValue<'a>> {
1593    type Ok = Value<'a>;
1594    type Error = BoundaryError;
1595    fn serialize_field<T: ?Sized + Serialize>(
1596        &mut self,
1597        key: &'static str,
1598        v: &T,
1599    ) -> Result<(), BoundaryError> {
1600        serde::ser::SerializeStruct::serialize_field(&mut self.1, key, v)
1601    }
1602    fn end(self) -> Result<Value<'a>, BoundaryError> {
1603        Ok(Value::object(IndexMap::from([(
1604            self.0.to_string(),
1605            self.1.finish(),
1606        )])))
1607    }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612    // D36 §6, decided without a clock or a file system: each case is a story
1613    // of what the file system reported and when, and what the watcher may do.
1614    mod settle {
1615        use super::super::{Settle, Stats, Step};
1616        use std::time::{Duration, Instant, SystemTime};
1617
1618        fn file(len: u64, mtime_ms: u64) -> Stats {
1619            vec![Some((
1620                len,
1621                Some(SystemTime::UNIX_EPOCH + Duration::from_millis(mtime_ms)),
1622            ))]
1623        }
1624
1625        /// Feeds `(ms, stats)` observations and returns the steps.
1626        fn run(settle_ms: u64, story: &[(u64, Stats)]) -> Vec<Step> {
1627            let t0 = Instant::now();
1628            let mut g = Settle::new(Duration::from_millis(settle_ms));
1629            story
1630                .iter()
1631                .map(|(ms, s)| g.observe(t0 + Duration::from_millis(*ms), s))
1632                .collect()
1633        }
1634
1635        #[test]
1636        fn files_left_alone_for_the_settle_period_are_read() {
1637            let s = file(100, 5);
1638            let steps = run(1000, &[(0, s.clone()), (500, s.clone()), (1000, s)]);
1639            assert_eq!(steps, [Step::Wait, Step::Wait, Step::Look]);
1640        }
1641
1642        /// The CI case: a writer stalls between truncating and filling for
1643        /// longer than two short intervals. The file reads empty and still -
1644        /// but not for the settle period, so it is never read.
1645        #[test]
1646        fn a_writer_stalled_mid_write_is_not_read() {
1647            let empty = file(0, 40);
1648            let full = file(100, 90);
1649            let steps = run(
1650                1000,
1651                &[
1652                    (40, empty.clone()),
1653                    (60, empty.clone()),
1654                    (80, empty),
1655                    (100, full),
1656                ],
1657            );
1658            assert!(!steps.contains(&Step::Look), "{steps:?}");
1659        }
1660
1661        /// A writer cut off at the end of a rule leaves a valid file. Still
1662        /// for 600 ms of a 1 s settle, then the rest arrives: never read.
1663        #[test]
1664        fn a_valid_prefix_that_does_not_last_is_not_read() {
1665            let prefix = file(60, 10);
1666            let whole = file(100, 610);
1667            let mut story: Vec<(u64, Stats)> =
1668                (0..=6).map(|i| (10 + i * 100, prefix.clone())).collect();
1669            story.push((610, whole.clone()));
1670            story.push((1600, whole.clone()));
1671            story.push((1610, whole));
1672            let steps = run(1000, &story);
1673            let looks: Vec<_> = steps
1674                .iter()
1675                .enumerate()
1676                .filter(|(_, s)| **s == Step::Look)
1677                .collect();
1678            assert_eq!(looks.len(), 1, "{steps:?}");
1679            assert_eq!(
1680                looks[0].0,
1681                story.len() - 1,
1682                "read before the whole file settled"
1683            );
1684        }
1685
1686        /// Any change - size or modification time - starts the count again.
1687        #[test]
1688        fn every_change_restarts_the_count() {
1689            let steps = run(
1690                1000,
1691                &[
1692                    (0, file(10, 0)),
1693                    (900, file(10, 900)),  // touched: mtime moved
1694                    (1800, file(11, 900)), // grew
1695                    (2700, file(11, 900)),
1696                    (2800, file(11, 900)),
1697                ],
1698            );
1699            assert_eq!(
1700                steps,
1701                [Step::Wait, Step::Wait, Step::Wait, Step::Wait, Step::Look]
1702            );
1703        }
1704
1705        #[test]
1706        fn reset_starts_over() {
1707            let s = file(1, 1);
1708            let t0 = Instant::now();
1709            let mut g = Settle::new(Duration::from_millis(100));
1710            g.observe(t0, &s);
1711            g.reset();
1712            assert_eq!(g.observe(t0 + Duration::from_millis(200), &s), Step::Wait);
1713            assert_eq!(g.observe(t0 + Duration::from_millis(300), &s), Step::Look);
1714        }
1715
1716        #[test]
1717        fn the_default_is_two_intervals_and_at_least_a_second() {
1718            use super::super::default_settle;
1719            assert_eq!(
1720                default_settle(Duration::from_millis(20)),
1721                Duration::from_secs(1)
1722            );
1723            assert_eq!(
1724                default_settle(Duration::from_secs(2)),
1725                Duration::from_secs(4)
1726            );
1727        }
1728    }
1729
1730    use super::*;
1731
1732    #[test]
1733    fn a_script_is_send_and_sync() {
1734        fn check<T: Send + Sync>() {}
1735        check::<Script>();
1736    }
1737
1738    #[test]
1739    fn diagnostics_render_as_the_cli_does() {
1740        let e = Error::host("E0002", "the script exports nothing named 'g'");
1741        assert_eq!(e.code(), "E0002");
1742        assert!(e.to_string().contains("E0002"));
1743    }
1744}