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}