Skip to main content

cove_schema/
lib.rs

1//! The machine-readable schemas the compiler and the runtime both read.
2//!
3//! There are two of them, and they are here for the same reason. The Host API
4//! schema, below, is what a host module declares about itself; [`builtins`] is
5//! what the language declares about its own types. Neither could live in
6//! `cove-runtime` or in `cove-sema`, because each is read by both and the
7//! dependency between those two runs one way.
8//!
9//! The two keep separate vocabularies on purpose. A host operation's
10//! signature is monomorphic, so [`HostType`] has no type parameters and needs
11//! none; a builtin's is generic, receiver-relative, and sometimes
12//! higher-order, so [`builtins::BuiltinType`] has all three and a host
13//! signature would have no use for them. [`builtins`] argues that at length.
14//!
15//! ADR 0001 states what the Host API half has to carry:
16//!
17//! > A machine-readable Host API schema is shared by the compiler, runtime,
18//! > and CLI. Each operation describes its argument, result, and error types;
19//! > capability; serialization and resource ownership; cancellation and
20//! > recordability; and whether it is a read, reversible write, or
21//! > irreversible write.
22//!
23//! and the Language Card adds the sentence that makes the schema load-bearing
24//! for tasks: "Host resources declare task-safety in their Host API schema."
25//!
26//! "Shared by the compiler, runtime, and CLI" is why this is a crate of its
27//! own and not a module of `cove-runtime`. `cove-sema` checks a host call
28//! against the same description the boundary dispatches it through, and the
29//! dependency between the two runs one way: the compiler must not gain a
30//! dependency on the runtime to say what it already knows. So the description
31//! moved below both of them, where each can read it, and `cove-runtime`
32//! re-exports it so a host written against the runtime still names one crate.
33//! Two lists that must agree and cannot see each other drift silently, which
34//! this repository has already paid for once; the fix that scales is one
35//! list.
36//!
37//! A schema entry is Rust data, not a parsed declaration, because a host is
38//! written in Rust: `HostApi::schema` returns a `'static` table, so a module
39//! and its declaration of itself cannot drift apart at run time. The shipped
40//! hosts' tables are in [`hosts`], and the module that implements each of
41//! them returns the table from there rather than one of its own.
42//!
43//! Only the parts of ADR 0001's list that something reads are modelled here.
44//! Serialization is left out: every value that crosses the boundary is an
45//! ordinary runtime value, and a field nothing consults is a claim nothing
46//! checks.
47//!
48//! Resource ownership is no longer among the omissions. A host may declare
49//! types of its own — [`TypeSchema`] for the ones that are plain data, and
50//! [`ResourceSchema`] for the ones the host keeps on the far side of the
51//! boundary — and ADR 0013 makes the second of those the whole of a resource
52//! handle's contract: which operations it answers, what capability each of
53//! them needs, and whether the handle may cross a task boundary.
54//!
55//! What a *value* has to be for a declared type to admit it is not here. That
56//! question needs values, which this crate has none of; `cove_runtime::schema`
57//! answers it, on the side of the boundary where values live.
58
59use std::fmt;
60
61pub mod builtins;
62pub mod hosts;
63
64pub use builtins::{builtin, free_builtin, is_builtin_type};
65pub use hosts::{module, shipped, HostSchemas};
66
67/// A type in a Host API signature, written in Cove's source vocabulary.
68///
69/// This is a small enum rather than `cove_syntax::ast::Type` because an
70/// `ast::Type` is a *parsed* type: every node carries a span into a source
71/// file and its path segments are identifiers with spans of their own, all of
72/// which would have to be invented for an operation that has no source to
73/// point at. The rendering vocabulary is the same one — [`fmt::Display`]
74/// produces the form the type would be written in Cove — so a signature
75/// printed from a schema entry reads exactly like a signature written by
76/// hand.
77///
78/// Add a variant when a host needs it; an unused variant is a type nobody can
79/// produce. That used to read "the variants cover exactly the types the
80/// shipped hosts use", and [`HostType::Set`] and [`HostType::Map`] are why it
81/// no longer does: no shipped host has needed either, and an embedder did
82/// ([issue #153](https://github.com/myuon/cove/issues/153)). An embedder is
83/// not a lesser kind of host — embedding is why `HostApi` is a trait — so the
84/// list is what a host may declare rather than what this workspace happens to
85/// ship, and `crates/cove-runtime/tests/embedding.rs` is where the two new
86/// ones are produced and consumed.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum HostType {
89    /// `Unit`, the value an operation returns when it returns nothing.
90    Unit,
91    /// `Bool`.
92    Bool,
93    /// `Int`, a signed 64-bit integer.
94    Int,
95    /// `String`.
96    String,
97    /// `Duration`, a signed count of nanoseconds.
98    Duration,
99    /// `Error`, the builtin error struct.
100    Error,
101    /// `Array<T>`, the fixed-length immutable sequence.
102    Array(&'static HostType),
103    /// `Set<T>`, the key-ordered immutable set.
104    ///
105    /// `T` must be one [`HostType::may_be_a_key`] allows, because a `Set`
106    /// element is a map key: [`ModuleSchema::validate`] is what refuses a
107    /// declaration that says otherwise, and it refuses it where the schema is
108    /// read rather than where a value is.
109    Set(&'static HostType),
110    /// `Map<K, V>`, the key-ordered immutable map.
111    ///
112    /// `K` carries the same restriction a [`HostType::Set`] element does, and
113    /// for the same reason; `V` carries none.
114    Map(&'static HostType, &'static HostType),
115    /// `Option<T>`.
116    Option(&'static HostType),
117    /// `Result<T, E>`. Expected failure is part of an operation's result
118    /// type, exactly as it is in Cove source, rather than a second channel
119    /// beside it.
120    Result(&'static HostType, &'static HostType),
121    /// A type the host declares, written qualified: `http.Response`.
122    ///
123    /// The name is the one Cove source writes, module included, because that
124    /// is what a signature in a diagnostic has to read as. Whether it names a
125    /// [`TypeSchema`] or a [`ResourceSchema`] is the host's business; a
126    /// signature says only which type it is.
127    Named(&'static str),
128    /// Any value at all.
129    ///
130    /// This is not a missing type: it is the type of an operation whose
131    /// meaning does not depend on which value it was given. `http.json`
132    /// renders whatever it is handed, and a callback a host stores and calls
133    /// later is a value the host never looks inside.
134    ///
135    /// What it promises, and what it costs, are different at the two ends of
136    /// a signature, and both are worth stating exactly.
137    ///
138    /// In a *parameter* it promises that every value is accepted: no
139    /// argument of any type is a mistake, the compiler rejects none, and the
140    /// boundary rejects none either. Nothing is given up by it, because
141    /// there was never a constraint to check.
142    ///
143    /// In a *result* it says the operation may answer with a value of any
144    /// type. That does cost something: from the call onwards the program
145    /// holds a value no schema described, so the compiler cannot prove what
146    /// a field read off it, a call made on it, or a place it is stored into
147    /// will do. Those are checked at run time and by nothing before it.
148    /// `cove check` reports each such call rather than letting the silence
149    /// pass for a proof — as a note, because a schema declaring `Any` is a
150    /// deliberate design decision and not a fault in the program that calls
151    /// it.
152    Any,
153}
154
155impl fmt::Display for HostType {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            HostType::Unit => f.write_str("Unit"),
159            HostType::Bool => f.write_str("Bool"),
160            HostType::Int => f.write_str("Int"),
161            HostType::String => f.write_str("String"),
162            HostType::Duration => f.write_str("Duration"),
163            HostType::Error => f.write_str("Error"),
164            HostType::Array(inner) => write!(f, "Array<{inner}>"),
165            HostType::Set(inner) => write!(f, "Set<{inner}>"),
166            HostType::Map(key, value) => write!(f, "Map<{key}, {value}>"),
167            HostType::Option(inner) => write!(f, "Option<{inner}>"),
168            HostType::Result(ok, error) => write!(f, "Result<{ok}, {error}>"),
169            HostType::Named(name) => f.write_str(name),
170            HostType::Any => f.write_str("Any"),
171        }
172    }
173}
174
175impl HostType {
176    /// Whether a value of this type may be a `Map` key or a `Set` element.
177    ///
178    /// Cove's own rule is `cove_runtime::value::MapKey`'s: "mutable handles
179    /// and structs containing them are not valid map keys", because a key's
180    /// equality must not change while a collection holds it. That rule is
181    /// about a *value*, and this is the most a *name* can say about it.
182    ///
183    /// Everything made only of the scalar types qualifies, and so does any
184    /// composition of qualifying types: an `Array`, an `Option`, a `Result`, a
185    /// `Set`, or a `Map` is a key exactly when everything nested inside it is.
186    ///
187    /// [`HostType::Named`] and [`HostType::Any`] do not, and the reason is the
188    /// same in both cases: neither says what its values are made of.
189    /// `cove_runtime::schema::Admits` checks a named type by the name the
190    /// value carries and deliberately looks no further — ADR 0013's amendment
191    /// draws that line — so a schema naming `reviews.PullRequest` has made no
192    /// claim about the ten fields behind it, and a `ResourceSchema`'s handle
193    /// can never be a key at all. `Any` says less again. A declaration that
194    /// promised more than the boundary checks would be a promise nothing
195    /// keeps.
196    pub fn may_be_a_key(&self) -> bool {
197        match self {
198            HostType::Unit
199            | HostType::Bool
200            | HostType::Int
201            | HostType::String
202            | HostType::Duration
203            | HostType::Error => true,
204            HostType::Array(item) | HostType::Set(item) | HostType::Option(item) => {
205                item.may_be_a_key()
206            }
207            HostType::Map(key, value) => key.may_be_a_key() && value.may_be_a_key(),
208            HostType::Result(ok, error) => ok.may_be_a_key() && error.may_be_a_key(),
209            HostType::Named(_) | HostType::Any => false,
210        }
211    }
212
213    /// The first part of this type that is declared as a key and cannot be
214    /// one.
215    ///
216    /// `Some(t)` names the offending key or element type rather than the
217    /// collection around it, because `t` is what a reader has to change.
218    fn unkeyable(&self) -> Option<HostType> {
219        match self {
220            HostType::Set(item) => {
221                if item.may_be_a_key() {
222                    item.unkeyable()
223                } else {
224                    Some(**item)
225                }
226            }
227            HostType::Map(key, value) => {
228                if key.may_be_a_key() {
229                    key.unkeyable().or_else(|| value.unkeyable())
230                } else {
231                    Some(**key)
232                }
233            }
234            HostType::Array(item) | HostType::Option(item) => item.unkeyable(),
235            HostType::Result(ok, error) => ok.unkeyable().or_else(|| error.unkeyable()),
236            _ => None,
237        }
238    }
239}
240
241/// A schema that declares something no value can be.
242///
243/// One kind of fault so far, and it is the one adding `Map` and `Set` to the
244/// vocabulary introduced: a key or an element position may only hold a type
245/// [`HostType::may_be_a_key`] allows. Everything else a `HostType` can say is
246/// satisfiable by construction.
247#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct SchemaFault {
249    /// Where in the module it was found, as a reader would name the place:
250    /// `reviews.pull`'s result, or `reviews.PullRequest.labels`.
251    pub place: String,
252    /// The whole declared type the fault was found in.
253    pub declared: HostType,
254    /// The part of it that is declared as a key and cannot be one.
255    pub key: HostType,
256}
257
258impl fmt::Display for SchemaFault {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(
261            f,
262            "{} is declared `{}`, and `{}` cannot be a `Map` key or a `Set` element",
263            self.place, self.declared, self.key
264        )
265    }
266}
267
268/// Whether an operation observes the world or changes it, and whether the
269/// change can be taken back.
270///
271/// ADR 0001 asks each operation to say which of the three it is. The
272/// distinction is about the world outside the run, not about the host's own
273/// bookkeeping: waiting is a [`Effect::Read`] because nothing outside the run
274/// is different afterwards.
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum Effect {
277    /// Observes state without changing it.
278    Read,
279    /// Changes state in a way the same host can put back.
280    ReversibleWrite,
281    /// Changes state nothing can put back, such as bytes already on a
282    /// terminal or a message already sent.
283    IrreversibleWrite,
284}
285
286impl fmt::Display for Effect {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            Effect::Read => f.write_str("read"),
290            Effect::ReversibleWrite => f.write_str("reversible write"),
291            Effect::IrreversibleWrite => f.write_str("irreversible write"),
292        }
293    }
294}
295
296/// One operation of one host module.
297///
298/// Every field is read by something: `HostRegistry::call` checks `name` and
299/// arity before it dispatches, holds a call to `params` before the host sees
300/// it and the host to `result` after it answers, `params` and `result` render
301/// the signature a diagnostic shows, `capability` is the gate, and
302/// `result_is_task_safe` answers the Language Card's rule for values leaving
303/// a host call for a task. `cove-sema` reads `params` and `result` too, at
304/// the call site, where a mistake still has a span to point at. `effect`,
305/// `cancellable`, and `recordable` are the three ADR 0001 facts whose
306/// consumers are named but not yet built — `cove replay` for `recordable`,
307/// `cove impact` for `effect`, and for `cancellable`, a host that could
308/// abandon a call in flight: a cancelled task stops at its next safepoint,
309/// which is after the call it is already inside returns.
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311pub struct OperationSchema {
312    /// The name Cove source calls, such as `println`.
313    pub name: &'static str,
314    /// Parameter types in declaration order.
315    ///
316    /// This is a promise both ends hold a call to rather than a label on it:
317    /// `cove check` checks each argument at its call site, and the boundary
318    /// checks them again for the hosts the checker cannot see.
319    pub params: &'static [HostType],
320    /// Whether the last parameter is variadic.
321    ///
322    /// Cove writes a variadic parameter `items: T...` and makes it an
323    /// immutable `Array<T>` inside the callee, so it accepts zero or more
324    /// arguments: a variadic operation's minimum arity is one less than
325    /// `params.len()`.
326    pub variadic: bool,
327    /// The type the operation produces.
328    ///
329    /// This is a promise the boundary holds the host to rather than a label
330    /// on it: `HostRegistry` checks what the host answered against this
331    /// before handing it on, so an operation cannot declare one type and
332    /// produce another. `cove_runtime::schema::Admits` says how far the check
333    /// goes.
334    pub result: HostType,
335    /// The capability a host must grant before this operation may be called.
336    pub capability: &'static str,
337    /// Whether the operation reads, writes reversibly, or writes
338    /// irreversibly.
339    pub effect: Effect,
340    /// Whether abandoning a call that is already in flight is meaningful and
341    /// safe. A wait can be abandoned because nothing has happened yet; a
342    /// write that has already reached the outside world cannot.
343    pub cancellable: bool,
344    /// Whether the call's result can be recorded and handed back later
345    /// without calling the host again, which is what `cove replay` needs.
346    ///
347    /// An operation that opens a resource is recordable, because ADR 0013
348    /// makes a handle a name rather than a live thing: what the trace records
349    /// is the identity the host issued, and a replay hands the same identity
350    /// back and answers the calls made on it from the trace too.
351    pub recordable: bool,
352    /// Whether the value this operation produces may cross a task boundary.
353    ///
354    /// The Language Card puts this decision here rather than in the value:
355    /// "Host resources declare task-safety in their Host API schema."
356    pub result_is_task_safe: bool,
357}
358
359impl OperationSchema {
360    /// The fewest arguments the operation accepts.
361    pub fn min_arity(&self) -> usize {
362        if self.variadic {
363            self.params.len().saturating_sub(1)
364        } else {
365            self.params.len()
366        }
367    }
368
369    /// Whether a call with `arity` arguments has the right number of them.
370    pub fn accepts(&self, arity: usize) -> bool {
371        if self.variadic {
372            arity >= self.min_arity()
373        } else {
374            arity == self.params.len()
375        }
376    }
377
378    /// The type declared for the argument at `index`.
379    ///
380    /// A variadic operation's last parameter answers for every argument from
381    /// its own position onwards, because that is what `items: T...` means:
382    /// one declared `T` and as many arguments of it as the call likes. An
383    /// index past a fixed operation's parameters has no declared type, which
384    /// is an arity mistake and reported as one.
385    pub fn param(&self, index: usize) -> Option<&'static HostType> {
386        self.params.get(index).or_else(|| {
387            if self.variadic {
388                self.params.last()
389            } else {
390                None
391            }
392        })
393    }
394
395    /// The signature, in the form it would be written in Cove source, without
396    /// the module qualifier: `println(String...) -> Result<Unit, Error>`.
397    pub fn signature(&self) -> String {
398        let mut params = self
399            .params
400            .iter()
401            .map(HostType::to_string)
402            .collect::<Vec<_>>();
403        if self.variadic {
404            if let Some(last) = params.last_mut() {
405                last.push_str("...");
406            }
407        }
408        format!("{}({}) -> {}", self.name, params.join(", "), self.result)
409    }
410
411    /// How many arguments this operation takes, phrased for a diagnostic.
412    pub fn expected_arity(&self) -> String {
413        let least = self.min_arity();
414        let noun = if least == 1 { "argument" } else { "arguments" };
415        if self.variadic {
416            format!("at least {least} {noun}")
417        } else {
418            format!("{least} {noun}")
419        }
420    }
421}
422
423/// One field of a host type.
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
425pub struct FieldSchema {
426    /// The label Cove source writes in the initializer, such as `method`.
427    pub name: &'static str,
428    /// The field's type.
429    pub ty: HostType,
430}
431
432/// The shape of one type a host declares.
433///
434/// A host type is ordinary data: `http.Method` is an enum whose cases carry
435/// nothing, and `http.Route` is a struct initialized with labels, exactly as a
436/// Cove struct is. Neither needs a representation of its own — the runtime
437/// builds an enum or a struct value whose type name is qualified by the module
438/// — so what the schema adds is only the vocabulary: which names exist and
439/// what they are made of.
440///
441/// A type whose values the host keeps rather than hands over is a
442/// [`ResourceSchema`] instead.
443#[derive(Clone, Copy, Debug, PartialEq, Eq)]
444pub struct TypeSchema {
445    /// The name Cove source writes after the module, such as `Route`.
446    pub name: &'static str,
447    /// The cases, for an enum. Empty for a struct.
448    pub cases: &'static [&'static str],
449    /// The fields, for a struct. Empty for an enum.
450    pub fields: &'static [FieldSchema],
451}
452
453impl TypeSchema {
454    /// Whether this is an enum, which is what having cases means.
455    pub fn is_enum(&self) -> bool {
456        !self.cases.is_empty()
457    }
458
459    /// The initializer, in the form it would be written in Cove source:
460    /// `Route(method: http.Method, path: String, handler: Any)`.
461    pub fn initializer(&self) -> String {
462        let fields = self
463            .fields
464            .iter()
465            .map(|field| format!("{}: {}", field.name, field.ty))
466            .collect::<Vec<_>>();
467        format!("{}({})", self.name, fields.join(", "))
468    }
469}
470
471/// One kind of host resource: a value the host owns and Cove only names.
472///
473/// ADR 0013 states the contract this carries. A handle is an identity, not
474/// state: the host keeps whatever a `database.Connection` really is, and Cove
475/// holds the name of it. So a resource declares three things and nothing
476/// else — what it is called, which operations it answers, and whether the
477/// name may cross a task boundary.
478///
479/// `task_safe` is the Language Card's sentence applied to a host's own types:
480/// "Host resources declare task-safety in their Host API schema." A resource
481/// whose state the host keeps behind a lock says `true`, and its name then
482/// crosses like any other immutable value; one whose state belongs to the
483/// task that opened it says `false`, and the name is refused at the boundary
484/// with the same diagnostic a vector gets.
485#[derive(Clone, Copy, Debug, PartialEq, Eq)]
486pub struct ResourceSchema {
487    /// The name Cove source writes after the module, such as `Connection`.
488    pub name: &'static str,
489    /// Whether a handle to this resource may cross a task boundary.
490    pub task_safe: bool,
491    /// The operations a handle answers, called as methods on it.
492    pub operations: &'static [OperationSchema],
493}
494
495impl ResourceSchema {
496    /// The operation `name`, if this resource has one.
497    pub fn operation(&self, name: &str) -> Option<&'static OperationSchema> {
498        self.operations.iter().find(|entry| entry.name == name)
499    }
500}
501
502/// The name, capability, operations, types, and resources of one host module.
503///
504/// This is the whole of what a module declares about itself, detached from
505/// any module: `cove-sema` reads it with no host to ask and no runtime to
506/// depend on, and `cove trace` and `cove replay` read it with nothing running
507/// at all. A live module is asked through `HostApi`, whose answers are these
508/// same tables.
509///
510/// # A schema assembled at run time is built once and leaked
511///
512/// Every field here is `&'static`, and so is every payload a [`HostType`]
513/// inside one points at. A schema written as a `const` — every module the
514/// toolchain ships, and every one in the tests — costs nothing for that: it
515/// was in the binary already, and being [`Copy`] is what lets a caller hold a
516/// schema while it goes on reading whatever it asked.
517///
518/// A module whose shape is only known once the process is running pays,
519/// though. A name from configuration, operations from a plugin manifest,
520/// resources from a table list discovered at connect time — none of that is
521/// `'static`, and `HostApi::module_schema` hands the table back by value, so
522/// there is nowhere to borrow from. Such a host assembles its table once, the
523/// first time it is asked, leaks it, and hands out the same copy afterwards:
524///
525/// ```
526/// # use cove_schema::{Effect, HostType, ModuleSchema, OperationSchema};
527/// # use std::sync::OnceLock;
528/// struct Plugin {
529///     /// The operations the manifest named, read once at startup.
530///     manifest: Vec<String>,
531///     /// The table they describe, assembled on the first ask and no other.
532///     schema: OnceLock<ModuleSchema>,
533/// }
534///
535/// impl Plugin {
536///     fn module_schema(&self) -> ModuleSchema {
537///         *self.schema.get_or_init(|| ModuleSchema {
538///             name: "plugin",
539///             capability: "plugin",
540///             operations: Vec::leak(
541///                 self.manifest
542///                     .iter()
543///                     .map(|name| OperationSchema {
544///                         name: String::leak(name.clone()),
545///                         params: &[HostType::String],
546///                         variadic: false,
547///                         result: HostType::Result(&HostType::String, &HostType::Error),
548///                         capability: "plugin",
549///                         effect: Effect::Read,
550///                         cancellable: false,
551///                         recordable: true,
552///                         result_is_task_safe: true,
553///                     })
554///                     .collect::<Vec<_>>(),
555///             ),
556///             types: &[],
557///             resources: &[],
558///         })
559///     }
560/// }
561/// ```
562///
563/// The [`OnceLock`](std::sync::OnceLock) is the whole of the discipline, and
564/// it is what makes the cost a bounded one. A handful of allocations per
565/// module for the life of a process is what an in-process embedding
566/// registered at startup pays, once; a host that assembles its table inside
567/// `module_schema` instead pays it again on every call the registry
568/// dispatches, which is not bounded by anything.
569/// `crates/cove-runtime/tests/embedding.rs` runs the pattern end to end and
570/// asserts the bound.
571///
572/// # Why the fields are `&'static` and not `&'a`
573///
574/// [Issue #86](https://github.com/myuon/cove/issues/86) asked for
575/// `ModuleSchema<'a>` and called it the principled fix. It is not a fix. It
576/// would spread a lifetime through every crate that names this type and
577/// leave the leak where it was, because neither of the two things a host
578/// could borrow a schema from is available to it.
579///
580/// It cannot borrow from itself. A host holding `names: Vec<String>` beside
581/// `operations: Vec<OperationSchema<'a>>` needs `'a` to be the lifetime of
582/// the field next to it, which is a self-referential struct and not
583/// something safe Rust builds.
584///
585/// It cannot borrow from anything longer-lived either, because a registered
586/// module is a `Box<dyn HostApi>`, which is `Box<dyn HostApi + 'static>`. It
587/// has to be: ADR 0008 gives every spawned task a thread of its own,
588/// `std::thread::Builder::spawn` takes a `'static` closure, and that closure
589/// holds the `Arc<Runtime>` that holds the registry. A registry that
590/// borrowed its modules would be a run that could not spawn a task.
591///
592/// The representation that *would* remove the leak is the other one: a
593/// schema that owns what it describes, so a host keeps one in a field and
594/// hands back `&self.schema`. What rules it out is not the reason issue #86
595/// gives. `Cow::Borrowed` is const-constructible, so the shipped tables
596/// could stay `const` — though the recursive [`HostType`] payloads would
597/// need a hand-written `Static | Shared` pair beside it, because
598/// `Cow<'static, HostType>` is a layout cycle. What rules it out is the
599/// price. Some 260 fields across `hosts.rs` stop being written as literals
600/// and start being written as constructor calls, in a table that is
601/// hand-written because being read by hand is the point of it. [`Copy`]
602/// goes, and with it the shape of every reader that holds a schema while it
603/// goes on working: `HostRegistry::host_type` hands the interpreter an entry
604/// rather than a borrow precisely because the interpreter is about to
605/// evaluate arguments, which it cannot do while borrowing the registry. And
606/// a clone of an owned half is a deep copy where a copy of a static one was
607/// free. A trait with two implementations pays the same noise for a dynamic
608/// call on every read, and gives one description two vocabularies — the
609/// drift this crate exists to prevent.
610///
611/// So the tables stay literals, the readers stay [`Copy`], and the leak
612/// stays: bounded, documented here, and exercised by a test.
613#[derive(Clone, Copy, Debug, PartialEq, Eq)]
614pub struct ModuleSchema {
615    /// The name Cove source uses, such as `console`.
616    pub name: &'static str,
617    /// The capability a host must grant for this module.
618    ///
619    /// A capability is a plain name here rather than `cove_sema::Capability`,
620    /// because that type belongs to the crate that reads `cove.toml` and this
621    /// one sits below it.
622    pub capability: &'static str,
623    /// Every operation the module exposes.
624    pub operations: &'static [OperationSchema],
625    /// Every type the module declares.
626    pub types: &'static [TypeSchema],
627    /// Every kind of resource the module can open.
628    pub resources: &'static [ResourceSchema],
629}
630
631impl ModuleSchema {
632    /// The operation `name`, if this module exposes one.
633    pub fn operation(&self, name: &str) -> Option<&'static OperationSchema> {
634        self.operations.iter().find(|entry| entry.name == name)
635    }
636
637    /// The type `name`, if this module declares one that is plain data.
638    pub fn declared_type(&self, name: &str) -> Option<&'static TypeSchema> {
639        self.types.iter().find(|entry| entry.name == name)
640    }
641
642    /// The kind of resource `name`, if this module can open one.
643    pub fn resource(&self, name: &str) -> Option<&'static ResourceSchema> {
644        self.resources.iter().find(|entry| entry.name == name)
645    }
646
647    /// Whether this module declares `name` as a type of its own, either as
648    /// plain data or as a resource it keeps.
649    ///
650    /// The two are one question wherever a name is being read rather than
651    /// used: `http.Response` and `http.Server` are both written the same way
652    /// in a signature, and which of them the host keeps is the host's
653    /// business.
654    pub fn declares_type(&self, name: &str) -> bool {
655        self.declared_type(name).is_some() || self.resource(name).is_some()
656    }
657
658    /// Whether every type this module declares is one some value could be.
659    ///
660    /// There is one way to write a type that nothing can satisfy, and
661    /// [`HostType::Set`] and [`HostType::Map`] are what introduced it: a `Set`
662    /// element and a `Map` key have to satisfy Cove's `MapKey` restriction,
663    /// and [`HostType::may_be_a_key`] says which declarations do.
664    ///
665    /// It is checked here, where a schema is *read*, rather than at the
666    /// boundary where a value is. A `Set<reviews.PullRequest>` the boundary
667    /// refused would be refused on the first call that carried one, in
668    /// production, in whichever operation happened to come first — which is
669    /// the failure mode ADR 0017 moved a Host API description out of the
670    /// runtime to prevent. Read here it is one sentence naming the field.
671    ///
672    /// Every table this workspace ships is held to this by
673    /// `cove_schema::hosts`'s own tests. An embedder's table is the
674    /// embedder's, so an embedder calls this on it — one assertion in the test
675    /// that already exists is enough, and
676    /// `examples/rules/host/tests/embedding.rs` is where that is written down.
677    pub fn validate(&self) -> Result<(), SchemaFault> {
678        let operations = self
679            .operations
680            .iter()
681            .map(|entry| (self.name.to_string(), entry))
682            .chain(self.resources.iter().flat_map(|resource| {
683                resource
684                    .operations
685                    .iter()
686                    .map(move |entry| (format!("{}.{}", self.name, resource.name), entry))
687            }));
688        for (owner, entry) in operations {
689            for (index, param) in entry.params.iter().enumerate() {
690                fault(
691                    format!("argument {} of `{owner}.{}`", index + 1, entry.name),
692                    param,
693                )?;
694            }
695            fault(
696                format!("the result of `{owner}.{}`", entry.name),
697                &entry.result,
698            )?;
699        }
700        for declared in self.types {
701            for field in declared.fields {
702                fault(
703                    format!("`{}.{}.{}`", self.name, declared.name, field.name),
704                    &field.ty,
705                )?;
706            }
707        }
708        Ok(())
709    }
710}
711
712/// The fault `declared` carries at `place`, if it carries one.
713fn fault(place: String, declared: &HostType) -> Result<(), SchemaFault> {
714    match declared.unkeyable() {
715        Some(key) => Err(SchemaFault {
716            place,
717            declared: *declared,
718            key,
719        }),
720        None => Ok(()),
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    const READ_A_STRING: OperationSchema = OperationSchema {
729        name: "read",
730        params: &[HostType::String],
731        variadic: false,
732        result: HostType::Result(&HostType::String, &HostType::Error),
733        capability: "documents",
734        effect: Effect::Read,
735        cancellable: false,
736        recordable: true,
737        result_is_task_safe: true,
738    };
739
740    const PRINT_MANY: OperationSchema = OperationSchema {
741        name: "println",
742        params: &[HostType::String],
743        variadic: true,
744        result: HostType::Result(&HostType::Unit, &HostType::Error),
745        capability: "console",
746        effect: Effect::IrreversibleWrite,
747        cancellable: false,
748        recordable: true,
749        result_is_task_safe: true,
750    };
751
752    #[test]
753    fn types_render_in_cove_source_form() {
754        assert_eq!(HostType::Unit.to_string(), "Unit");
755        assert_eq!(HostType::Bool.to_string(), "Bool");
756        assert_eq!(HostType::Int.to_string(), "Int");
757        assert_eq!(HostType::Duration.to_string(), "Duration");
758        assert_eq!(
759            HostType::Array(&HostType::String).to_string(),
760            "Array<String>"
761        );
762        assert_eq!(
763            HostType::Option(&HostType::String).to_string(),
764            "Option<String>"
765        );
766        assert_eq!(
767            HostType::Result(&HostType::Unit, &HostType::Error).to_string(),
768            "Result<Unit, Error>"
769        );
770        assert_eq!(HostType::Set(&HostType::String).to_string(), "Set<String>");
771        assert_eq!(
772            HostType::Map(&HostType::String, &HostType::Int).to_string(),
773            "Map<String, Int>"
774        );
775    }
776
777    // ------------------------------------------- what may be a key, and why
778    //
779    // A `Set` element and a `Map` key have to satisfy Cove's `MapKey`
780    // restriction. These pin what a *name* can promise about that, which is
781    // less than what a value can be held to and is the whole of what a schema
782    // gets to say.
783
784    #[test]
785    fn a_type_made_of_scalars_may_be_a_key() {
786        for scalar in [
787            HostType::Unit,
788            HostType::Bool,
789            HostType::Int,
790            HostType::String,
791            HostType::Duration,
792            HostType::Error,
793        ] {
794            assert!(scalar.may_be_a_key(), "{scalar}");
795        }
796        assert!(HostType::Array(&HostType::String).may_be_a_key());
797        assert!(HostType::Option(&HostType::Int).may_be_a_key());
798        assert!(HostType::Set(&HostType::String).may_be_a_key());
799        assert!(HostType::Map(&HostType::String, &HostType::Int).may_be_a_key());
800        assert!(HostType::Result(&HostType::Int, &HostType::Error).may_be_a_key());
801    }
802
803    /// Neither says what its values are made of, so neither can promise the
804    /// one thing a key position needs.
805    #[test]
806    fn a_named_type_and_any_may_not_be_a_key() {
807        assert!(!HostType::Named("reviews.PullRequest").may_be_a_key());
808        assert!(!HostType::Any.may_be_a_key());
809        assert!(!HostType::Array(&HostType::Any).may_be_a_key());
810        assert!(!HostType::Set(&HostType::Named("reviews.PullRequest")).may_be_a_key());
811    }
812
813    /// The rule is only about the key half. A map from a name to a pull
814    /// request is ordinary, and only a map *keyed* by one is not.
815    #[test]
816    fn a_module_declaring_a_key_no_value_can_be_is_refused_where_it_is_read() {
817        const KEYED_BY_A_STRUCT: ModuleSchema = ModuleSchema {
818            name: "reviews",
819            capability: "reviews",
820            operations: &[],
821            types: &[TypeSchema {
822                name: "Board",
823                cases: &[],
824                fields: &[FieldSchema {
825                    name: "open",
826                    ty: HostType::Set(&HostType::Named("reviews.PullRequest")),
827                }],
828            }],
829            resources: &[],
830        };
831        let fault = KEYED_BY_A_STRUCT
832            .validate()
833            .expect_err("a set of a named type is not a set anything can be");
834        assert_eq!(
835            fault.to_string(),
836            "`reviews.Board.open` is declared `Set<reviews.PullRequest>`, and `reviews.PullRequest` cannot be a `Map` key or a `Set` element"
837        );
838
839        const VALUED_BY_A_STRUCT: ModuleSchema = ModuleSchema {
840            types: &[TypeSchema {
841                name: "Board",
842                cases: &[],
843                fields: &[FieldSchema {
844                    name: "open",
845                    ty: HostType::Map(&HostType::String, &HostType::Named("reviews.PullRequest")),
846                }],
847            }],
848            ..KEYED_BY_A_STRUCT
849        };
850        assert!(VALUED_BY_A_STRUCT.validate().is_ok());
851    }
852
853    /// An operation's own signature is read the same way a declared type's
854    /// fields are, and the place a fault names is the one a reader has to go
855    /// and edit.
856    #[test]
857    fn an_operation_s_signature_is_read_for_the_same_fault() {
858        const TAKES_ONE: OperationSchema = OperationSchema {
859            name: "post",
860            params: &[HostType::Set(&HostType::Any)],
861            variadic: false,
862            result: HostType::Unit,
863            capability: "reviews",
864            effect: Effect::Read,
865            cancellable: false,
866            recordable: true,
867            result_is_task_safe: true,
868        };
869        const MODULE: ModuleSchema = ModuleSchema {
870            name: "reviews",
871            capability: "reviews",
872            operations: &[TAKES_ONE],
873            types: &[],
874            resources: &[],
875        };
876        assert_eq!(
877            MODULE
878                .validate()
879                .expect_err("`Any` promises nothing about a key")
880                .place,
881            "argument 1 of `reviews.post`"
882        );
883
884        const ANSWERS_ONE: ModuleSchema = ModuleSchema {
885            operations: &[OperationSchema {
886                params: &[],
887                result: HostType::Result(
888                    &HostType::Map(&HostType::Named("reviews.PullRequest"), &HostType::Int),
889                    &HostType::Error,
890                ),
891                ..TAKES_ONE
892            }],
893            ..MODULE
894        };
895        assert_eq!(
896            ANSWERS_ONE
897                .validate()
898                .expect_err("a map keyed by a named type is not one either")
899                .place,
900            "the result of `reviews.post`"
901        );
902    }
903
904    #[test]
905    fn a_fixed_operation_accepts_exactly_its_parameters() {
906        assert!(!READ_A_STRING.accepts(0));
907        assert!(READ_A_STRING.accepts(1));
908        assert!(!READ_A_STRING.accepts(2));
909        assert_eq!(READ_A_STRING.min_arity(), 1);
910        assert_eq!(READ_A_STRING.expected_arity(), "1 argument");
911    }
912
913    #[test]
914    fn a_variadic_operation_accepts_zero_or_more() {
915        assert!(PRINT_MANY.accepts(0));
916        assert!(PRINT_MANY.accepts(1));
917        assert!(PRINT_MANY.accepts(7));
918        assert_eq!(PRINT_MANY.min_arity(), 0);
919        assert_eq!(PRINT_MANY.expected_arity(), "at least 0 arguments");
920    }
921
922    /// The declared type of an argument, which is what both ends check one
923    /// against. A variadic parameter answers for every argument from its own
924    /// position onwards; a fixed one answers for exactly its own.
925    #[test]
926    fn a_parameter_answers_for_the_argument_at_its_position() {
927        assert_eq!(READ_A_STRING.param(0), Some(&HostType::String));
928        assert_eq!(READ_A_STRING.param(1), None);
929
930        assert_eq!(PRINT_MANY.param(0), Some(&HostType::String));
931        assert_eq!(PRINT_MANY.param(6), Some(&HostType::String));
932    }
933
934    #[test]
935    fn signatures_read_like_source() {
936        assert_eq!(
937            READ_A_STRING.signature(),
938            "read(String) -> Result<String, Error>"
939        );
940        assert_eq!(
941            PRINT_MANY.signature(),
942            "println(String...) -> Result<Unit, Error>"
943        );
944    }
945}