Skip to main content

cove_runtime/
host.rs

1//! The Host API boundary.
2//!
3//! Cove code has no ambient authority. Files, network, clocks, processes, and
4//! databases are explicit capabilities with replaceable real, fake, filtered,
5//! or denied implementations. The runtime rejects Host API calls that were not
6//! granted.
7//!
8//! This module holds the boundary itself — [`HostApi`], [`Grants`], and
9//! [`HostRegistry`] — together with the three small modules that have nothing
10//! else to say: [`Console`], [`Env`], and [`Documents`]. A host with rules of
11//! its own gets a module of its own: [`crate::clock`], [`crate::files`],
12//! [`crate::http`], [`crate::process`], and [`crate::database`].
13//!
14//! Two things cross this boundary besides plain values. A [`ResourceHandle`]
15//! goes outwards: the host keeps a connection or a listening socket and
16//! Cove holds the name of it, so a later call names the resource the way a
17//! method names its receiver. A [`Reentry`] goes inwards: a host that was
18//! handed a Cove callback — a route's handler, a repeating timer's body, the
19//! work a timeout bounds — needs a way to run it, and this is the only one
20//! there is. Both are ADR 0013's.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::io::Write;
24use std::path::PathBuf;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28
29use cove_sema::Capability;
30
31use crate::budget::{Budget, Cancellation, Meter};
32use crate::error::RuntimeError;
33use crate::schema::{
34    Admits, Effect, Mismatch, ModuleSchema, OperationSchema, Part, ResourceSchema, TypeSchema,
35};
36use crate::trace::{HostOutcome, NullSink, RecordedValue, RunOutcome, TraceEvent, TraceSink};
37use crate::value::{Repr, Value};
38use crate::wallclock::Instant;
39
40/// One host-provided module, such as `console` or `env`.
41///
42/// A host is shared by every task of a run, so an operation is invoked
43/// through a shared reference and a host is `Send + Sync`. A host that needs
44/// mutable state of its own says so with a lock it owns, which is also what
45/// decides how much of it two tasks may do at once: `console` serializes its
46/// writes so a line is never torn, while `clock.sleep` holds nothing, so two
47/// tasks can wait at the same time instead of queueing behind each other.
48///
49/// # An operation that blocks
50///
51/// A host call is a hole in the run's safepoint chain. The interpreter checks
52/// fuel, the deadline, and cancellation at loop back edges, calls, and
53/// `await`, and a program sitting inside a host reaches none of the three;
54/// [`Budget::charge_host_call`] checks the deadline and the cancellation flag
55/// once more before dispatch, but that bounds when a call *starts*, not how
56/// long it runs. Nothing in the runtime can interrupt a host that is waiting
57/// in `accept` or `read`. So this is a contract the boundary states and each
58/// host keeps, rather than something the boundary can enforce.
59///
60/// An operation that waits must bound how long it waits. It polls in steps
61/// short enough that the run's controls are still responsive, asks the
62/// [`Reentry`] it was handed whether the run has been stopped
63/// ([`Reentry::is_cancelled`]) and how long it has left
64/// ([`Reentry::time_left`]) between steps, and holds no lock while it does —
65/// a host waiting under its own mutex blocks every other task that wants it.
66/// One total allowance covers a multi-part operation: a per-read timeout that
67/// starts again on every successful read bounds nothing, because a peer that
68/// makes slow progress can hold the call open forever. `http.Server.handle`
69/// is the worked example. It accepts by polling rather than blocking, gives
70/// the whole of one request a single deadline clamped by what the run has
71/// left, and answers "nothing more to serve" when the run is stopped, so the
72/// program's own loop ends and the budget reports the stop it owns.
73///
74/// An operation that genuinely cannot cooperate — a C library call with no
75/// timeout, a syscall that cannot be interrupted — must say so in its own
76/// documentation, so an embedder knows the run's deadline does not bound that
77/// call and can decide what to do about it. What is not acceptable is a host
78/// that blocks indefinitely and says nothing.
79///
80/// # Migrating from the five-accessor form
81///
82/// This trait used to ask a module to describe itself five times — `name()`,
83/// `capability()`, `schema()`, `types()`, and `resources()`. It asks once
84/// now, through [`HostApi::module_schema`], and the five are gone rather than
85/// defaulted: a defaulted accessor is an overridable one, and an overridable
86/// one is a second description of the module, which the checker and the
87/// boundary could then read differently. An implementation written against
88/// the old shape fails to compile with `not all trait items implemented`,
89/// which is the intended way to find out.
90///
91/// The migration is to delete all five and write the table they were reading
92/// from:
93///
94/// ```
95/// # use cove_runtime::error::RuntimeError;
96/// # use cove_runtime::host::HostApi;
97/// # use cove_runtime::schema::ModuleSchema;
98/// # use cove_runtime::value::Value;
99/// # struct Company;
100/// const COMPANY: ModuleSchema = ModuleSchema {
101///     name: "company",
102///     capability: "directory",
103///     operations: &[],
104///     types: &[],
105///     resources: &[],
106/// };
107///
108/// impl HostApi for Company {
109///     fn module_schema(&self) -> ModuleSchema {
110///         COMPANY
111///     }
112///
113///     fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
114///         # let _ = (op, args);
115///         // unchanged
116///         # Ok(Value::unit())
117///     }
118/// }
119/// ```
120///
121/// `name` is the string `name()` returned, `capability` is the string behind
122/// the `Capability` `capability()` returned, and the three slices are what
123/// `schema()`, `types()`, and `resources()` returned. `call`, `call_with`,
124/// and `call_resource` are untouched.
125///
126/// The one thing that gets harder is a schema assembled at run time. The old
127/// accessors handed back borrows of `self`, so a host could keep a `String`
128/// and a `Vec<OperationSchema>` and return references into itself;
129/// [`ModuleSchema`] is `Copy` with `'static` contents, so a module whose
130/// shape comes from configuration or a manifest builds its table once, leaks
131/// it, and hands out the same copy.
132///
133/// Putting a lifetime on [`ModuleSchema`] would not give the old form back.
134/// A module registered here is a `Box<dyn HostApi>`, which is
135/// `Box<dyn HostApi + 'static>` — a spawned task's thread holds the registry
136/// and `std::thread::Builder::spawn` takes a `'static` closure — so a host
137/// has nothing outside itself to borrow a schema from, and borrowing from a
138/// field beside another one of its own is a self-referential struct.
139/// [`ModuleSchema`]'s own documentation weighs that against the alternatives
140/// and spells the pattern out; `crates/cove-runtime/tests/embedding.rs` runs
141/// it end to end.
142pub trait HostApi: Send + Sync {
143    /// The whole of what this module declares about itself: the name Cove
144    /// source uses, the capability a host must grant for it, the operations
145    /// it exposes, the types it declares, and the kinds of resource it can
146    /// open.
147    ///
148    /// The schema is the module's declaration of itself: a host cannot
149    /// expose an operation without saying what it takes, what it produces,
150    /// what it costs the outside world, and whether its result may cross a
151    /// task boundary. The boundary holds every call to it, so an operation
152    /// arriving in `call` has already been checked against what is declared
153    /// here.
154    ///
155    /// One table rather than five methods, because this exact value is also
156    /// what the *checker* reads: `cove_sema::Compiler::with_host_schema`
157    /// takes a [`ModuleSchema`], so the description a run enforces and the
158    /// description `cove check` checked a call against are the same bytes
159    /// for an embedder's module as they already are for a shipped one. This
160    /// is the only way to ask a module what it declares — there is nothing
161    /// else on this trait to override instead, so a module cannot describe
162    /// itself one way to the boundary and another way to the checker.
163    fn module_schema(&self) -> ModuleSchema;
164
165    /// Invokes one operation.
166    ///
167    /// The default forwards to [`HostApi::call`], which is what a module that
168    /// never runs a Cove callback wants. A module that does — `clock.every`,
169    /// `http.Server.handle` — overrides this instead and leaves `call`
170    /// unreachable.
171    ///
172    /// `back` is the way into the program that made this call, and it is on
173    /// loan for the duration of the call and no longer. [`Reentry`] states
174    /// the whole of what may be done with it, and the parts an implementor is
175    /// most likely to get wrong are these: it may not be retained past this
176    /// return, it may be used as many times as the operation means, it may be
177    /// used from this thread only, and no lock this module owns may be held
178    /// while it is used, because the Cove code it runs may call this module
179    /// again.
180    fn call_with(
181        &self,
182        op: &str,
183        args: Vec<Value>,
184        back: &mut dyn Reentry,
185    ) -> Result<Value, RuntimeError> {
186        let _ = back;
187        self.call(op, args)
188    }
189
190    /// Invokes one operation.
191    fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError>;
192
193    /// Invokes one operation on a handle this module issued.
194    ///
195    /// A module that declares no resources can never be reached here, so the
196    /// default says so rather than inventing an answer.
197    ///
198    /// `back` is the same loan [`HostApi::call_with`] describes, under the
199    /// same rules, and the lock rule is sharper here than anywhere else: the
200    /// table of open resources is exactly the lock a module holds, and the
201    /// callback is exactly the code that may ask for a resource in it. Take
202    /// what the callback's work needs, release the guard, and reenter after.
203    fn call_resource(
204        &self,
205        handle: &ResourceHandle,
206        op: &str,
207        args: Vec<Value>,
208        back: &mut dyn Reentry,
209    ) -> Result<Value, RuntimeError> {
210        let _ = (args, back);
211        Err(RuntimeError::new(format!(
212            "host module `{}` issues no resource handles, so `{}.{op}` cannot be called",
213            self.module_schema().name,
214            handle.qualified_type()
215        )))
216    }
217}
218
219/// How a host runs a Cove callback it was handed.
220///
221/// A Host API call is a value in and a value out, which is enough until an
222/// operation is given work to do rather than data to act on. A route's
223/// handler, a repeating timer's body, and the block a timeout bounds are all
224/// Cove closures the host holds and has to run; without this they could be
225/// stored and never called.
226///
227/// The callback runs on the task that made the host call, on that task's own
228/// stack, charged to that task's budget. There is no second thread and no
229/// scheduler: a host that wants concurrency spawns nothing, because
230/// concurrency in Cove belongs to a task scope the program wrote.
231///
232/// It is also how a host asks about the run it is inside.
233/// [`Reentry::is_cancelled`] and [`Reentry::time_left`] answer the two
234/// questions an operation that waits has to keep asking, and they are the
235/// only way to ask them: a host holds no budget and no interpreter of its
236/// own. [`HostApi`] says what a host owes them.
237///
238/// And it is what the boundary itself asks one question of.
239/// [`Reentry::task`] says which task is calling, which nothing else at the
240/// boundary knows: a [`HostRegistry`] is shared by every thread of a run,
241/// while the way back borrows the interpreter of exactly one task. The answer
242/// goes on the call's trace event, so a trace of a run with concurrent tasks
243/// can be grouped by whose I/O each call was.
244///
245/// # For the current call, and no longer
246///
247/// A `&mut dyn Reentry` arrives with a lifetime of its own, shorter than the
248/// `&self` beside it, and `dyn Reentry` is neither `Send` nor `Sync`. So a
249/// host cannot put one in a field: a [`HostApi`] is `Send + Sync` and shared
250/// across the tasks of a run, and neither the lifetime nor the auto traits
251/// will let this through. Both refusals are deliberate rather than an
252/// accident of how the signature was written, and neither is worth working
253/// around with an `Arc` or a raw pointer. Behind the reference is a borrow of
254/// the interpreter running the task that made this call. It is valid while
255/// that call is on the stack, and once the call returns the interpreter has
256/// moved on: a retained one would name a stack frame the task has left, and
257/// would run Cove code on a task that is doing something else.
258///
259/// A host that wants work done later does not keep the way back. It keeps
260/// the callback — a [`Value`] is an ordinary owned value — and asks for
261/// another call, which is what `http.Server.handle` is and why the loop
262/// around it is written in Cove.
263///
264/// # As many times as the operation means
265///
266/// A host may call its callback none, once, or many times. `clock.every`
267/// calls it once a period until the timer's task is cancelled;
268/// `http.Server.handle` calls it once, and not at all when no request
269/// arrived. Nothing here counts invocations and nothing makes the second one
270/// cheaper than the first.
271///
272/// Each one is a call the run pays for in full. Fuel is charged at the
273/// callback's own safepoints, its calls count against the run's call-depth
274/// limit while it is on the stack, and the run's deadline and cancellation
275/// stop it wherever they would stop any other Cove code. A host that loops
276/// therefore does not have to police the run: a body that would overrun the
277/// budget stops of its own accord and the error comes back out of
278/// [`Reentry::call`]. What the host owes is to stop looping when it is told
279/// to — [`Reentry::is_cancelled`] between rounds — rather than to keep
280/// starting rounds that will all fail.
281///
282/// # Nested, up to a bound
283///
284/// A callback is Cove code, so it may call any host the run granted,
285/// including this one, and that host may in turn be handed work. The nesting
286/// is real: the second host call builds a second way back further down the
287/// same native stack, and it reenters the same interpreter, so the inner
288/// callback sees the same task, the same heap, and the same budget as the
289/// outer one.
290///
291/// How deep it may go is a runtime control, like recursion depth and for the
292/// same reason. Two bound it. The Cove frames a callback makes count against
293/// the run's call-depth limit, since they are ordinary calls. And the number
294/// of host calls running a callback that may be stacked on one thread is
295/// bounded separately and much lower, because between one callback's frame
296/// and the next sits however much native stack the host chose to use, which
297/// nothing can measure. Past that bound the next reentry is refused with a
298/// [`RuntimeError`]; the run stops, rather than the process. It is a bound
299/// and not a proof: a host that uses an enormous amount of stack before it
300/// reenters can still exhaust it at the first level, and that is the host's
301/// responsibility, not the boundary's.
302///
303/// # One at a time, on the calling thread
304///
305/// A host may not call back from a thread of its own, and cannot: there is
306/// exactly one `&mut dyn Reentry` per host call and it is neither `Send` nor
307/// `Sync`, so two threads cannot hold it and it cannot be moved to one. This
308/// is the design's answer and not a limitation waiting to be lifted.
309/// Concurrency in Cove belongs to a task scope the program wrote; a host that
310/// ran a program's code on threads the program never asked for would be
311/// deciding how much of that program runs at once, and would be doing it
312/// outside every control the run was given.
313///
314/// # No lock may be held across it
315///
316/// A host must not hold a resource mutex, or any other non-reentrant lock,
317/// while it calls a callback. The callback is Cove code and Cove code may
318/// call this same host again; a `std::sync::Mutex` is not reentrant, so the
319/// second call would deadlock the task on a lock the first call is holding
320/// three frames up its own stack. Nothing detects this, because from the
321/// lock's point of view nothing is wrong.
322///
323/// The shape that works is to take what the callback's work needs while the
324/// lock is held, release it, and then reenter. `http`'s `Server.handle` is
325/// the worked example: it takes the next request, or a clone of the listening
326/// socket, out from under the table of open listeners, drops the guard, and
327/// only then runs the route's handler — which is free to call `http.listen`
328/// again, or to serve on the same handle.
329///
330/// # Reentry is not task transfer
331///
332/// Nothing crosses a task boundary here, so the rules that govern one do not
333/// apply. A callback's arguments are handed from a host to the interpreter of
334/// the task that called it, and its result comes back the same way, both on
335/// one thread, both belonging to one task throughout. [`crate::task::Transfer`]
336/// is not consulted and cannot be: an argument that may not cross a task
337/// boundary — a `Vector`, a resource handle whose schema says
338/// `task_safe: false` — is a perfectly ordinary argument to a callback, and a
339/// callback may answer with one.
340///
341/// Two things nearby do belong to tasks, and it is worth saying which.
342/// [`ResourceSchema::task_safe`] still decides whether the handle an
343/// operation *returns* may later be captured by a `spawn`; that is a question
344/// about the value, asked at the boundary the value eventually crosses, and
345/// reentry is not that boundary. And a callback that is an `async fn` answers
346/// with a task, which the implementation settles before handing the value
347/// back — the host was given a callback rather than a task, so this is what
348/// `await` would have done at the call site the host is standing in for. That
349/// settle is a join, and a value coming back out of a *spawned* task does
350/// cross a boundary and is checked; a settled `async fn` body ran on this
351/// thread and crosses nothing.
352pub trait Reentry {
353    /// Calls `callee` with `args` and answers what it produced.
354    ///
355    /// The call runs to completion before this returns, on this thread. An
356    /// `Err` is what the callback failed with, or what stopped the run while
357    /// it was running — exhausted fuel, an expired deadline, a raised
358    /// cancellation, a depth limit — and a host that receives one has nothing
359    /// useful to add: pass it on, so the reason the run stopped reaches the
360    /// caller as the runtime wrote it.
361    ///
362    /// No lock the host owns may be held across this call. See the trait's
363    /// documentation for why, and for what a host may and may not do with
364    /// the way back it was handed.
365    fn call(&mut self, callee: &Value, args: Vec<Value>) -> Result<Value, RuntimeError>;
366
367    /// Calls `callee` with `args`, stopping it at its next safepoint if
368    /// `stop` is raised while it runs.
369    ///
370    /// This is how a timeout is a timeout rather than a measurement taken
371    /// afterwards: the body observes the flag exactly where it observes its
372    /// own task's cancellation, and stops there.
373    ///
374    /// `stop` bounds this call and everything inside it, including a further
375    /// host call the body makes and any callback that host runs in turn — a
376    /// bound that a nested call escaped would not be a bound. It adds to the
377    /// reasons the body may stop and replaces none of them: the run's own
378    /// cancellation and deadline still apply, and a body stopped by one of
379    /// those is not stopped by this. A caller that needs to tell the two
380    /// apart reads `stop` afterwards, which is what `clock.timeout` does to
381    /// decide whether to report its bound or the error it was given.
382    fn call_until(
383        &mut self,
384        callee: &Value,
385        args: Vec<Value>,
386        stop: &Cancellation,
387    ) -> Result<Value, RuntimeError>;
388
389    /// Whether the work that made this host call has been asked to stop.
390    ///
391    /// This is everything a safepoint in Cove code would answer to: the run's
392    /// own cancellation, the task's, and the flag of any bounded call this
393    /// one is nested inside — a blocking call made from the body of a
394    /// `clock.timeout` is inside that bound as much as any Cove statement is.
395    ///
396    /// A host that loops or waits reads this between rounds and gives up when
397    /// it is raised: `clock.every` ends the timer rather than leaving it
398    /// running with nobody waiting, and `http.Server.handle` stops waiting
399    /// for a connection nobody is going to make. Giving up means answering
400    /// whatever the operation's own "nothing happened" is. The stop belongs
401    /// to the runtime, which reports it at the next safepoint with the limit
402    /// that was configured; a host that raised an error of its own would be
403    /// answering a question it was not asked.
404    fn is_cancelled(&self) -> bool;
405
406    /// How long the run that made this host call has before its deadline
407    /// expires.
408    ///
409    /// `None` means the run has no deadline and nothing here bounds it.
410    /// `Some(Duration::ZERO)` means the deadline has passed, and is as much a
411    /// reason to stop as [`Reentry::is_cancelled`] answering true.
412    ///
413    /// A host that waits reads this for two things. It stops when the answer
414    /// reaches zero, the same way it stops when the run is cancelled. And it
415    /// clamps its own timeouts by it, so an operation willing to wait thirty
416    /// seconds for a peer does not sit there for thirty seconds on behalf of
417    /// a run that had two hundred milliseconds left: the shorter of the two
418    /// allowances is the one that is honest.
419    ///
420    /// The answer is a duration rather than an instant because that is what a
421    /// host does with it — pass it to a socket timeout, or compare it against
422    /// zero — and because a run's deadline is measured from when the run
423    /// started, which is the budget's business and not the host's.
424    fn time_left(&self) -> Option<Duration>;
425
426    /// Which task made this host call: the innermost spawned task's id, or
427    /// [`crate::runtime::ENTRY_TASK`] when the call came from the entry.
428    ///
429    /// Nothing else can answer it. A [`HostRegistry`] is shared by every
430    /// thread of a run and knows nothing about who is calling; the way back is
431    /// the one thing at the boundary that belongs to one task, because it
432    /// borrows that task's interpreter. So the boundary asks it, and writes
433    /// the answer on the call's trace event — which is what lets a trace of a
434    /// run with concurrent tasks be grouped by whose I/O each call was.
435    ///
436    /// A host is not expected to do anything with this. It is asked once per
437    /// call, before the operation is dispatched, and a host that reads it is
438    /// reading an identity rather than a capability: knowing which task is
439    /// calling grants nothing, and two calls from one task are as unrelated
440    /// as any other two.
441    fn task(&self) -> u64;
442}
443
444/// A [`Reentry`] for a caller that has no interpreter to reenter.
445///
446/// [`HostRegistry::call`] uses it, so a host that never runs a callback can
447/// still be driven from a test or a tool with no program behind it. An
448/// operation that does need one is told what is missing rather than being
449/// handed a closure it cannot run.
450pub struct NoReentry;
451
452impl Reentry for NoReentry {
453    fn call(&mut self, callee: &Value, _args: Vec<Value>) -> Result<Value, RuntimeError> {
454        Err(RuntimeError::new(format!(
455            "this host call cannot run {}, because it was not made from a running program",
456            callee.type_name()
457        )))
458    }
459
460    fn call_until(
461        &mut self,
462        callee: &Value,
463        args: Vec<Value>,
464        _stop: &Cancellation,
465    ) -> Result<Value, RuntimeError> {
466        self.call(callee, args)
467    }
468
469    fn is_cancelled(&self) -> bool {
470        false
471    }
472
473    /// A caller with no run behind it has no deadline to run out of, so a
474    /// host driven from a test or a tool waits for as long as its own limits
475    /// allow and no less.
476    fn time_left(&self) -> Option<Duration> {
477        None
478    }
479
480    /// A caller with no program behind it made the call the way an entry
481    /// makes one: outside any spawned task.
482    fn task(&self) -> u64 {
483        crate::runtime::ENTRY_TASK
484    }
485}
486
487/// The identity of one resource a host owns.
488///
489/// ADR 0013: a handle is a name. Every field here is part of the name and
490/// none of them is state — `module` and `type_name` say what kind of thing is
491/// named, `id` says which one, and `task_safe` is the schema's answer copied
492/// onto the handle so the task boundary can read it without a registry.
493///
494/// That is why a handle is [`Arc`]-shared and immutable: copying one copies a
495/// name, two tasks holding it name the same resource, and a trace that
496/// records it records something a replay can reproduce exactly.
497#[derive(Clone, Debug, PartialEq, Eq)]
498pub struct ResourceHandle {
499    /// The host module that issued this handle, such as `database`.
500    pub module: String,
501    /// The resource kind, such as `Connection`.
502    pub type_name: String,
503    /// Which resource of that kind, unique among the ones this host issued.
504    pub id: u64,
505    /// Whether this handle may cross a task boundary, copied from the
506    /// resource's [`ResourceSchema`] when the handle was issued.
507    pub task_safe: bool,
508}
509
510impl ResourceHandle {
511    /// Issues a handle for resource `id` of kind `resource` in `module`.
512    pub fn new(module: &str, resource: &ResourceSchema, id: u64) -> Arc<ResourceHandle> {
513        Arc::new(ResourceHandle {
514            module: module.to_string(),
515            type_name: resource.name.to_string(),
516            id,
517            task_safe: resource.task_safe,
518        })
519    }
520
521    /// The type as Cove source writes it: `database.Connection`.
522    pub fn qualified_type(&self) -> String {
523        format!("{}.{}", self.module, self.type_name)
524    }
525
526    /// Whether two handles name the same resource.
527    pub fn names_same(&self, other: &ResourceHandle) -> bool {
528        self.module == other.module && self.type_name == other.type_name && self.id == other.id
529    }
530}
531
532/// A handle shows as the name it is: the type, and which one.
533impl std::fmt::Display for ResourceHandle {
534    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535        write!(f, "{}#{}", self.qualified_type(), self.id)
536    }
537}
538
539/// The set of capabilities granted at the execution boundary.
540#[derive(Clone, Debug, Default)]
541pub struct Grants {
542    granted: BTreeSet<Capability>,
543}
544
545impl Grants {
546    pub fn new(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
547        Grants {
548            granted: names.into_iter().map(Capability::new).collect(),
549        }
550    }
551
552    pub fn allows(&self, capability: &Capability) -> bool {
553        self.granted.contains(capability)
554    }
555
556    pub fn iter(&self) -> impl Iterator<Item = &Capability> {
557        self.granted.iter()
558    }
559}
560
561/// Where a run's grants came from, and so what a reader has to change to
562/// widen them.
563///
564/// A `cove run` reads `[run.<name>] allow` every time it starts, so editing
565/// that table is the answer. A binary `cove build` produced carries the grant
566/// set it was built with and reads no configuration at all, so the answer
567/// there is to change the table and build again — advice to edit a
568/// `cove.toml` would be advice that does nothing.
569#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
570pub enum GrantSource {
571    /// The `[run.<name>] allow` table this run read when it started.
572    #[default]
573    Config,
574    /// The grant set baked into a built binary.
575    Sealed,
576}
577
578/// Holds every host module available to a run, and the grants that gate them.
579///
580/// `HostRegistry::call` is the single choke point through which Cove code
581/// reaches external authority, so it is also the right place to observe that
582/// authority being exercised: an optional [`Budget`] charges every call
583/// against the run's host-call limit before dispatch, and an optional
584/// [`TraceSink`] records every call, granted or denied, with how long it
585/// took.
586pub struct HostRegistry {
587    modules: Vec<Box<dyn HostApi>>,
588    grants: Grants,
589    grant_source: GrantSource,
590    trace: Arc<dyn TraceSink>,
591    /// The run's budget, shared by every task: ADR 0008 draws a task's fuel
592    /// from the run's budget rather than giving each task one of its own, so
593    /// there is still exactly one authoritative count of what the run spent.
594    budget: Mutex<Option<Budget>>,
595    irreversible_writes: AtomicU64,
596}
597
598/// What one dispatch is addressed to: a module's operation, or a handle's.
599///
600/// The two differ only in how they are named, so this carries the naming and
601/// [`HostRegistry::dispatch`] carries the rules.
602struct Callee {
603    /// The host module, which is what a trace records and what a grant gates.
604    module: String,
605    /// The operation as the trace records it: `query` for a module's, and
606    /// `Connection.query` for a handle's.
607    op: String,
608    /// The handle the call was made on, for a handle's operation.
609    ///
610    /// A trace records it as the call's first argument, so a run holding two
611    /// connections records which one each query went to — and a replay can
612    /// tell them apart.
613    receiver: Option<Value>,
614    /// What has the operation, as a diagnostic names it.
615    owner: String,
616    /// Every operation the owner has, for the help when this one is not among
617    /// them.
618    known: Vec<&'static str>,
619}
620
621impl Callee {
622    /// The call as Cove source writes it: `database.query`, or
623    /// `database.Connection.query`.
624    fn shown(&self) -> String {
625        format!("{}.{}", self.module, self.op)
626    }
627
628    /// The operation's own name, without the resource that answers it.
629    fn bare_op(&self) -> &str {
630        match self.op.rsplit_once('.') {
631            Some((_, op)) => op,
632            None => &self.op,
633        }
634    }
635
636    /// The declared signature, qualified the way this callee is named.
637    fn signature(&self, schema: &OperationSchema) -> String {
638        match self.op.rsplit_once('.') {
639            Some((resource, _)) => format!("{resource}.{}", schema.signature()),
640            None => schema.signature(),
641        }
642    }
643}
644
645impl HostRegistry {
646    pub fn new(grants: Grants) -> Self {
647        HostRegistry {
648            modules: Vec::new(),
649            grants,
650            grant_source: GrantSource::default(),
651            trace: Arc::new(NullSink),
652            budget: Mutex::new(None),
653            irreversible_writes: AtomicU64::new(0),
654        }
655    }
656
657    pub fn register(&mut self, module: Box<dyn HostApi>) {
658        self.modules.push(module);
659    }
660
661    pub fn grants(&self) -> &Grants {
662        &self.grants
663    }
664
665    /// Records where these grants came from, which is what a refused call
666    /// tells the reader to change.
667    pub fn set_grant_source(&mut self, source: GrantSource) {
668        self.grant_source = source;
669    }
670
671    pub fn contains(&self, name: &str) -> bool {
672        self.modules.iter().any(|m| m.module_schema().name == name)
673    }
674
675    /// Installs where this registry's trace events go. Replaces any sink
676    /// installed earlier; the default is [`NullSink`], which discards
677    /// everything.
678    ///
679    /// This is the Host API boundary's own sink, and it carries exactly one
680    /// event: [`TraceEvent::HostCall`]. Everything else a run traces — task
681    /// lifecycle, a heap's summary, and the entry's own
682    /// [`TraceEvent::EntryEnter`], [`TraceEvent::EntryExit`] and
683    /// [`TraceEvent::RunEnded`] — goes through
684    /// [`Runtime::with_trace`](crate::Runtime::with_trace) instead, which
685    /// has a `NullSink` of its own to install into. An embedding that installs
686    /// only this one and only measures host calls will not notice; one that
687    /// expects a full tape from this alone gets an empty one for everything
688    /// but `HostCall`, with no error to say so.
689    pub fn set_trace(&mut self, sink: Arc<dyn TraceSink>) {
690        self.trace = sink;
691    }
692
693    /// Installs the budget every call is charged against. Replaces any
694    /// budget installed earlier; the default is no budget, which imposes no
695    /// host-call limit here (the interpreter's own safepoints still apply
696    /// its other limits).
697    ///
698    /// This arranges a registry before anything runs, and what it installs is
699    /// spent over every run the registry serves. That is exactly right for a
700    /// `cove run`, which is one run, and it is what `[run.<name>]`'s limits
701    /// come through. It is not what an embedding that invokes one compiled
702    /// program many times wants, because there the limits of every request
703    /// would add up over the life of the process:
704    /// [`Vm::invoke_within`](crate::Vm::invoke_within) and its three
705    /// siblings are how a single invocation is bounded instead.
706    pub fn set_budget(&mut self, budget: Budget) {
707        *self
708            .budget
709            .get_mut()
710            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(budget);
711    }
712
713    /// Installs `budget` for the run that is about to start, and starts its
714    /// deadline clock here.
715    ///
716    /// This is what makes a limit bound *one* invocation rather than the whole
717    /// life of a registry, which is issue #152. A `Budget` has to live where
718    /// every thread of a run can reach it — ADR 0008 draws a task's fuel from
719    /// the run's budget, and a task thread reaches this registry through the
720    /// `Arc<Runtime>` it holds — so it stays here; what changes is when it is
721    /// put here. [`HostRegistry::set_budget`] arranges a registry before
722    /// anything runs, and this replaces that arrangement for the duration of
723    /// one run and leaves what the run spent behind it, which is the same
724    /// state `cove run` reads its `--stats` out of.
725    ///
726    /// It takes `&self` where `set_budget` takes `&mut self`, and it is
727    /// `pub(crate)` because of it. ADR 0024 states each stop as a bound that
728    /// holds over a run, and a budget that could be swapped while the run it
729    /// bounds is executing would make every one of those bounds a claim about
730    /// a thing that had changed underneath it. So the only doors to this are
731    /// [`Vm::invoke_within`](crate::Vm::invoke_within) and its three
732    /// siblings, each of which takes `&mut self` on the backend: a backend
733    /// running an invocation is mutably borrowed for its whole duration, so a
734    /// second invocation on it cannot begin, and the shape rather than a rule
735    /// in a comment is what prevents the swap.
736    pub(crate) fn begin_run(&self, mut budget: Budget) {
737        budget.restart();
738        *self
739            .budget
740            .lock()
741            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(budget);
742    }
743
744    /// Runs `f` against the run's budget, if the host installed one.
745    ///
746    /// This is how a caller reads the counters after a run, how a host call
747    /// and a `spawn` are charged, and how a budget is looked at by anything
748    /// that has no [`Meter`] of its own. Every thread of a run reaches the one
749    /// budget through here, so the lock is held for the charge and nothing
750    /// else.
751    ///
752    /// It is *not* how a safepoint charges. That used to be exactly what this
753    /// was for, and issue #182 measured the mutex at 36% of `benches/call`
754    /// against the predecessor's `execute` at 46%, because every call and
755    /// every return is a safepoint. [`HostRegistry::budget_meter`] is what a
756    /// backend takes once per run instead, and [`Meter`] is where the
757    /// argument for it is.
758    pub fn with_budget<R>(&self, f: impl FnOnce(&Budget) -> R) -> Option<R> {
759        let budget = self
760            .budget
761            .lock()
762            .unwrap_or_else(|poisoned| poisoned.into_inner());
763        budget.as_ref().map(f)
764    }
765
766    /// The run's budget in the form a safepoint charges it, or `None` if the
767    /// host installed none.
768    ///
769    /// `None` means no budget at all, which is what an embedder that
770    /// installed none has, and what it has always meant here: no limit.
771    ///
772    /// This takes the lock once, and it is the last time a run touches it on
773    /// a per-instruction path: a [`Meter`] charges the same accounting over
774    /// atomics. A backend takes one where a run begins — see [`Meter`] for why
775    /// that is the only place it may be taken — and both of them do.
776    pub fn budget_meter(&self) -> Option<Meter> {
777        let budget = self
778            .budget
779            .lock()
780            .unwrap_or_else(|poisoned| poisoned.into_inner());
781        budget.as_ref().map(Budget::meter)
782    }
783
784    /// How many calls this run dispatched whose schema declares them
785    /// [`Effect::IrreversibleWrite`].
786    ///
787    /// This is what reads the `effect` an operation declares. Cove makes
788    /// irreversible operations require visible intent, so a run is able to
789    /// say how many of the things it did cannot be taken back; `cove run
790    /// --stats` prints the count. Whether each call actually reached the
791    /// outside world is the host's business rather than the registry's, so a
792    /// call the host answered with `Err` is still counted: the registry knows
793    /// only that a program asked for something irreversible.
794    pub fn irreversible_writes(&self) -> u64 {
795        self.irreversible_writes.load(Ordering::Relaxed)
796    }
797
798    /// The table every registered module declares itself with.
799    ///
800    /// This is the pairing the checker needs. An embedding registers its
801    /// hosts here and hands these same values to `cove_sema::Compiler`, so
802    /// the program is checked against the descriptions this registry is
803    /// about to enforce rather than against a second set written out beside
804    /// them:
805    ///
806    /// ```ignore
807    /// let program = Compiler::new()
808    ///     .with_host_schemas(hosts.module_schemas())
809    ///     .compile(&package)?;
810    /// ```
811    ///
812    /// A registry that has two modules registered under one name still
813    /// dispatches a call to only one of them: every lookup below that finds
814    /// a module by name — `contains`, `schema_for`, `host_type`, `call`,
815    /// `call_with`, `call_resource`, `module_for_operation` — takes the
816    /// first one registered. So this keeps only the first schema registered
817    /// under each name too, rather than handing the checker a second
818    /// description of a module the runtime will never reach through: the
819    /// list it hands back describes exactly what this registry dispatches
820    /// to, not everything that was ever registered.
821    pub fn module_schemas(&self) -> Vec<ModuleSchema> {
822        let mut seen = BTreeSet::new();
823        self.modules
824            .iter()
825            .map(|module| module.module_schema())
826            .filter(|schema| seen.insert(schema.name))
827            .collect()
828    }
829
830    /// Looks up which host module exposes `op`, for unqualified `use` imports.
831    pub fn module_for_operation(&self, op: &str) -> Option<&'static str> {
832        self.modules.iter().find_map(|m| {
833            let schema = m.module_schema();
834            schema
835                .operations
836                .iter()
837                .any(|entry| entry.name == op)
838                .then_some(schema.name)
839        })
840    }
841
842    /// The schema of one operation, if the module and the operation both
843    /// exist.
844    pub fn schema_for(&self, module: &str, op: &str) -> Option<&'static OperationSchema> {
845        self.modules
846            .iter()
847            .find(|m| m.module_schema().name == module)?
848            .module_schema()
849            .operations
850            .iter()
851            .find(|entry| entry.name == op)
852    }
853
854    /// The type `module.name` declares, if the module declares one.
855    ///
856    /// A [`TypeSchema`] is [`Copy`], so this hands back the entry itself
857    /// rather than a borrow of the registry: the interpreter that asks is
858    /// about to evaluate arguments, which it cannot do while holding one.
859    pub fn host_type(&self, module: &str, name: &str) -> Option<TypeSchema> {
860        self.modules
861            .iter()
862            .find(|m| m.module_schema().name == module)?
863            .module_schema()
864            .types
865            .iter()
866            .find(|declared| declared.name == name)
867            .copied()
868    }
869
870    /// Whether the value `module.op` produces may cross a task boundary, or
871    /// `None` when no such operation exists.
872    ///
873    /// The Language Card puts this decision in the schema rather than in the
874    /// value: "Host resources declare task-safety in their Host API schema."
875    pub fn result_is_task_safe(&self, module: &str, op: &str) -> Option<bool> {
876        Some(self.schema_for(module, op)?.result_is_task_safe)
877    }
878
879    /// Describes one call for a trace, or hands back nothing when no sink
880    /// will read it.
881    ///
882    /// The handle a resource operation was called on comes first, so a run
883    /// holding two connections records which one each query went to. A
884    /// [`RecordedValue`] is a copy, and one of a value no boundary may carry
885    /// is also a rendering of it, so an untraced run makes neither: an event
886    /// nothing keeps has nothing worth describing.
887    fn record_call(&self, callee: &Callee, args: &[Value]) -> Vec<RecordedValue> {
888        if !self.trace.is_recording() {
889            return Vec::new();
890        }
891        callee
892            .receiver
893            .iter()
894            .chain(args)
895            .map(RecordedValue::of)
896            .collect()
897    }
898
899    /// Dispatches a Host API call after checking the grant, the schema, and
900    /// the budget, tracing the outcome either way.
901    ///
902    /// This is the boundary's one choke point, and it takes no interpreter:
903    /// an operation that was handed a Cove callback cannot be reached through
904    /// it. [`HostRegistry::call_with`] is the same dispatch with a way back
905    /// into the program.
906    pub fn call(&self, module: &str, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
907        self.call_with(module, op, args, &mut NoReentry)
908    }
909
910    /// Dispatches a Host API call that may run a Cove callback it was given.
911    pub fn call_with(
912        &self,
913        module: &str,
914        op: &str,
915        args: Vec<Value>,
916        back: &mut dyn Reentry,
917    ) -> Result<Value, RuntimeError> {
918        let task = back.task();
919        let Some(entry) = self
920            .modules
921            .iter()
922            .find(|m| m.module_schema().name == module)
923        else {
924            return Err(RuntimeError::new(format!("unknown host module `{module}`"))
925                .with_outcome(RunOutcome::HostBoundary));
926        };
927        let schema = entry.module_schema();
928        let declared = schema
929            .operations
930            .iter()
931            .find(|entry| entry.name == op)
932            .copied();
933        // An operation declares the capability it needs; the module's own
934        // capability stands in for an operation that does not exist, so a
935        // call into an ungranted module is still reported as ungranted rather
936        // than as a misspelling.
937        let capability = match &declared {
938            Some(op_schema) => Capability::new(op_schema.capability),
939            None => Capability::new(schema.capability),
940        };
941        let callee = Callee {
942            module: module.to_string(),
943            op: op.to_string(),
944            receiver: None,
945            owner: format!("host module `{module}`"),
946            known: schema.operations.iter().map(|e| e.name).collect(),
947        };
948        self.dispatch(task, &callee, declared, capability, args, |args| {
949            entry.call_with(op, args, back)
950        })
951    }
952
953    /// Dispatches an operation on a resource handle, through the same gate
954    /// every other Host API call passes.
955    ///
956    /// A handle is a name, so nothing here trusts it: the module it names has
957    /// to exist, the resource kind has to be one that module declares, and
958    /// the operation has to be one that kind answers. A handle that outlived
959    /// what it named fails inside the host instead, which is where the only
960    /// record of what is still open lives.
961    pub fn call_resource(
962        &self,
963        handle: &ResourceHandle,
964        op: &str,
965        args: Vec<Value>,
966        back: &mut dyn Reentry,
967    ) -> Result<Value, RuntimeError> {
968        let task = back.task();
969        let qualified = handle.qualified_type();
970        let Some(entry) = self
971            .modules
972            .iter()
973            .find(|m| m.module_schema().name == handle.module)
974        else {
975            return Err(
976                RuntimeError::new(format!("unknown host module `{}`", handle.module))
977                    .with_help(format!(
978                        "`{qualified}` names a resource of a host module this run has none of"
979                    ))
980                    .with_outcome(RunOutcome::HostBoundary),
981            );
982        };
983        let schema = entry.module_schema();
984        let Some(resource) = schema
985            .resources
986            .iter()
987            .find(|resource| resource.name == handle.type_name)
988        else {
989            return Err(RuntimeError::new(format!(
990                "host module `{}` issues no `{}` handles",
991                handle.module, handle.type_name
992            ))
993            .with_outcome(RunOutcome::HostBoundary));
994        };
995        let declared = resource.operation(op).copied();
996        let capability = match &declared {
997            Some(op_schema) => Capability::new(op_schema.capability),
998            None => Capability::new(schema.capability),
999        };
1000        let callee = Callee {
1001            module: handle.module.clone(),
1002            // A resource operation is recorded under the name that says which
1003            // resource answered it, so a trace of a run holding two kinds of
1004            // handle does not read as one flat list of `query` calls.
1005            op: format!("{}.{op}", handle.type_name),
1006            receiver: Some(Value(Repr::Resource(Arc::new(handle.clone())))),
1007            owner: format!("`{qualified}`"),
1008            known: resource.operations.iter().map(|e| e.name).collect(),
1009        };
1010        self.dispatch(task, &callee, declared, capability, args, |args| {
1011            entry.call_resource(handle, op, args, back)
1012        })
1013    }
1014
1015    /// The grant check, the schema check on the way in, the budget charge,
1016    /// the trace, the dispatch itself, and the schema check on the way out —
1017    /// everything a Host API call passes through, whether it was addressed to
1018    /// a module or to a handle.
1019    ///
1020    /// Both schema checks read one declaration from both sides: the
1021    /// arguments must be what `params` says before the host is reached, and
1022    /// the result must be what `result` says before it is handed on.
1023    fn dispatch(
1024        &self,
1025        task: u64,
1026        callee: &Callee,
1027        declared: Option<OperationSchema>,
1028        capability: Capability,
1029        args: Vec<Value>,
1030        invoke: impl FnOnce(Vec<Value>) -> Result<Value, RuntimeError>,
1031    ) -> Result<Value, RuntimeError> {
1032        let shown = callee.shown();
1033        let refused = |args: Vec<RecordedValue>| TraceEvent::HostCall {
1034            task,
1035            module: callee.module.clone(),
1036            op: callee.op.clone(),
1037            capability: capability.to_string(),
1038            wait: std::time::Duration::ZERO,
1039            granted: false,
1040            args,
1041            outcome: None,
1042        };
1043        if !self.grants.allows(&capability) {
1044            self.trace.record(refused(self.record_call(callee, &args)));
1045            return Err(RuntimeError::new(format!(
1046                "`{shown}` requires the `{capability}` capability, which this run was not granted"
1047            ))
1048            .with_rule("Cove code has no ambient authority; the host grants capabilities at the execution boundary.")
1049            .with_help(match self.grant_source {
1050                GrantSource::Config => {
1051                    format!("add `{capability}` to `allow` in the run's `cove.toml` table")
1052                }
1053                // Naming a `cove.toml` here would name a file this binary
1054                // never reads: its grants were fixed when it was built.
1055                GrantSource::Sealed => format!(
1056                    "this binary carries the capabilities it was built with; add `{capability}` to `allow` in the run's `cove.toml` table and build it again"
1057                ),
1058            })
1059            .with_outcome(RunOutcome::HostBoundary)
1060            .with_denied_capability(capability.to_string()));
1061        }
1062        let Some(schema) = declared else {
1063            return Err(RuntimeError::new(format!(
1064                "{} has no operation `{}`",
1065                callee.owner,
1066                callee.bare_op()
1067            ))
1068            .with_help(format!(
1069                "{} exposes {}",
1070                callee.owner,
1071                if callee.known.is_empty() {
1072                    "no operations".to_string()
1073                } else {
1074                    callee
1075                        .known
1076                        .iter()
1077                        .map(|name| format!("`{name}`"))
1078                        .collect::<Vec<_>>()
1079                        .join(", ")
1080                }
1081            ))
1082            .with_outcome(RunOutcome::HostBoundary));
1083        };
1084        if !schema.accepts(args.len()) {
1085            return Err(RuntimeError::new(format!(
1086                "`{shown}` takes {}, but {} were given",
1087                schema.expected_arity(),
1088                args.len()
1089            ))
1090            .with_help(format!(
1091                "the Host API schema declares `{}.{}`",
1092                callee.module,
1093                callee.signature(&schema)
1094            ))
1095            .with_outcome(RunOutcome::HostBoundary));
1096        }
1097        // Arity and types are the same check on the same declaration, so they
1098        // are made together and in the same place: before the host is
1099        // reached, before the budget is charged, and with nothing on the
1100        // trace, because a call refused here never happened.
1101        //
1102        // `cove check` makes this check too, at the call site, where the
1103        // mistake has a span to point at — but it can only make it for the
1104        // hosts it can see. An embedder's own module is registered at run
1105        // time and named in no table the compiler reads, so this is the only
1106        // thing standing between such a host and an argument its schema does
1107        // not admit.
1108        if let Some(mismatch) = undeclared_argument(&schema, &args) {
1109            return Err(RuntimeError::new(
1110                mismatch
1111                    .what
1112                    .describe(&shown, Part::Argument(mismatch.position)),
1113            )
1114            .with_rule(A_CALL_KEEPS_THE_SCHEMA)
1115            .with_help(format!(
1116                "the Host API schema declares `{}.{}`",
1117                callee.module,
1118                callee.signature(&schema)
1119            ))
1120            .with_outcome(RunOutcome::HostBoundary));
1121        }
1122
1123        if let Some(Err(error)) = self.with_budget(|budget| {
1124            budget
1125                .charge_host_call()
1126                .map_err(|stopped| budget.to_runtime_error(stopped))
1127        }) {
1128            self.trace.record(refused(self.record_call(callee, &args)));
1129            return Err(error);
1130        }
1131
1132        if schema.effect == Effect::IrreversibleWrite {
1133            self.irreversible_writes.fetch_add(1, Ordering::Relaxed);
1134        }
1135
1136        // A trace has to carry the arguments to be replayable, and the host
1137        // takes ownership of them, so they are recorded before dispatch
1138        // rather than reconstructed afterwards.
1139        let recorded_args = self.record_call(callee, &args);
1140        let started = Instant::now();
1141        let result = invoke(args);
1142        let wait = started.elapsed();
1143        // The schema decides whether the result is written down. An operation
1144        // that is not recordable has its call recorded and its result left
1145        // out: replaying `process.exit` by handing back a value would keep
1146        // running a program that had ended.
1147        if self.trace.is_recording() {
1148            let outcome = if schema.recordable {
1149                match &result {
1150                    Ok(value) => HostOutcome::Value(RecordedValue::of(value)),
1151                    Err(error) => HostOutcome::Error(error.message.clone()),
1152                }
1153            } else {
1154                HostOutcome::NotRecordable
1155            };
1156            self.trace.record(TraceEvent::HostCall {
1157                task,
1158                module: callee.module.clone(),
1159                op: callee.op.clone(),
1160                capability: capability.to_string(),
1161                wait,
1162                granted: true,
1163                args: recorded_args,
1164                outcome: Some(outcome),
1165            });
1166        }
1167
1168        // The last thing the boundary asks is the one thing it never used to:
1169        // that the host answered the type it declared. ADR 0001 makes the
1170        // schema one description shared by the compiler, runtime, and CLI,
1171        // and a description nothing enforces is a comment. The trace is
1172        // written first, so what the host actually did is on the record
1173        // either way; the value is what stops here.
1174        //
1175        // Only a value is checked. A host that answers `Err` has already
1176        // failed on its own terms, and the `Error` a schema declares is the
1177        // one inside a Cove `Result`, not this one.
1178        if let Ok(value) = &result {
1179            if let Err(mismatch) = schema.result.admits(value) {
1180                return Err(RuntimeError::new(mismatch.describe(&shown, Part::Result))
1181                    .with_rule(HOST_KEEPS_ITS_SCHEMA)
1182                    .with_help(format!(
1183                        "the Host API schema declares `{}.{}`",
1184                        callee.module,
1185                        callee.signature(&schema)
1186                    ))
1187                    .with_outcome(RunOutcome::HostBoundary));
1188            }
1189        }
1190        result
1191    }
1192}
1193
1194/// The first argument the operation's declaration does not admit, if there is
1195/// one.
1196///
1197/// A loop of its own rather than one more paragraph inside
1198/// [`HostRegistry::dispatch`], which carries six checks already. A call that
1199/// keeps its declaration pays one walk of its own arguments and allocates
1200/// nothing: the description of a disagreement is built on the way out of a
1201/// failure.
1202fn undeclared_argument(schema: &OperationSchema, args: &[Value]) -> Option<UndeclaredArgument> {
1203    for (index, argument) in args.iter().enumerate() {
1204        // Arity was checked first, so an index past a fixed operation's
1205        // parameters cannot happen; a variadic one answers for the rest.
1206        let declared = schema.param(index)?;
1207        if let Err(what) = declared.admits(argument) {
1208            return Some(UndeclaredArgument {
1209                position: index + 1,
1210                what,
1211            });
1212        }
1213    }
1214    None
1215}
1216
1217/// One argument that is not what its operation declared, and which one it was.
1218struct UndeclaredArgument {
1219    /// Which argument, counted from one as a diagnostic counts.
1220    position: usize,
1221    /// Where it stopped agreeing with the declared type.
1222    what: Mismatch,
1223}
1224
1225/// The rule a host breaks by answering something its own declaration does not
1226/// admit.
1227///
1228/// This is a broken invariant on the host's side of the boundary rather than
1229/// an expected failure, so it stops the run instead of arriving in Cove code
1230/// as a value that program never asked for and cannot handle.
1231const HOST_KEEPS_ITS_SCHEMA: &str = "A host operation answers the type its Host API schema declares; the schema is one description shared by the compiler, runtime, and CLI.";
1232
1233/// The rule a call breaks by passing an argument the operation's own
1234/// declaration does not admit.
1235///
1236/// This is the program's mistake rather than the host's, and `cove check`
1237/// reports it at the call site before a run starts. It is stated again here
1238/// because the checker reads only the schema of the modules the toolchain
1239/// ships, and a host may be anyone's.
1240const A_CALL_KEEPS_THE_SCHEMA: &str = "A Host API call passes the argument types its operation's schema declares; the schema is one description shared by the compiler, runtime, and CLI.";
1241
1242/// The schema of every host module the toolchain ships.
1243///
1244/// `cove trace` and `cove replay` read a trace without a host to ask, and
1245/// both need what the schema says: which calls the trace recorded are
1246/// irreversible, which capability each one needs, and whether a result was
1247/// recordable. `cove-sema` needs the same table with no runtime to depend on
1248/// at all. So the table is [`cove_schema::hosts::SHIPPED`] and every module
1249/// below answers with its entry from it: not a copy that agrees, the same
1250/// bytes.
1251pub fn shipped_schema() -> &'static [ModuleSchema] {
1252    cove_schema::hosts::shipped()
1253}
1254
1255/// What `console` declares about itself.
1256///
1257/// The table is [`cove_schema::hosts::CONSOLE`], so the description the
1258/// compiler checks a call against and the one the boundary dispatches through
1259/// are the same bytes.
1260const CONSOLE_SCHEMA: ModuleSchema = cove_schema::hosts::CONSOLE;
1261
1262/// `console`: line-oriented output on two streams.
1263///
1264/// The two writers are the whole of the difference between the streams. What
1265/// a program produces goes to `out` through `println` and `print`; what it
1266/// says about what it produces goes to `err` through `eprintln` and `eprint`,
1267/// and a host that wants the two apart is a host that hands over two
1268/// different writers. `cove run` gives the process's stdout and stderr, which
1269/// is why a program's records can now be piped somewhere while its complaints
1270/// stay on the terminal.
1271///
1272/// The two streams are one capability. All four operations require
1273/// `console`, so a run that may print may complain, and this type is where
1274/// the difference between the streams is: an embedding that wants a program's
1275/// output captured while its complaints reach the terminal hands over a
1276/// buffer and `std::io::stderr()`, which is a wiring choice rather than an
1277/// authority one. Nothing here consults the grants at all — the boundary has
1278/// already decided by the time an operation arrives.
1279///
1280/// # Migrating from the one-writer form
1281///
1282/// `Console::new` took one writer and now takes two. The second is where
1283/// diagnostics go, and there is deliberately no default: a host that captured
1284/// a program's output before this existed would silently start capturing its
1285/// diagnostics too, which is exactly the mixing the second stream is for
1286/// undoing. `Console::new(w)` therefore fails to compile rather than changing
1287/// meaning. An embedding that genuinely wants one stream says so —
1288/// `Console::new(w.clone(), w)` for a shared writer, `Console::new(w,
1289/// std::io::sink())` to drop diagnostics — and one that wants the process's
1290/// streams writes `Console::new(std::io::stdout(), std::io::stderr())`.
1291pub struct Console<O: Write + Send, E: Write + Send> {
1292    /// Held under a lock so that one task's line is written whole: two tasks
1293    /// printing at once must interleave lines, never halves of a line.
1294    out: Mutex<O>,
1295    /// The diagnostic stream, under a lock of its own: a task writing a
1296    /// warning must not queue behind a task writing a record, since the two
1297    /// streams are usually two different files.
1298    err: Mutex<E>,
1299}
1300
1301impl<O: Write + Send, E: Write + Send> Console<O, E> {
1302    /// A console whose output goes to `out` and whose diagnostics go to
1303    /// `err`.
1304    pub fn new(out: O, err: E) -> Self {
1305        Console {
1306            out: Mutex::new(out),
1307            err: Mutex::new(err),
1308        }
1309    }
1310}
1311
1312impl<O: Write + Send, E: Write + Send> HostApi for Console<O, E> {
1313    fn module_schema(&self) -> ModuleSchema {
1314        CONSOLE_SCHEMA
1315    }
1316
1317    fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
1318        let text = args
1319            .iter()
1320            .map(|v| v.to_string())
1321            .collect::<Vec<_>>()
1322            .join(" ");
1323        // Each arm holds one stream's lock for the whole of one write, and
1324        // neither arm takes the other's: the two streams are independent, so
1325        // a diagnostic never waits on a record.
1326        let result = match op {
1327            "println" => write_line(&self.out, &text, true),
1328            "print" => write_line(&self.out, &text, false),
1329            "eprintln" => write_line(&self.err, &text, true),
1330            "eprint" => write_line(&self.err, &text, false),
1331            _ => unreachable!("checked by HostRegistry::call"),
1332        };
1333        match result {
1334            Ok(()) => Ok(Value::ok(Value(Repr::Unit))),
1335            Err(e) => Ok(Value::err(Value::error(format!("console: {e}")))),
1336        }
1337    }
1338}
1339
1340/// Writes `text` to one of a console's streams, with a newline after it when
1341/// `newline`, and flushes.
1342///
1343/// Both streams write the same way, and a stream that is written differently
1344/// from the other is a stream a program can tell apart by something other
1345/// than where it goes.
1346fn write_line<W: Write + Send>(
1347    stream: &Mutex<W>,
1348    text: &str,
1349    newline: bool,
1350) -> std::io::Result<()> {
1351    let mut stream = stream
1352        .lock()
1353        .unwrap_or_else(|poisoned| poisoned.into_inner());
1354    if newline {
1355        writeln!(stream, "{text}")?;
1356    } else {
1357        write!(stream, "{text}")?;
1358    }
1359    stream.flush()
1360}
1361
1362/// What `env` declares about itself.
1363const ENV_SCHEMA: ModuleSchema = cove_schema::hosts::ENV;
1364
1365/// `env`: read-only access to the environment the host supplies.
1366///
1367/// The map is given to the constructor rather than read from the process, so a
1368/// host decides exactly which variables a run can observe.
1369pub struct Env {
1370    vars: BTreeMap<String, String>,
1371}
1372
1373impl Env {
1374    /// Builds an environment from the variables the host chooses to expose.
1375    pub fn new(vars: BTreeMap<String, String>) -> Self {
1376        Env { vars }
1377    }
1378
1379    /// Snapshots the real process environment. Explicit by design: nothing
1380    /// else in the runtime reads `std::env`.
1381    pub fn from_process() -> Self {
1382        Env {
1383            vars: std::env::vars().collect(),
1384        }
1385    }
1386}
1387
1388impl HostApi for Env {
1389    fn module_schema(&self) -> ModuleSchema {
1390        ENV_SCHEMA
1391    }
1392
1393    fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
1394        match op {
1395            "get" => {
1396                let [Value(Repr::Str(name))] = args.as_slice() else {
1397                    unreachable!("checked by HostRegistry::call")
1398                };
1399                Ok(match self.vars.get(&**name) {
1400                    Some(value) => Value::some(Value(Repr::Str(value.as_str().into()))),
1401                    None => Value::none(),
1402                })
1403            }
1404            _ => unreachable!("checked by HostRegistry::call"),
1405        }
1406    }
1407}
1408
1409/// What `documents` declares about itself.
1410const DOCUMENTS_SCHEMA: ModuleSchema = cove_schema::hosts::DOCUMENTS;
1411
1412/// `documents`: a filtered, read-only view over a fixed set of named text
1413/// documents.
1414///
1415/// Granting `documents` never grants filesystem access. A host names exactly
1416/// which documents exist; there is no way to reach a path this module was not
1417/// built to expose, so a grant of `documents` is narrow authority, never
1418/// ambient access to a directory.
1419pub struct Documents {
1420    source: DocumentsSource,
1421}
1422
1423enum DocumentsSource {
1424    InMemory(BTreeMap<String, String>),
1425    Rooted(PathBuf),
1426}
1427
1428impl Documents {
1429    /// A fake implementation backed by an in-memory map, for tests.
1430    pub fn in_memory(documents: BTreeMap<String, String>) -> Self {
1431        Documents {
1432            source: DocumentsSource::InMemory(documents),
1433        }
1434    }
1435
1436    /// Reads `<root>/<name>.txt` for a document named `name`.
1437    ///
1438    /// `name` must be a single plain path component: empty names, `.`, `..`,
1439    /// and names containing `/`, `\`, or a NUL byte are all rejected before
1440    /// the filesystem is touched. This keeps the capability narrow: a grant
1441    /// of `documents` can only ever reach the fixed set of `.txt` files under
1442    /// `root`, never an arbitrary path via traversal or an absolute path.
1443    pub fn rooted(root: PathBuf) -> Self {
1444        Documents {
1445            source: DocumentsSource::Rooted(root),
1446        }
1447    }
1448
1449    fn read(&self, name: &str) -> Result<String, String> {
1450        let missing = || format!("no document named `{name}`");
1451        match &self.source {
1452            DocumentsSource::InMemory(documents) => {
1453                documents.get(name).cloned().ok_or_else(missing)
1454            }
1455            DocumentsSource::Rooted(root) => {
1456                if !is_plain_document_name(name) {
1457                    return Err(missing());
1458                }
1459                std::fs::read_to_string(root.join(format!("{name}.txt"))).map_err(|_| missing())
1460            }
1461        }
1462    }
1463}
1464
1465/// Whether `name` is safe to join onto a root: a single component, never a
1466/// path that could escape it.
1467fn is_plain_document_name(name: &str) -> bool {
1468    !name.is_empty()
1469        && name != "."
1470        && name != ".."
1471        && !name.contains('/')
1472        && !name.contains('\\')
1473        && !name.contains('\0')
1474}
1475
1476impl HostApi for Documents {
1477    fn module_schema(&self) -> ModuleSchema {
1478        DOCUMENTS_SCHEMA
1479    }
1480
1481    fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
1482        match op {
1483            "read" => {
1484                let [Value(Repr::Str(name))] = args.as_slice() else {
1485                    unreachable!("checked by HostRegistry::call")
1486                };
1487                Ok(match self.read(name) {
1488                    Ok(text) => Value::ok(Value(Repr::Str(text.into()))),
1489                    Err(message) => Value::err(Value::error(message)),
1490                })
1491            }
1492            _ => unreachable!("checked by HostRegistry::call"),
1493        }
1494    }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499    use super::*;
1500    use crate::schema::HostType;
1501    use std::path::Path;
1502
1503    /// A temporary directory, removed on drop.
1504    struct TempDir(PathBuf);
1505
1506    impl TempDir {
1507        fn new(name: &str) -> Self {
1508            let dir = std::env::temp_dir().join(format!(
1509                "cove-runtime-test-{name}-{}-{}",
1510                std::process::id(),
1511                nanos()
1512            ));
1513            std::fs::create_dir_all(&dir).unwrap();
1514            TempDir(dir)
1515        }
1516
1517        fn path(&self) -> &Path {
1518            &self.0
1519        }
1520    }
1521
1522    impl Drop for TempDir {
1523        fn drop(&mut self) {
1524            let _ = std::fs::remove_dir_all(&self.0);
1525        }
1526    }
1527
1528    fn nanos() -> u128 {
1529        std::time::SystemTime::now()
1530            .duration_since(std::time::UNIX_EPOCH)
1531            .unwrap()
1532            .as_nanos()
1533    }
1534
1535    fn ok_str(value: Value) -> String {
1536        match value.ok_payload() {
1537            Some(payload) => match payload.first() {
1538                Some(Value(Repr::Str(text))) => text.to_string(),
1539                other => panic!("expected `Ok(String)`, found {other:?}"),
1540            },
1541            None => panic!("expected `Ok(String)`, found {value}"),
1542        }
1543    }
1544
1545    fn err_message(value: Value) -> String {
1546        match value.err_payload() {
1547            Some(payload) => payload.first().map(ToString::to_string).unwrap_or_default(),
1548            None => panic!("expected `Err(...)`, found {value}"),
1549        }
1550    }
1551
1552    #[test]
1553    fn in_memory_read_hits_and_misses() {
1554        let documents = Documents::in_memory(BTreeMap::from([(
1555            "input".to_string(),
1556            "hello world".to_string(),
1557        )]));
1558
1559        let hit = documents
1560            .call("read", vec![Value(Repr::Str("input".into()))])
1561            .expect("no runtime error");
1562        assert_eq!(ok_str(hit), "hello world");
1563
1564        let miss = documents
1565            .call("read", vec![Value(Repr::Str("missing".into()))])
1566            .expect("no runtime error");
1567        assert_eq!(err_message(miss), "no document named `missing`");
1568    }
1569
1570    #[test]
1571    fn rooted_reads_a_real_file() {
1572        let dir = TempDir::new("rooted-read");
1573        std::fs::write(dir.path().join("input.txt"), "five little words here").unwrap();
1574        let documents = Documents::rooted(dir.path().to_path_buf());
1575
1576        let read = documents
1577            .call("read", vec![Value(Repr::Str("input".into()))])
1578            .expect("no runtime error");
1579        assert_eq!(ok_str(read), "five little words here");
1580    }
1581
1582    #[test]
1583    fn rooted_rejects_a_missing_document() {
1584        let dir = TempDir::new("rooted-missing");
1585        let documents = Documents::rooted(dir.path().to_path_buf());
1586
1587        let read = documents
1588            .call("read", vec![Value(Repr::Str("absent".into()))])
1589            .expect("no runtime error");
1590        assert_eq!(err_message(read), "no document named `absent`");
1591    }
1592
1593    #[test]
1594    fn rooted_rejects_path_traversal() {
1595        let dir = TempDir::new("rooted-traversal");
1596        let documents = Documents::rooted(dir.path().to_path_buf());
1597
1598        let read = documents
1599            .call("read", vec![Value(Repr::Str("..".into()))])
1600            .expect("no runtime error");
1601        assert_eq!(err_message(read), "no document named `..`");
1602    }
1603
1604    #[test]
1605    fn rooted_rejects_a_nested_path() {
1606        let dir = TempDir::new("rooted-nested");
1607        let documents = Documents::rooted(dir.path().to_path_buf());
1608
1609        let read = documents
1610            .call("read", vec![Value(Repr::Str("a/b".into()))])
1611            .expect("no runtime error");
1612        assert_eq!(err_message(read), "no document named `a/b`");
1613    }
1614
1615    #[test]
1616    fn rooted_rejects_an_empty_name() {
1617        let dir = TempDir::new("rooted-empty");
1618        let documents = Documents::rooted(dir.path().to_path_buf());
1619
1620        let read = documents
1621            .call("read", vec![Value(Repr::Str("".into()))])
1622            .expect("no runtime error");
1623        assert_eq!(err_message(read), "no document named ``");
1624    }
1625
1626    #[test]
1627    fn registry_without_the_documents_grant_rejects_the_call() {
1628        let mut hosts = HostRegistry::new(Grants::new(Vec::<String>::new()));
1629        hosts.register(Box::new(Documents::in_memory(BTreeMap::new())));
1630
1631        let error = hosts
1632            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1633            .expect_err("the call should be rejected");
1634        assert!(error.message.contains("documents"), "{}", error.message);
1635    }
1636
1637    #[test]
1638    fn registry_with_the_documents_grant_allows_the_call() {
1639        let mut hosts = HostRegistry::new(Grants::new(["documents"]));
1640        hosts.register(Box::new(Documents::in_memory(BTreeMap::from([(
1641            "input".to_string(),
1642            "hello world".to_string(),
1643        )]))));
1644
1645        let value = hosts
1646            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1647            .expect("the call should be allowed");
1648        assert_eq!(ok_str(value), "hello world");
1649    }
1650
1651    /// Collects every event recorded into it, for assertions.
1652    #[derive(Clone, Default)]
1653    struct RecordingSink(Arc<Mutex<Vec<TraceEvent>>>);
1654
1655    impl RecordingSink {
1656        fn events(&self) -> Vec<TraceEvent> {
1657            self.0
1658                .lock()
1659                .expect("no test panics while recording")
1660                .clone()
1661        }
1662    }
1663
1664    impl TraceSink for RecordingSink {
1665        fn record(&self, event: TraceEvent) {
1666            self.0
1667                .lock()
1668                .expect("no test panics while recording")
1669                .push(event);
1670        }
1671    }
1672
1673    /// What a recorded value shows as, which is the value it carried on the
1674    /// far side of the boundary the event crossed.
1675    fn shown(recorded: &RecordedValue) -> String {
1676        recorded_value(recorded).to_string()
1677    }
1678
1679    /// The value a recorded value carried.
1680    fn recorded_value(recorded: &RecordedValue) -> Value {
1681        match recorded {
1682            RecordedValue::Carried(transfer) => transfer.clone().into_value(),
1683            RecordedValue::Opaque { shown, .. } => Value(Repr::Str(shown.as_str().into())),
1684        }
1685    }
1686
1687    fn registry_with_documents() -> HostRegistry {
1688        let mut hosts = HostRegistry::new(Grants::new(["documents"]));
1689        hosts.register(Box::new(Documents::in_memory(BTreeMap::from([(
1690            "input".to_string(),
1691            "hello world".to_string(),
1692        )]))));
1693        hosts
1694    }
1695
1696    #[test]
1697    fn budget_stops_a_call_before_it_dispatches() {
1698        use crate::budget::{Budget, Limits};
1699
1700        let mut hosts = registry_with_documents();
1701        hosts.set_budget(Budget::new(Limits {
1702            max_host_calls: Some(0),
1703            ..Limits::default()
1704        }));
1705        let sink = RecordingSink::default();
1706        hosts.set_trace(Arc::new(sink.clone()));
1707
1708        let error = hosts
1709            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1710            .expect_err("the call should be stopped by the budget");
1711        assert!(error.rule.is_some(), "{error:?}");
1712
1713        let events = sink.events();
1714        assert_eq!(events.len(), 1, "{events:?}");
1715        match &events[0] {
1716            TraceEvent::HostCall { granted, .. } => assert!(!granted),
1717            other => panic!("expected a HostCall event, found {other:?}"),
1718        }
1719        assert_eq!(hosts.with_budget(|budget| budget.host_calls()), Some(1));
1720    }
1721
1722    #[test]
1723    fn a_granted_call_produces_one_event_with_a_plausible_wait() {
1724        let mut hosts = registry_with_documents();
1725        let sink = RecordingSink::default();
1726        hosts.set_trace(Arc::new(sink.clone()));
1727
1728        hosts
1729            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1730            .expect("the call should be allowed");
1731
1732        let events = sink.events();
1733        assert_eq!(events.len(), 1, "{events:?}");
1734        match &events[0] {
1735            TraceEvent::HostCall {
1736                task,
1737                module,
1738                op,
1739                capability,
1740                wait,
1741                granted,
1742                args,
1743                outcome,
1744            } => {
1745                // Nothing ran a program here, so the call belongs to the
1746                // entry's own id, which is what a call made outside a task
1747                // reports.
1748                assert_eq!(*task, crate::runtime::ENTRY_TASK);
1749                assert_eq!(module, "documents");
1750                assert_eq!(op, "read");
1751                assert_eq!(capability, "documents");
1752                assert!(*granted);
1753                assert!(*wait < std::time::Duration::from_secs(1), "{wait:?}");
1754                // A trace that says only that a call happened cannot replay
1755                // it, so the event carries the call's arguments and its
1756                // result too.
1757                assert_eq!(args.len(), 1);
1758                assert_eq!(shown(&args[0]), "input");
1759                match outcome {
1760                    Some(HostOutcome::Value(value)) => {
1761                        assert_eq!(ok_str(recorded_value(value)), "hello world")
1762                    }
1763                    other => panic!("expected a recorded value, found {other:?}"),
1764                }
1765            }
1766            other => panic!("expected a HostCall event, found {other:?}"),
1767        }
1768    }
1769
1770    /// The schema's `recordable` flag decides whether a result is written
1771    /// down, and `process.exit` is the one shipped operation it decides
1772    /// against: replaying it by handing back a value would keep running a
1773    /// program that had ended.
1774    #[test]
1775    fn a_result_is_recorded_only_when_the_schema_says_it_may_be() {
1776        let mut hosts = HostRegistry::new(Grants::new(["process"]));
1777        hosts.register(Box::new(crate::process::Process::recorded(
1778            vec!["one".to_string()],
1779            BTreeMap::new(),
1780            crate::process::ProcessLog::new(),
1781        )));
1782        let sink = RecordingSink::default();
1783        hosts.set_trace(Arc::new(sink.clone()));
1784
1785        hosts.call("process", "args", Vec::new()).expect("granted");
1786        hosts
1787            .call("process", "exit", vec![Value(Repr::Int(2))])
1788            .expect("granted");
1789
1790        let events = sink.events();
1791        assert_eq!(events.len(), 2, "{events:?}");
1792        match &events[0] {
1793            // `process.args` is recordable, so its result is recorded.
1794            TraceEvent::HostCall {
1795                outcome: Some(HostOutcome::Value(value)),
1796                ..
1797            } => assert_eq!(shown(value), "[one]"),
1798            other => panic!("expected a recorded value, found {other:?}"),
1799        }
1800        match &events[1] {
1801            TraceEvent::HostCall {
1802                op,
1803                args,
1804                outcome: Some(HostOutcome::NotRecordable),
1805                ..
1806            } => {
1807                assert_eq!(op, "exit");
1808                // The call itself is still recorded, arguments and all: what
1809                // the program asked for is exactly the part worth knowing.
1810                assert_eq!(args.len(), 1);
1811                assert_eq!(shown(&args[0]), "2");
1812            }
1813            other => panic!("expected `not recordable`, found {other:?}"),
1814        }
1815    }
1816
1817    /// A sink that reads nothing is asked for nothing: describing a call's
1818    /// values costs a copy of each, and an untraced run — whose sink is
1819    /// [`NullSink`] — should not pay for a description nobody keeps.
1820    #[test]
1821    fn a_sink_that_is_not_recording_is_given_no_events() {
1822        /// Records every event it is given, while saying it will not read
1823        /// them, exactly as `NullSink` does.
1824        #[derive(Clone, Default)]
1825        struct Deaf(RecordingSink);
1826
1827        impl TraceSink for Deaf {
1828            fn record(&self, event: TraceEvent) {
1829                self.0.record(event);
1830            }
1831
1832            fn is_recording(&self) -> bool {
1833                false
1834            }
1835        }
1836
1837        let mut hosts = registry_with_documents();
1838        let sink = Deaf::default();
1839        hosts.set_trace(Arc::new(sink.clone()));
1840
1841        hosts
1842            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1843            .expect("the call should be allowed");
1844
1845        assert!(sink.0.events().is_empty(), "{:?}", sink.0.events());
1846    }
1847
1848    /// A call the run was not granted never reaches a host, so there is no
1849    /// result to record — but there is still a request worth recording.
1850    #[test]
1851    fn a_refused_call_records_its_arguments_and_no_result() {
1852        let mut hosts = HostRegistry::new(Grants::new(Vec::<String>::new()));
1853        hosts.register(Box::new(Documents::in_memory(BTreeMap::new())));
1854        let sink = RecordingSink::default();
1855        hosts.set_trace(Arc::new(sink.clone()));
1856
1857        hosts
1858            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
1859            .expect_err("the call should be rejected");
1860
1861        let events = sink.events();
1862        match &events[0] {
1863            TraceEvent::HostCall {
1864                granted,
1865                args,
1866                outcome,
1867                ..
1868            } => {
1869                assert!(!granted);
1870                assert_eq!(args.len(), 1);
1871                assert!(outcome.is_none(), "{outcome:?}");
1872            }
1873            other => panic!("expected a HostCall event, found {other:?}"),
1874        }
1875    }
1876
1877    #[test]
1878    fn an_unknown_operation_lists_the_operations_that_exist() {
1879        let hosts = registry_with_documents();
1880
1881        let error = hosts
1882            .call("documents", "write", Vec::new())
1883            .expect_err("`documents` has no `write`");
1884        assert_eq!(
1885            error.message,
1886            "host module `documents` has no operation `write`"
1887        );
1888        assert_eq!(
1889            error.help.as_deref(),
1890            Some("host module `documents` exposes `read`")
1891        );
1892    }
1893
1894    #[test]
1895    fn too_many_arguments_are_rejected_before_the_host_sees_them() {
1896        let hosts = registry_with_documents();
1897
1898        let error = hosts
1899            .call(
1900                "documents",
1901                "read",
1902                vec![
1903                    Value(Repr::Str("input".into())),
1904                    Value(Repr::Str("extra".into())),
1905                ],
1906            )
1907            .expect_err("`documents.read` takes one argument");
1908        assert_eq!(
1909            error.message,
1910            "`documents.read` takes 1 argument, but 2 were given"
1911        );
1912        assert_eq!(
1913            error.help.as_deref(),
1914            Some("the Host API schema declares `documents.read(String) -> Result<String, Error>`")
1915        );
1916    }
1917
1918    #[test]
1919    fn too_few_arguments_are_rejected_too() {
1920        let hosts = registry_with_documents();
1921
1922        let error = hosts
1923            .call("documents", "read", Vec::new())
1924            .expect_err("`documents.read` takes one argument");
1925        assert_eq!(
1926            error.message,
1927            "`documents.read` takes 1 argument, but 0 were given"
1928        );
1929    }
1930
1931    /// `console.println("a", "b")` is one line of two parts, so the arity
1932    /// check must not reject it.
1933    #[test]
1934    fn a_variadic_operation_accepts_any_number_of_arguments() {
1935        let mut hosts = HostRegistry::new(Grants::new(["console"]));
1936        hosts.register(Box::new(Console::new(Vec::new(), Vec::new())));
1937
1938        for arity in 0..3 {
1939            hosts
1940                .call(
1941                    "console",
1942                    "println",
1943                    vec![Value(Repr::Str("part".into())); arity],
1944                )
1945                .unwrap_or_else(|e| panic!("arity {arity} should be accepted: {}", e.message));
1946        }
1947    }
1948
1949    /// A writer two owners can share: one of them is inside a [`Console`] and
1950    /// the other is the assertion.
1951    #[derive(Clone, Default)]
1952    struct Shared(Arc<Mutex<Vec<u8>>>);
1953
1954    impl Shared {
1955        fn written(&self) -> String {
1956            String::from_utf8(
1957                self.0
1958                    .lock()
1959                    .expect("nothing panicked while writing")
1960                    .clone(),
1961            )
1962            .expect("a console writes what it was given, which was text")
1963        }
1964    }
1965
1966    impl Write for Shared {
1967        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1968            self.0
1969                .lock()
1970                .expect("nothing panicked while writing")
1971                .extend_from_slice(buf);
1972            Ok(buf.len())
1973        }
1974
1975        fn flush(&mut self) -> std::io::Result<()> {
1976            Ok(())
1977        }
1978    }
1979
1980    /// A registry holding one [`Console`] over the two writers given, granted
1981    /// the one capability both of its streams answer to.
1982    fn registry_with_console(out: Shared, err: Shared) -> HostRegistry {
1983        let mut hosts = HostRegistry::new(Grants::new(["console"]));
1984        hosts.register(Box::new(Console::new(out, err)));
1985        hosts
1986    }
1987
1988    /// The whole of what the second stream is for: what a program says about
1989    /// its output is not in its output.
1990    #[test]
1991    fn what_the_diagnostic_stream_is_given_stays_out_of_the_output_stream() {
1992        let out = Shared::default();
1993        let err = Shared::default();
1994        let hosts = registry_with_console(out.clone(), err.clone());
1995
1996        for (op, text) in [
1997            ("println", "id,name"),
1998            ("eprintln", "line 2 is malformed, skipping"),
1999            ("println", "1,ada"),
2000        ] {
2001            hosts
2002                .call("console", op, vec![Value(Repr::Str(text.into()))])
2003                .unwrap_or_else(|e| panic!("`console.{op}` should be allowed: {}", e.message));
2004        }
2005
2006        assert_eq!(out.written(), "id,name\n1,ada\n");
2007        assert_eq!(err.written(), "line 2 is malformed, skipping\n");
2008    }
2009
2010    /// `print` and `eprint` differ from `println` and `eprintln` in the same
2011    /// way on both streams: the newline, and nothing else.
2012    #[test]
2013    fn the_unterminated_form_writes_the_same_way_on_either_stream() {
2014        let out = Shared::default();
2015        let err = Shared::default();
2016        let hosts = registry_with_console(out.clone(), err.clone());
2017
2018        for op in ["print", "eprint"] {
2019            hosts
2020                .call(
2021                    "console",
2022                    op,
2023                    vec![
2024                        Value(Repr::Str("two".into())),
2025                        Value(Repr::Str("parts".into())),
2026                    ],
2027                )
2028                .unwrap_or_else(|e| panic!("`console.{op}` should be allowed: {}", e.message));
2029        }
2030
2031        assert_eq!(out.written(), "two parts");
2032        assert_eq!(err.written(), "two parts");
2033    }
2034
2035    /// One capability covers both streams. A run granted `console` may write
2036    /// a record and may complain about it, which is why a grant written
2037    /// before `eprintln` existed did not have to be read again when it
2038    /// arrived; a run granted nothing may do neither, and is refused under
2039    /// the one name either way.
2040    #[test]
2041    fn one_grant_covers_both_streams() {
2042        let out = Shared::default();
2043        let err = Shared::default();
2044        let granted = registry_with_console(out.clone(), err.clone());
2045
2046        granted
2047            .call(
2048                "console",
2049                "println",
2050                vec![Value(Repr::Str("record".into()))],
2051            )
2052            .expect("`console` grants the output stream");
2053        granted
2054            .call(
2055                "console",
2056                "eprintln",
2057                vec![Value(Repr::Str("warning".into()))],
2058            )
2059            .expect("`console` grants the diagnostic stream too");
2060        assert_eq!(out.written(), "record\n");
2061        assert_eq!(err.written(), "warning\n");
2062
2063        let mut ungranted = HostRegistry::new(Grants::new(Vec::<String>::new()));
2064        ungranted.register(Box::new(Console::new(Shared::default(), Shared::default())));
2065        for op in ["println", "print", "eprintln", "eprint"] {
2066            let refused = ungranted
2067                .call("console", op, vec![Value(Repr::Str("anything".into()))])
2068                .expect_err("a run granted nothing reaches neither stream");
2069            assert_eq!(
2070                refused.denied_capability.as_deref(),
2071                Some("console"),
2072                "`console.{op}`: {}",
2073                refused.message
2074            );
2075        }
2076    }
2077
2078    #[test]
2079    fn task_safety_of_a_host_result_comes_from_the_schema() {
2080        let hosts = registry_with_documents();
2081
2082        assert_eq!(hosts.result_is_task_safe("documents", "read"), Some(true));
2083        assert_eq!(hosts.result_is_task_safe("documents", "write"), None);
2084        assert_eq!(hosts.result_is_task_safe("network", "read"), None);
2085    }
2086
2087    /// A host used only to give two modules the same name, so
2088    /// [`module_schemas_describes_the_module_dispatch_will_actually_reach`]
2089    /// can tell which one a lookup actually reached: its answer to `ping` is
2090    /// baked in rather than computed, so the test reads it off the return
2091    /// value instead of having to ask the host anything more.
2092    struct DuplicateNamed {
2093        schema: ModuleSchema,
2094        answer: &'static str,
2095    }
2096
2097    impl HostApi for DuplicateNamed {
2098        fn module_schema(&self) -> ModuleSchema {
2099            self.schema
2100        }
2101
2102        fn call(&self, _op: &str, _args: Vec<Value>) -> Result<Value, RuntimeError> {
2103            Ok(Value(Repr::Str(self.answer.into())))
2104        }
2105    }
2106
2107    static DUPLICATE_FIRST_OPERATIONS: &[OperationSchema] = &[OperationSchema {
2108        name: "ping",
2109        params: &[],
2110        variadic: false,
2111        result: HostType::String,
2112        capability: "duplicate-first",
2113        effect: Effect::Read,
2114        cancellable: false,
2115        recordable: true,
2116        result_is_task_safe: true,
2117    }];
2118
2119    static DUPLICATE_FIRST: ModuleSchema = ModuleSchema {
2120        name: "duplicate",
2121        capability: "duplicate-first",
2122        operations: DUPLICATE_FIRST_OPERATIONS,
2123        types: &[],
2124        resources: &[],
2125    };
2126
2127    static DUPLICATE_SECOND_OPERATIONS: &[OperationSchema] = &[OperationSchema {
2128        name: "ping",
2129        params: &[],
2130        variadic: false,
2131        result: HostType::String,
2132        capability: "duplicate-second",
2133        effect: Effect::Read,
2134        cancellable: false,
2135        recordable: true,
2136        result_is_task_safe: true,
2137    }];
2138
2139    static DUPLICATE_SECOND: ModuleSchema = ModuleSchema {
2140        name: "duplicate",
2141        capability: "duplicate-second",
2142        operations: DUPLICATE_SECOND_OPERATIONS,
2143        types: &[],
2144        resources: &[],
2145    };
2146
2147    /// [`HostRegistry::module_schemas`] hands the checker one schema per
2148    /// name, and every dispatch method resolves a duplicate name by taking
2149    /// the first module registered under it — so the schema this hands out
2150    /// for a duplicated name had better be the first module's, the same one
2151    /// [`HostRegistry::call`] reaches, or the checker would be checking a
2152    /// call against a module the runtime never dispatches to.
2153    #[test]
2154    fn module_schemas_describes_the_module_dispatch_will_actually_reach() {
2155        let mut hosts = HostRegistry::new(Grants::new(["duplicate-first", "duplicate-second"]));
2156        hosts.register(Box::new(DuplicateNamed {
2157            schema: DUPLICATE_FIRST,
2158            answer: "first",
2159        }));
2160        hosts.register(Box::new(DuplicateNamed {
2161            schema: DUPLICATE_SECOND,
2162            answer: "second",
2163        }));
2164
2165        assert_eq!(hosts.module_schemas(), vec![DUPLICATE_FIRST]);
2166
2167        match hosts
2168            .call("duplicate", "ping", Vec::new())
2169            .expect("the call should be allowed")
2170        {
2171            Value(Repr::Str(text)) => assert_eq!(&*text, "first"),
2172            other => panic!("expected a Str, found {other:?}"),
2173        }
2174    }
2175
2176    /// Every module a run registers declares itself out of
2177    /// [`cove_schema::hosts::SHIPPED`] rather than out of a table of its own.
2178    ///
2179    /// This is what makes "one description shared by the compiler, runtime,
2180    /// and CLI" a fact rather than an intention. `cove-sema` reads that table
2181    /// and never sees a host, so a module answering with anything else would
2182    /// be a second description with nothing holding it against the first —
2183    /// which is exactly how `http` once came to be missing from the
2184    /// compiler's list of host modules with no diagnostic anywhere. A test
2185    /// used to compare the two lists; there is one list now, and this is what
2186    /// keeps it one.
2187    ///
2188    /// A module agreeing with the table in isolation is not the same claim
2189    /// as a registry holding every shipped module agreeing with it too, so
2190    /// this also registers all eight and checks the registry's own dispatch
2191    /// tables — `contains`, `schema_for`, `host_type` — against the same
2192    /// table, the way [`HostRegistry::call`] and [`HostRegistry::call_with`]
2193    /// read them.
2194    #[test]
2195    fn every_module_a_run_registers_declares_itself_out_of_the_shared_schema() {
2196        let modules: Vec<Box<dyn HostApi>> = vec![
2197            Box::new(Console::new(std::io::sink(), std::io::sink())),
2198            Box::new(Env::new(BTreeMap::new())),
2199            Box::new(Documents::in_memory(BTreeMap::new())),
2200            Box::new(crate::clock::Clock::real()),
2201            Box::new(crate::files::Files::in_memory(BTreeMap::new())),
2202            Box::new(crate::process::Process::real(Vec::new(), Vec::new())),
2203            Box::new(crate::database::Database::denied()),
2204            Box::new(crate::http::Http::real()),
2205        ];
2206
2207        assert_eq!(
2208            modules.len(),
2209            shipped_schema().len(),
2210            "a module the schema describes is one a run registers, and the reverse"
2211        );
2212        for (module, declared) in modules.iter().zip(shipped_schema()) {
2213            assert_eq!(&module.module_schema(), declared, "`{}`", declared.name);
2214        }
2215
2216        let mut hosts = HostRegistry::new(Grants::new(Vec::<String>::new()));
2217        for module in modules {
2218            hosts.register(module);
2219        }
2220        for declared in shipped_schema() {
2221            assert!(hosts.contains(declared.name), "`{}`", declared.name);
2222            for op in declared.operations {
2223                assert_eq!(
2224                    hosts.schema_for(declared.name, op.name),
2225                    Some(op),
2226                    "`{}.{}`",
2227                    declared.name,
2228                    op.name
2229                );
2230            }
2231            for ty in declared.types {
2232                assert_eq!(
2233                    hosts.host_type(declared.name, ty.name),
2234                    Some(*ty),
2235                    "`{}.{}`",
2236                    declared.name,
2237                    ty.name
2238                );
2239            }
2240        }
2241    }
2242
2243    /// The counter behind `cove run --stats`, and the one thing that reads
2244    /// an operation's declared `effect`.
2245    #[test]
2246    fn only_the_calls_the_schema_calls_irreversible_are_counted() {
2247        let mut hosts = HostRegistry::new(Grants::new(["console", "files"]));
2248        hosts.register(Box::new(Console::new(Vec::new(), Vec::new())));
2249        hosts.register(Box::new(crate::files::Files::in_memory(BTreeMap::new())));
2250        assert_eq!(hosts.irreversible_writes(), 0);
2251
2252        hosts
2253            .call("files", "exists", vec![Value(Repr::Str("a.txt".into()))])
2254            .expect("the call should be allowed");
2255        assert_eq!(hosts.irreversible_writes(), 0);
2256
2257        hosts
2258            .call(
2259                "files",
2260                "write",
2261                vec![
2262                    Value(Repr::Str("a.txt".into())),
2263                    Value(Repr::Str("x".into())),
2264                ],
2265            )
2266            .expect("the call should be allowed");
2267        hosts
2268            .call("console", "println", vec![Value(Repr::Str("a".into()))])
2269            .expect("the call should be allowed");
2270        assert_eq!(hosts.irreversible_writes(), 2);
2271    }
2272
2273    /// A call the run was not granted never reaches the host, so it never
2274    /// changed anything to count.
2275    #[test]
2276    fn an_ungranted_irreversible_call_is_not_counted() {
2277        let mut hosts = HostRegistry::new(Grants::new(Vec::<String>::new()));
2278        hosts.register(Box::new(crate::files::Files::in_memory(BTreeMap::new())));
2279
2280        hosts
2281            .call(
2282                "files",
2283                "write",
2284                vec![
2285                    Value(Repr::Str("a.txt".into())),
2286                    Value(Repr::Str("x".into())),
2287                ],
2288            )
2289            .expect_err("the call should be rejected");
2290        assert_eq!(hosts.irreversible_writes(), 0);
2291    }
2292
2293    #[test]
2294    fn a_denied_call_produces_an_event_with_granted_false() {
2295        let mut hosts = HostRegistry::new(Grants::new(Vec::<String>::new()));
2296        hosts.register(Box::new(Documents::in_memory(BTreeMap::new())));
2297        let sink = RecordingSink::default();
2298        hosts.set_trace(Arc::new(sink.clone()));
2299
2300        hosts
2301            .call("documents", "read", vec![Value(Repr::Str("input".into()))])
2302            .expect_err("the call should be rejected for the missing grant");
2303
2304        let events = sink.events();
2305        assert_eq!(events.len(), 1, "{events:?}");
2306        match &events[0] {
2307            TraceEvent::HostCall {
2308                capability,
2309                granted,
2310                ..
2311            } => {
2312                assert_eq!(capability, "documents");
2313                assert!(!granted);
2314            }
2315            other => panic!("expected a HostCall event, found {other:?}"),
2316        }
2317    }
2318
2319    // ----------------------------------------------- a host and its schema
2320    //
2321    // ADR 0001 makes the schema shared property: "A machine-readable Host API
2322    // schema is shared by the compiler, runtime, and CLI. Each operation
2323    // describes its argument, result, and error types". Every shipped host is
2324    // written to obey its own, which is what makes them useless for asking
2325    // what happens when one does not. These tests register a host that can be
2326    // told to disagree with its declaration, and pin which disagreements the
2327    // boundary catches.
2328
2329    /// The operation [`Wayward`] declares.
2330    static WAYWARD_SCHEMA: &[OperationSchema] = &[
2331        OperationSchema {
2332            name: "read",
2333            params: &[HostType::String],
2334            variadic: false,
2335            result: HostType::Result(&HostType::String, &HostType::Error),
2336            capability: "wayward",
2337            effect: Effect::Read,
2338            cancellable: false,
2339            recordable: true,
2340            result_is_task_safe: true,
2341        },
2342        OperationSchema {
2343            name: "open",
2344            params: &[],
2345            variadic: false,
2346            result: HostType::Named("wayward.Handle"),
2347            capability: "wayward",
2348            effect: Effect::Read,
2349            cancellable: false,
2350            recordable: true,
2351            result_is_task_safe: true,
2352        },
2353        // A result with something inside it, so a disagreement can be nested
2354        // rather than sitting at the top of the value.
2355        OperationSchema {
2356            name: "list",
2357            params: &[],
2358            variadic: false,
2359            result: HostType::Result(&HostType::Array(&HostType::String), &HostType::Error),
2360            capability: "wayward",
2361            effect: Effect::Read,
2362            cancellable: false,
2363            recordable: true,
2364            result_is_task_safe: true,
2365        },
2366        // An operation that declares nothing about what it produces, which is
2367        // what `clock.timeout` and `clock.every` declare about the work they
2368        // are given.
2369        OperationSchema {
2370            name: "anything",
2371            params: &[],
2372            variadic: false,
2373            result: HostType::Any,
2374            capability: "wayward",
2375            effect: Effect::Read,
2376            cancellable: false,
2377            recordable: true,
2378            result_is_task_safe: true,
2379        },
2380        // An argument with something inside it, so a disagreement can be
2381        // nested there too.
2382        OperationSchema {
2383            name: "send",
2384            params: &[HostType::Array(&HostType::String)],
2385            variadic: false,
2386            result: HostType::Result(&HostType::String, &HostType::Error),
2387            capability: "wayward",
2388            effect: Effect::Read,
2389            cancellable: false,
2390            recordable: true,
2391            result_is_task_safe: true,
2392        },
2393        // One declared parameter answering for as many arguments as the call
2394        // makes, which is what `console.println` declares.
2395        OperationSchema {
2396            name: "say",
2397            params: &[HostType::String],
2398            variadic: true,
2399            result: HostType::Result(&HostType::String, &HostType::Error),
2400            capability: "wayward",
2401            effect: Effect::Read,
2402            cancellable: false,
2403            recordable: true,
2404            result_is_task_safe: true,
2405        },
2406        // An operation that declares nothing about what it is *given*, which
2407        // is what `clock.timeout` declares of the work it bounds.
2408        OperationSchema {
2409            name: "bound",
2410            params: &[HostType::Any],
2411            variadic: false,
2412            result: HostType::Result(&HostType::String, &HostType::Error),
2413            capability: "wayward",
2414            effect: Effect::Read,
2415            cancellable: false,
2416            recordable: true,
2417            result_is_task_safe: true,
2418        },
2419    ];
2420
2421    /// The one kind of resource [`Wayward`] declares it can open.
2422    static WAYWARD_RESOURCES: &[ResourceSchema] = &[ResourceSchema {
2423        name: "Handle",
2424        task_safe: true,
2425        operations: &[OperationSchema {
2426            name: "close",
2427            params: &[],
2428            variadic: false,
2429            result: HostType::Result(&HostType::Unit, &HostType::Error),
2430            capability: "wayward",
2431            effect: Effect::ReversibleWrite,
2432            cancellable: false,
2433            recordable: true,
2434            result_is_task_safe: true,
2435        }],
2436    }];
2437
2438    /// A kind of resource [`Wayward`] does *not* declare, which it mints a
2439    /// handle for anyway.
2440    static UNDECLARED_RESOURCE: ResourceSchema = ResourceSchema {
2441        name: "Ghost",
2442        task_safe: true,
2443        operations: &[],
2444    };
2445
2446    /// What [`Wayward`] answers with.
2447    ///
2448    /// A host holds data and builds its answer at the call, because a
2449    /// [`Value`] is reference-counted and belongs to the thread that built it
2450    /// while a host is shared by every task of a run. So this says which
2451    /// answer to build rather than holding one.
2452    #[derive(Clone, Copy)]
2453    enum Answer {
2454        /// What each operation declares: `Ok("what was declared")`, an
2455        /// `Ok` of an array of strings, and a handle of the one resource
2456        /// kind this module says it can open.
2457        Declared,
2458        /// The same operations, each answering something its own declaration
2459        /// does not admit: an `Int` where a `Result` was declared, an array
2460        /// with an `Int` among its strings, and a handle naming a resource
2461        /// kind this module never declared.
2462        Undeclared,
2463    }
2464
2465    /// A host whose behaviour can be made to disagree with its schema.
2466    ///
2467    /// It counts how often it was reached, so a test can tell a call the
2468    /// registry refused from one it dispatched: "before the host sees them"
2469    /// is a claim about where the check happens, not only about what it says.
2470    struct Wayward {
2471        answer: Answer,
2472        calls: Arc<AtomicU64>,
2473    }
2474
2475    impl Wayward {
2476        fn answering(answer: Answer) -> (Wayward, Arc<AtomicU64>) {
2477            let calls = Arc::new(AtomicU64::new(0));
2478            (
2479                Wayward {
2480                    answer,
2481                    calls: Arc::clone(&calls),
2482                },
2483                calls,
2484            )
2485        }
2486    }
2487
2488    /// The module this test host declares itself with, which is the one
2489    /// the registry holds it to.
2490    const WAYWARD: ModuleSchema = ModuleSchema {
2491        name: "wayward",
2492        capability: "wayward",
2493        operations: WAYWARD_SCHEMA,
2494        types: &[],
2495        resources: WAYWARD_RESOURCES,
2496    };
2497
2498    impl HostApi for Wayward {
2499        fn module_schema(&self) -> ModuleSchema {
2500            WAYWARD
2501        }
2502
2503        fn call(&self, op: &str, _args: Vec<Value>) -> Result<Value, RuntimeError> {
2504            self.calls.fetch_add(1, Ordering::Relaxed);
2505            let declared = matches!(self.answer, Answer::Declared);
2506            Ok(match op {
2507                // A handle naming a resource this module's schema does not
2508                // declare: the value is well formed and the name is a lie.
2509                "open" => Value(Repr::Resource(ResourceHandle::new(
2510                    "wayward",
2511                    if declared {
2512                        &WAYWARD_RESOURCES[0]
2513                    } else {
2514                        &UNDECLARED_RESOURCE
2515                    },
2516                    1,
2517                ))),
2518                "list" => Value::ok(Value(Repr::Array(if declared {
2519                    vec![Value(Repr::Str("what was declared".into()))].into()
2520                } else {
2521                    vec![
2522                        Value(Repr::Str("what was declared".into())),
2523                        Value(Repr::Int(3)),
2524                    ]
2525                    .into()
2526                }))),
2527                // Declared as `Any`, so this one cannot disagree with itself.
2528                "anything" => Value(Repr::Int(3)),
2529                _ if declared => Value::ok(Value(Repr::Str("what was declared".into()))),
2530                _ => Value(Repr::Int(3)),
2531            })
2532        }
2533
2534        fn call_resource(
2535            &self,
2536            _handle: &ResourceHandle,
2537            _op: &str,
2538            _args: Vec<Value>,
2539            _back: &mut dyn Reentry,
2540        ) -> Result<Value, RuntimeError> {
2541            self.calls.fetch_add(1, Ordering::Relaxed);
2542            Ok(match self.answer {
2543                Answer::Declared => Value::ok(Value(Repr::Unit)),
2544                Answer::Undeclared => Value::ok(Value(Repr::Str("not the declared `Unit`".into()))),
2545            })
2546        }
2547    }
2548
2549    /// A registry holding one [`Wayward`], with its capability granted and
2550    /// its trace recorded.
2551    fn registry_with_wayward(answer: Answer) -> (HostRegistry, Arc<AtomicU64>, RecordingSink) {
2552        let (host, calls) = Wayward::answering(answer);
2553        let mut hosts = HostRegistry::new(Grants::new(["wayward"]));
2554        hosts.register(Box::new(host));
2555        let sink = RecordingSink::default();
2556        hosts.set_trace(Arc::new(sink.clone()));
2557        (hosts, calls, sink)
2558    }
2559
2560    /// The success half: a host that answers what it declared is dispatched,
2561    /// its value reaches the caller unchanged, and the trace records it.
2562    #[test]
2563    fn a_host_that_answers_what_its_schema_declares_is_dispatched_and_recorded() {
2564        let (hosts, calls, sink) = registry_with_wayward(Answer::Declared);
2565
2566        let value = hosts
2567            .call("wayward", "read", vec![Value(Repr::Str("input".into()))])
2568            .expect("a conforming call is dispatched");
2569
2570        assert_eq!(ok_str(value), "what was declared");
2571        assert_eq!(calls.load(Ordering::Relaxed), 1);
2572        let events = sink.events();
2573        assert_eq!(events.len(), 1, "{events:?}");
2574        match &events[0] {
2575            TraceEvent::HostCall {
2576                op,
2577                granted,
2578                outcome,
2579                ..
2580            } => {
2581                assert_eq!(op, "read");
2582                assert!(granted);
2583                match outcome {
2584                    Some(HostOutcome::Value(recorded)) => {
2585                        assert_eq!(shown(recorded), "Ok(what was declared)")
2586                    }
2587                    other => panic!("expected a recorded value, found {other:?}"),
2588                }
2589            }
2590            other => panic!("expected a HostCall event, found {other:?}"),
2591        }
2592    }
2593
2594    /// A handle is a name, and the boundary trusts no name it is given: a
2595    /// `wayward.Ghost` names a resource kind the module's own `resources()`
2596    /// does not declare, so an operation on it is refused without the host
2597    /// being asked.
2598    #[test]
2599    fn a_handle_naming_a_resource_the_module_never_declared_is_refused() {
2600        let (hosts, calls, _) = registry_with_wayward(Answer::Declared);
2601        let ghost = ResourceHandle::new("wayward", &UNDECLARED_RESOURCE, 1);
2602
2603        let error = hosts
2604            .call_resource(&ghost, "close", Vec::new(), &mut NoReentry)
2605            .expect_err("a handle the schema does not declare is refused");
2606
2607        assert_eq!(
2608            error.message,
2609            "host module `wayward` issues no `Ghost` handles"
2610        );
2611        assert_eq!(
2612            calls.load(Ordering::Relaxed),
2613            0,
2614            "the host was not asked to act on a handle its schema disowns"
2615        );
2616    }
2617
2618    /// The declared kind of handle passes, and the operations it declares can
2619    /// then be called on it: the check refuses a name, not handles.
2620    #[test]
2621    fn a_handle_of_the_kind_the_operation_declared_is_admitted() {
2622        let (hosts, _, _) = registry_with_wayward(Answer::Declared);
2623
2624        let opened = hosts
2625            .call("wayward", "open", Vec::new())
2626            .expect("a handle of the declared kind is admitted");
2627        let Value(Repr::Resource(handle)) = opened else {
2628            panic!("expected a resource handle, found {opened}");
2629        };
2630        assert_eq!(handle.qualified_type(), "wayward.Handle");
2631
2632        let closed = hosts
2633            .call_resource(&handle, "close", Vec::new(), &mut NoReentry)
2634            .expect("the handle answers the operation its kind declares");
2635        assert!(matches!(closed, Value(Repr::Enum(_))), "{closed}");
2636    }
2637
2638    /// An operation the schema does not declare is refused at the boundary,
2639    /// and the diagnostic lists what the module does declare rather than
2640    /// leaving the caller to guess.
2641    #[test]
2642    fn an_operation_the_schema_does_not_declare_is_refused_before_the_host_sees_it() {
2643        let (hosts, calls, _) = registry_with_wayward(Answer::Declared);
2644
2645        let error = hosts
2646            .call("wayward", "write", vec![Value(Repr::Str("input".into()))])
2647            .expect_err("an undeclared operation is refused");
2648
2649        assert_eq!(
2650            error.message,
2651            "host module `wayward` has no operation `write`"
2652        );
2653        let help = error.help.expect("the diagnostic lists what does exist");
2654        assert!(help.contains("`read`"), "{help}");
2655        assert!(help.contains("`open`"), "{help}");
2656        assert_eq!(calls.load(Ordering::Relaxed), 0);
2657    }
2658
2659    /// Arity is the first part of an operation's declared shape the boundary
2660    /// enforces, and it enforces it before the host is reached.
2661    #[test]
2662    fn arguments_the_schema_does_not_accept_are_refused_before_the_host_sees_them() {
2663        let (hosts, calls, _) = registry_with_wayward(Answer::Declared);
2664
2665        let error = hosts
2666            .call("wayward", "read", Vec::new())
2667            .expect_err("a call with too few arguments is refused");
2668
2669        assert_eq!(
2670            error.message,
2671            "`wayward.read` takes 1 argument, but 0 were given"
2672        );
2673        let help = error.help.expect("the diagnostic quotes the schema");
2674        assert!(
2675            help.contains("wayward.read(String) -> Result<String, Error>"),
2676            "{help}"
2677        );
2678        assert_eq!(calls.load(Ordering::Relaxed), 0);
2679    }
2680
2681    /// An argument the operation's own declaration does not admit is refused
2682    /// before the host sees it, and nothing of the call is recorded: a call
2683    /// stopped here never happened.
2684    ///
2685    /// This is the same table as the result check, read from the other side.
2686    /// `cove check` reports this mistake at the call site, where it has a
2687    /// span; the boundary reports it for the hosts the checker cannot see —
2688    /// which is every host an embedder writes.
2689    #[test]
2690    fn an_argument_the_schema_does_not_admit_is_refused_before_the_host_sees_it() {
2691        let (hosts, calls, sink) = registry_with_wayward(Answer::Declared);
2692
2693        let error = hosts
2694            .call("wayward", "read", vec![Value(Repr::Int(3))])
2695            .expect_err("an `Int` where a `String` was declared is refused");
2696
2697        assert_eq!(
2698            error.message,
2699            "`wayward.read` was given `Int` as argument 1, but its schema declares `String` there"
2700        );
2701        let help = error.help.expect("the diagnostic quotes the schema");
2702        assert!(
2703            help.contains("wayward.read(String) -> Result<String, Error>"),
2704            "{help}"
2705        );
2706        assert_eq!(calls.load(Ordering::Relaxed), 0);
2707        assert!(
2708            sink.events().is_empty(),
2709            "a call the boundary refused never reached the host to be recorded"
2710        );
2711    }
2712
2713    /// An argument is followed as far down as a result is, so an `Int` among
2714    /// the strings of a declared `Array<String>` is caught and the diagnostic
2715    /// says which element.
2716    #[test]
2717    fn a_violation_inside_an_argument_says_where_it_is() {
2718        let (hosts, calls, _) = registry_with_wayward(Answer::Declared);
2719
2720        let error = hosts
2721            .call(
2722                "wayward",
2723                "send",
2724                vec![Value(Repr::Array(
2725                    vec![Value(Repr::Str("one".into())), Value(Repr::Int(2))].into(),
2726                ))],
2727            )
2728            .expect_err("an `Int` among the declared strings is refused");
2729
2730        assert_eq!(
2731            error.message,
2732            "`wayward.send` was given `Int` at `[1]` of argument 1, but its schema declares `String` there"
2733        );
2734        assert_eq!(calls.load(Ordering::Relaxed), 0);
2735    }
2736
2737    /// A variadic operation's one declared parameter answers for every
2738    /// argument from its own position onwards, so the fourth `say` is checked
2739    /// against the same `String` the first one was.
2740    #[test]
2741    fn a_variadic_operation_checks_every_argument_against_its_declared_type() {
2742        let (hosts, _, _) = registry_with_wayward(Answer::Declared);
2743
2744        hosts
2745            .call(
2746                "wayward",
2747                "say",
2748                vec![Value(Repr::Str("a".into())), Value(Repr::Str("b".into()))],
2749            )
2750            .expect("strings all the way along are admitted");
2751
2752        let error = hosts
2753            .call(
2754                "wayward",
2755                "say",
2756                vec![Value(Repr::Str("a".into())), Value(Repr::Int(2))],
2757            )
2758            .expect_err("an `Int` among the declared strings is refused");
2759        assert_eq!(
2760            error.message,
2761            "`wayward.say` was given `Int` as argument 2, but its schema declares `String` there"
2762        );
2763    }
2764
2765    /// `Any` admits whatever it is given on the way in for the same reason it
2766    /// does on the way out: it is the type of an operation whose meaning does
2767    /// not depend on which value it was handed.
2768    #[test]
2769    fn an_argument_declared_any_admits_whatever_it_is_given() {
2770        let (hosts, calls, _) = registry_with_wayward(Answer::Declared);
2771
2772        for argument in [
2773            Value(Repr::Int(3)),
2774            Value(Repr::Unit),
2775            Value(Repr::Str("text".into())),
2776        ] {
2777            hosts
2778                .call("wayward", "bound", vec![argument])
2779                .expect("`Any` admits everything");
2780        }
2781        assert_eq!(calls.load(Ordering::Relaxed), 3);
2782    }
2783
2784    /// A host whose *result* violates its declared type is refused, and the
2785    /// diagnostic names the host that broke its word rather than the Cove
2786    /// code that would have received the value.
2787    ///
2788    /// `wayward.read` declares `Result<String, Error>` and answers `3`. ADR
2789    /// 0001 asks the schema to describe "argument, result, and error types"
2790    /// and to be "shared by the compiler, runtime, and CLI", and a
2791    /// description nothing enforces is a comment. The trace still records
2792    /// what the host did: the check refuses the value, not the fact.
2793    #[test]
2794    fn a_result_that_violates_its_declared_type_is_refused() {
2795        let (hosts, calls, sink) = registry_with_wayward(Answer::Undeclared);
2796
2797        let error = hosts
2798            .call("wayward", "read", vec![Value(Repr::Str("input".into()))])
2799            .expect_err("a host that breaks its own schema is refused");
2800
2801        assert_eq!(
2802            error.message,
2803            "`wayward.read` answered `Int`, but its schema declares `Result<String, Error>`"
2804        );
2805        let help = error.help.expect("the diagnostic quotes the schema");
2806        assert!(
2807            help.contains("wayward.read(String) -> Result<String, Error>"),
2808            "{help}"
2809        );
2810        assert_eq!(
2811            calls.load(Ordering::Relaxed),
2812            1,
2813            "the host was reached: this is a check on what it answered"
2814        );
2815        let events = sink.events();
2816        assert_eq!(events.len(), 1, "{events:?}");
2817        match &events[0] {
2818            TraceEvent::HostCall {
2819                granted, outcome, ..
2820            } => {
2821                assert!(granted);
2822                match outcome {
2823                    Some(HostOutcome::Value(recorded)) => assert_eq!(shown(recorded), "3"),
2824                    other => panic!("expected the answer on the record, found {other:?}"),
2825                }
2826            }
2827            other => panic!("expected a HostCall event, found {other:?}"),
2828        }
2829    }
2830
2831    /// The check follows the declared type's own recursion, so an array of
2832    /// the declared shape holding one element that is not is caught, and the
2833    /// diagnostic says which element.
2834    #[test]
2835    fn a_violation_inside_a_declared_result_says_where_it_is() {
2836        let (hosts, _, _) = registry_with_wayward(Answer::Undeclared);
2837
2838        let error = hosts
2839            .call("wayward", "list", Vec::new())
2840            .expect_err("an `Int` among the declared strings is refused");
2841
2842        assert_eq!(
2843            error.message,
2844            "`wayward.list` answered `Int` at `Ok(_)[1]` of its result, but its schema declares `String` there"
2845        );
2846    }
2847
2848    /// `Any` is not a missing type but the type of an operation whose meaning
2849    /// does not depend on which value it was given, so it admits whatever the
2850    /// host answers — including the `Int` every other declaration here
2851    /// refuses.
2852    #[test]
2853    fn a_result_declared_any_admits_whatever_the_host_answers() {
2854        for answer in [Answer::Declared, Answer::Undeclared] {
2855            let (hosts, _, _) = registry_with_wayward(answer);
2856
2857            let value = hosts
2858                .call("wayward", "anything", Vec::new())
2859                .expect("`Any` admits everything");
2860
2861            assert!(matches!(value, Value(Repr::Int(3))), "{value}");
2862        }
2863    }
2864
2865    /// A `Named` result is checked by the name the value carries, and a
2866    /// handle carries the module and kind it was issued for. So the lie the
2867    /// boundary used to catch only at the *next* call — a handle naming a
2868    /// resource kind the module never declared — is caught where it is told.
2869    #[test]
2870    fn a_handle_naming_a_kind_the_operation_did_not_declare_is_refused() {
2871        let (hosts, _, _) = registry_with_wayward(Answer::Undeclared);
2872
2873        let error = hosts
2874            .call("wayward", "open", Vec::new())
2875            .expect_err("a handle of an undeclared kind is refused where it is answered");
2876
2877        assert_eq!(
2878            error.message,
2879            "`wayward.open` answered `wayward.Ghost`, but its schema declares `wayward.Handle`"
2880        );
2881    }
2882
2883    /// A resource operation passes through the same choke point as a module
2884    /// operation, so it is held to its declaration the same way, and the
2885    /// diagnostic names it the way Cove source does.
2886    #[test]
2887    fn a_resource_operation_is_held_to_its_declaration_too() {
2888        let (hosts, _, _) = registry_with_wayward(Answer::Undeclared);
2889        let handle = ResourceHandle::new("wayward", &WAYWARD_RESOURCES[0], 1);
2890
2891        let error = hosts
2892            .call_resource(&handle, "close", Vec::new(), &mut NoReentry)
2893            .expect_err("a handle's operation that breaks its own schema is refused");
2894
2895        assert_eq!(
2896            error.message,
2897            "`wayward.Handle.close` answered `String` at `Ok(_)` of its result, but its schema declares `Unit` there"
2898        );
2899        let help = error.help.expect("the diagnostic quotes the schema");
2900        assert!(
2901            help.contains("wayward.Handle.close() -> Result<Unit, Error>"),
2902            "{help}"
2903        );
2904    }
2905}