Skip to main content

cove_runtime/vm/
mod.rs

1//! The execution backend [ADR 0034](../../../../docs/adr/0034-one-physical-word-stack.md)
2//! decided on, and since the cutover, the only one: this is the production
3//! path an embedder runs a Cove program on.
4//!
5//! [`docs/LINEAR_VM.md`](../../../../docs/LINEAR_VM.md) is the design. It was
6//! written as a clean-room replacement rather than a renovation: nothing here
7//! was derived from the backend it replaced, and at the cutover that backend
8//! was deleted. `lvm` and `cove-lir` were transitional spellings, worn while
9//! the predecessor still held these names; it is gone, and this module and
10//! `cove-ir` have taken them.
11//!
12//! Two things leave this module: [`Vm`], the type an embedder holds, and
13//! [`exec::SAFEPOINT_STRIDE`], a number a test asserts a bound against.
14//! Nothing else does, because what a caller can name is the whole of what
15//! this boundary decides. A word, a layout, a [`mem::Memory`] and a
16//! [`exec::Machine`] are the representation, and a representation that leaves
17//! the crate is one that cannot be changed without changing somebody else's
18//! code.
19//!
20//! # What a run owns, and what each of its tasks owns
21//!
22//! Issue #240's Q1 answers this and [`docs/LINEAR_VM.md`](../../../../docs/LINEAR_VM.md)'s
23//! "Ownership" section writes it out. Five things, and which of them is a
24//! value store is the whole of what ADR 0034 cares about:
25//!
26//! | | belongs to | where it is | a value store? |
27//! |---|---|---|---|
28//! | the object heap | the **run** | [`mem::Space`], one per run, behind an `Arc` | **yes**, and the only one |
29//! | a stack segment | a **task** | `[k * SEGMENT_WORDS, (k+1) * SEGMENT_WORDS)` of the same address space | yes, and it is part of the same one |
30//! | a `Shared` cell | the **run**'s heap | an ordinary object; its lock is one of its words ([`cell`]) | no — it *is* an object in the heap |
31//! | a host resource | the **host** | a table of names, shared by every task | no — see ADR 0031 |
32//! | a `Task`, a `TaskScope` | the **scheduler** | a table of control state, one per task | no |
33//!
34//! The last row is the one that has to be argued rather than asserted, because
35//! a `Task` is a value a Cove program writes down. Its word is **one past an
36//! index into a scheduler table**, the way a `Repr::Host` word is, and the
37//! entry it names holds a task id, a scope name and position, a
38//! `Cancellation`, and the *address* of the heap object holding its answer.
39//! Every one of those but the last is scheduler bookkeeping that no Cove value
40//! could be hidden in. The last is an address, which makes the table a **root
41//! provider** and not a second store: the answer's words are in the run's
42//! heap, in an object the spawning task allocated before the thread existed,
43//! and the table names one the way `Machine::literal_addrs` names a literal's
44//! address: an index into the run's own metadata, not a second store.
45//! Nothing that wanted to dodge a heap representation could be put there,
46//! which is the test ADR 0034 actually applies.
47//!
48//! # The scheduler's table is a *task's*, and the host's is the *run's*
49//!
50//! The two look alike — a word one past an index into a table of names — and
51//! they are owned differently, for a reason each states about itself rather
52//! than by analogy.
53//!
54//! A `Task` and a `TaskScope` may not cross a task boundary; the task-safety
55//! rule says so, and `cove_sema`'s `task_safe_offender` is where it is
56//! enforced. So a word formed in one task is only ever read in that task, the
57//! tables are disjoint by construction, and there is nothing to share. That is
58//! the same arithmetic that keeps two stack segments apart, and it is why
59//! [`exec::Machine`] holds its own.
60//!
61//! A **host resource** does cross: ADR 0013 gives the host the record of what
62//! is open, a resource declares its own task-safety in its schema, and a
63//! task-safe one is copied into a spawned closure like any other value — as
64//! its word. A table of one task's own would make that word an index into a
65//! list the receiving task does not have. So the resource table is the run's,
66//! behind a lock, and one resource is one word for the length of the run,
67//! which is what ADR 0013's *"two handles are equal when they name the same
68//! resource"* costs once there is more than one thread.
69//!
70//! There used to be a single `#![allow(dead_code)]` here, covering every
71//! submodule beneath it. Its own comment was honest about what it was *for*
72//! — several items below are reached only from their own `#[cfg(test)]`
73//! code, and one line in one place meant removing it was a single edit whose
74//! failure would list exactly what was still unused. What it did not say was
75//! that it covered far more than those items, because a module-wide allow
76//! does not distinguish "reached only by a test" from "reached by nothing at
77//! all": [ADR 0043](../../../../docs/adr/0043-a-method-moves-if-it-is-total-and-takes-no-closure.md)'s
78//! third migration condition is checked by deleting a builtin's dispatch arm
79//! and asking clippy whether the implementation it leaves behind is now
80//! unreachable, and inside this module clippy could never answer, allow or
81//! no. It was removed for that reason (issue #274).
82//!
83//! What replaces it is one `#[cfg_attr(not(test), allow(dead_code))]` per
84//! item that is genuinely reached only from this crate's own tests, each
85//! with a comment saying so beside it. That form says under `cargo test`
86//! exactly what the broad allow said all the time — nothing, because the
87//! item is used — and only turns the lint off for the build that has no
88//! caller, which is the build the check above runs against. A handful of
89//! items had no caller at all, not even a test; those were deleted rather
90//! than annotated, which is what removing the broad allow was for.
91
92use std::rc::Rc;
93
94use cove_diag::Span;
95use cove_ir::{Function, FunctionId, Program};
96
97use crate::budget::{Budget, Limits, Meter};
98use crate::error::RuntimeError;
99use crate::host::HostRegistry;
100use crate::runtime::Runtime;
101use crate::trace::{RunOutcome, Timing, TraceEvent};
102use crate::vm::debug::Debugger;
103use crate::vm::exec::Machine;
104// The public `Value` reaches this file for the one reason ADR 0034 allows it
105// to reach any of them: this is a boundary. An entry's arguments and its
106// answer are what a host hands in and reads back, and they are `Value`s on
107// both sides of that line. Nothing here stores one — every value named below
108// is on its way into [`boundary::from_value`] or out of
109// [`boundary::to_value`].
110use crate::value::Value;
111
112pub(crate) mod boundary;
113pub(crate) mod builtins;
114pub(crate) mod cell;
115pub(crate) mod debug;
116#[cfg(test)]
117mod differential;
118#[cfg(test)]
119mod erasure;
120pub(crate) mod exec;
121pub(crate) mod mem;
122pub mod profile;
123pub(crate) mod render;
124
125/// The words a run's heap region may grow to, for every [`Vm`] [`Vm::new`]
126/// builds. [`Vm::with_heap_words`] is the one way to build a run over a
127/// different budget, and its own doc comment says who that is for.
128///
129/// Four mebiwords, thirty-two mebibytes. Reserved is not committed: the
130/// backing store grows on demand, so a program that allocates nothing pays
131/// nothing, and what the number buys is a run that fails with "this run has no
132/// memory left" rather than taking the machine down with it. Like
133/// [`mem::STACK_WORDS`] it is an implementation choice and not a language
134/// fact.
135const DEFAULT_HEAP_WORDS: usize = 1 << 22;
136
137/// One run of a lowered program.
138///
139/// This is the type above the machine: it holds the program, the memory the
140/// run executes over, and the two things that make a run a run rather than a
141/// dispatch loop — the boundary a `Value` crosses, and the accounting a
142/// safepoint charges. The dispatch loop underneath knows nothing about any
143/// of them.
144///
145/// The two ways in are the two the language has, and they are the same two
146/// [`crate::interp::Interpreter`] offers. [`Vm::run_entry`] is how a
147/// *command* speaks to a program: the arguments are process arguments, which
148/// are strings. [`Vm::invoke`] is how an *application* does: the arguments
149/// are values the host built, held to the types the checker resolved before
150/// the first instruction runs. Everything below the two is one path.
151pub struct Vm<'a> {
152    runtime: &'a Runtime,
153    hosts: &'a HostRegistry,
154    program: &'a Program,
155    machine: Machine<'a>,
156    /// The run's accounting, in the handle a safepoint charges through.
157    ///
158    /// Taken once, where the run begins, for the reason [`Meter`] gives. A
159    /// registry with no budget installed answers `None`, which has always
160    /// meant no limit; a meter over default [`Limits`] is that, written down.
161    budget: Meter,
162}
163
164impl<'a> Vm<'a> {
165    /// A run of `program`, over `runtime`'s checked program and `hosts`.
166    ///
167    /// `program` is **encoded and verified here**, once, into the fixed-width
168    /// form [ADR 0041](../../../../docs/adr/0041-a-slot-number-fits-in-sixteen-bits.md)
169    /// decides and issue #245's Phase 5 made the only one a run executes.
170    /// There is no second representation to choose and no flag that selects
171    /// one: `Inst` is what the lowering produced and what a listing and the
172    /// debugger show, and what runs is sixteen bytes per instruction whose
173    /// operands were checked before the first of them ran.
174    ///
175    /// This stays infallible, and what that costs is stated where it is
176    /// paid. A program with no encoding — one whose frame is wider than a
177    /// sixteen-bit slot names, which `cove_ir::lower` already refuses with a
178    /// diagnostic — is refused by [`Vm::run_entry`] and [`Vm::invoke`]
179    /// before a frame is pushed, rather than by this constructor. The
180    /// alternative was a `Result` at every call site for a failure the
181    /// compiler in front of it has already made impossible.
182    ///
183    /// The heap budget is this module's `DEFAULT_HEAP_WORDS`. [`Vm::with_heap_words`]
184    /// is the constructor for a caller that needs a different one.
185    pub fn new(runtime: &'a Runtime, hosts: &'a HostRegistry, program: &'a Program) -> Vm<'a> {
186        Vm::with_heap_words(runtime, hosts, program, DEFAULT_HEAP_WORDS)
187    }
188
189    /// The same run, over a heap that may grow only to `heap_words` words
190    /// rather than `DEFAULT_HEAP_WORDS`.
191    ///
192    /// This is deliberately not a [`Limits`] field. [ADR 0011](../../../../docs/adr/0011-garbage-collection.md)'s
193    /// amendment retracted `Limits::max_memory` because a number that bounds
194    /// only what one collector's table can see is not a memory ceiling; it
195    /// is that instrument's readout wearing a ceiling's name. Nothing about
196    /// the linear-memory backend changes that argument for an *embedder*: its
197    /// heap is a fuller account of a run's Cove-owned values than the old
198    /// per-task heap ever was, per ADR 0034, but a Host's own allocations,
199    /// open resources and each task's stack region still sit outside it, so
200    /// naming `heap_words` beside `fuel` and `max_host_calls` would still
201    /// promise a bound this number cannot back.
202    ///
203    /// What this constructor is for is what `mem::STACK_WORDS` already is
204    /// — an implementation choice a test may need to name to provoke the
205    /// behaviour it bounds, not a knob an embedder is invited to reach for.
206    /// Prefer [`Vm::new`] unless the caller is deliberately forcing a small
207    /// heap so a collection has something to be tested against.
208    pub fn with_heap_words(
209        runtime: &'a Runtime,
210        hosts: &'a HostRegistry,
211        program: &'a Program,
212        heap_words: usize,
213    ) -> Vm<'a> {
214        Vm {
215            runtime,
216            hosts,
217            program,
218            machine: Machine::for_run(program, heap_words, Some(hosts), Some(runtime)),
219            budget: meter_of(hosts),
220        }
221    }
222
223    /// The same run, watched by `debugger`.
224    ///
225    /// A second constructor rather than a parameter on [`Vm::new`], for the
226    /// reason the heap budget is not one either: no existing caller has a
227    /// debugger to name, and a parameter every caller passes `None` to is a
228    /// question every caller is asked and none of them answers.
229    ///
230    /// What it costs the run is stated where it is paid, in
231    /// the debugger's own module: the machine asks before **every** instruction
232    /// for as long as the debugger is installed, so a debugged run is slower
233    /// by whatever the debugger does per instruction. A run built with
234    /// [`Vm::new`] is unchanged — the loop's comparison is the same one it
235    /// was, against the next safepoint.
236    pub fn debugged(
237        runtime: &'a Runtime,
238        hosts: &'a HostRegistry,
239        program: &'a Program,
240        debugger: &'a dyn Debugger,
241    ) -> Vm<'a> {
242        let mut vm = Vm::new(runtime, hosts, program);
243        vm.machine.watch(Some(debugger));
244        vm
245    }
246
247    /// Runs `module.name` with the process arguments `args`.
248    ///
249    /// An entry takes either no parameters or one `Array<String>`, and that
250    /// rule is the language's rather than a backend's — the oracle refuses
251    /// the third shape in these words, at this span.
252    pub fn run_entry(
253        &mut self,
254        module: &str,
255        name: &str,
256        args: Vec<Rc<str>>,
257    ) -> Result<Value, RuntimeError> {
258        let outcome = self.enter(module, name, args);
259        self.ended(outcome)
260    }
261
262    /// Calls `module.name` with values the host built.
263    ///
264    /// The arguments are held to what the checker resolved about the
265    /// declaration — the shape it has to be callable at all, the count, and
266    /// each value's type followed as deeply as the type goes — before
267    /// anything runs. That check is the crate's own `invoke`, shared with the
268    /// oracle so that a host that gets it wrong reads one answer and not one
269    /// per backend.
270    ///
271    /// One refusal belongs to this backend rather than to the language, and
272    /// it is about the *lowering* rather than about the program.
273    /// [`cove_ir::lower_entry`] lowers what one entry can reach and nothing
274    /// else, so a run built for one entry cannot invoke a function no path
275    /// from that entry leads to. Saying the package does not declare it would
276    /// be false and would send an embedder to the wrong file, so this says
277    /// which of the two is missing and what to lower instead.
278    pub fn invoke(
279        &mut self,
280        module: &str,
281        name: &str,
282        args: Vec<Value>,
283    ) -> Result<Value, RuntimeError> {
284        let outcome = self.invoke_checked(module, name, args);
285        self.ended(outcome)
286    }
287
288    /// [`Vm::run_entry`], bounded by `budget` and by nothing else.
289    ///
290    /// The command-shaped way in, bounded the way [`Vm::invoke_within`]
291    /// bounds the application-shaped one. Issue #152 is why both exist: an
292    /// application that runs somebody else's Cove wants the *request*
293    /// bounded, not the session, and a session is built once and invoked
294    /// many times.
295    pub fn run_entry_within(
296        &mut self,
297        budget: Budget,
298        module: &str,
299        name: &str,
300        args: Vec<Rc<str>>,
301    ) -> Result<Value, RuntimeError> {
302        self.hosts.begin_run(budget);
303        self.bind_budget();
304        let outcome = self.enter(module, name, args);
305        self.ended(outcome)
306    }
307
308    /// [`Vm::invoke`], bounded by `budget` and by nothing else.
309    ///
310    /// The check runs before the budget is installed, so a call refused for a
311    /// wrong argument spends none of the budget it was handed and leaves
312    /// whatever bounded this backend where it was.
313    pub fn invoke_within(
314        &mut self,
315        budget: Budget,
316        module: &str,
317        name: &str,
318        args: Vec<Value>,
319    ) -> Result<Value, RuntimeError> {
320        let outcome = self.checked_within(budget, module, name, args);
321        self.ended(outcome)
322    }
323
324    /// The check, the budget, and then the call.
325    fn checked_within(
326        &mut self,
327        budget: Budget,
328        module: &str,
329        name: &str,
330        args: Vec<Value>,
331    ) -> Result<Value, RuntimeError> {
332        crate::invoke::check(self.runtime.program(), module, name, &args)?;
333        self.hosts.begin_run(budget);
334        self.bind_budget();
335        let id = self.lowered(module, name)?;
336        self.enter_with(module, name, id, args)
337    }
338
339    /// Re-reads the meter after the registry was given a new budget.
340    ///
341    /// The handle is taken once where a run begins, so installing a budget
342    /// for one invocation has to be followed by taking the handle again;
343    /// otherwise the safepoints would go on charging the budget the session
344    /// was built over.
345    fn bind_budget(&mut self) {
346        self.budget = meter_of(self.hosts);
347    }
348
349    /// How many instructions this run has executed.
350    pub fn instructions(&self) -> u64 {
351        self.machine.instructions()
352    }
353
354    /// Words the heap region occupies, free blocks included.
355    pub fn heap_words(&self) -> u64 {
356        self.machine.heap_words()
357    }
358
359    /// Words handed out over the whole run, reuse counted each time.
360    pub fn allocated_words(&self) -> u64 {
361        self.machine.allocated_words()
362    }
363
364    /// How many collections this run's heap has done.
365    ///
366    /// [`Vm::live_words`] is `None` exactly when this is `0`: a heap that has
367    /// never collected has nothing that measured what is live.
368    pub fn collections(&self) -> u64 {
369        self.machine.collected().collections
370    }
371
372    /// Words the most recent collection found alive, or `None` if the heap
373    /// has never collected.
374    ///
375    /// [Issue #248](https://github.com/myuon/cove/issues/248) is why this
376    /// exists as its own accessor rather than only inside the trace's
377    /// `heap_summary` event: `Runtime::heap_stats` is filled in only by the
378    /// tree-walking backend (see its doc comment), so a `Vm` embedder asking
379    /// "does this run still hold what an early invocation allocated" has
380    /// nothing else public to read. `heap_words` and `allocated_words`
381    /// answer capacity and a monotonic total; this is the one that answers
382    /// what is live right now, as of the last sweep.
383    pub fn live_words(&self) -> Option<u64> {
384        let collected = self.machine.collected();
385        (collected.collections > 0).then_some(collected.live_words)
386    }
387
388    /// Where the most recent failed assertion was written, together with the
389    /// message it produced, or `None` when no assertion has failed.
390    ///
391    /// The same answer [`crate::interp::Interpreter::assertion_failure`]
392    /// gives, and it is here for the same caller: a test runner points at
393    /// the assertion the way every other error points at source. An
394    /// assertion that failed and was then handled inside the program is
395    /// still recorded, which is why the message is part of the answer — a
396    /// caller reports at this span only when the failure it is holding is
397    /// this one.
398    pub fn assertion_failure(&self) -> Option<(Span, &str)> {
399        self.machine.assertion_failure()
400    }
401
402    /// The process arguments as the one value an entry may take them as.
403    fn enter(
404        &mut self,
405        module: &str,
406        name: &str,
407        args: Vec<Rc<str>>,
408    ) -> Result<Value, RuntimeError> {
409        let id = self.lookup(module, name)?;
410        let function = self.program.function(id);
411        let arguments = match function.arity() {
412            0 => Vec::new(),
413            1 => vec![Value::array(args.into_iter().map(Value::string))],
414            other => {
415                return Err(RuntimeError::new(format!(
416                    "entry `{module}.{name}` declares {other} parameters"
417                ))
418                .at(function.span)
419                .with_rule(
420                    "An entry function takes either no parameters or one `Array<String>` of process arguments.",
421                )
422                .with_help(format!(
423                    "write `fn {name}()` or `fn {name}(args: Array<String>)`"
424                )));
425            }
426        };
427        self.enter_with(module, name, id, arguments)
428    }
429
430    /// The check, and then the call.
431    fn invoke_checked(
432        &mut self,
433        module: &str,
434        name: &str,
435        args: Vec<Value>,
436    ) -> Result<Value, RuntimeError> {
437        crate::invoke::check(self.runtime.program(), module, name, &args)?;
438        let id = self.lowered(module, name)?;
439        self.enter_with(module, name, id, args)
440    }
441
442    /// The call itself, from the arguments in to the answer out.
443    ///
444    /// The one seam. [`Vm::run_entry`] reaches it having turned the process
445    /// arguments into the array an entry declares, and [`Vm::invoke`]
446    /// reaches it having held a host's own values to what the checker
447    /// resolved; nothing below this line knows which of the two happened.
448    fn enter_with(
449        &mut self,
450        module: &str,
451        name: &str,
452        id: FunctionId,
453        args: Vec<Value>,
454    ) -> Result<Value, RuntimeError> {
455        let function = self.program.function(id);
456        let span = function.span;
457        let returns = function.returns;
458
459        self.runtime.trace(TraceEvent::EntryEnter {
460            module: module.to_string(),
461            function: name.to_string(),
462        });
463        // Started here and not in `run_entry`, because what this measures is
464        // the entry: the argument conversion is the entry's own boundary
465        // crossing and the run is what follows it.
466        let timing = Timing::start();
467        let waited = self.machine.host_wait();
468
469        let outcome = self
470            .words_of(function, &args)
471            .map_err(|e| e.at(span))
472            .and_then(|words| self.machine.run(id, &words, &self.budget))
473            .and_then(|answer| {
474                boundary::to_value(&self.machine, returns, &answer).map_err(|e| e.at(span))
475            });
476
477        // Both events on every path, the way the oracle writes them: an entry
478        // that failed still entered and still left, and a run that failed
479        // still allocated. A trace that recorded the exit only for a run that
480        // answered would be a trace whose shape depended on the answer.
481        self.runtime.trace(TraceEvent::EntryExit {
482            module: module.to_string(),
483            function: name.to_string(),
484            cpu: timing
485                .elapsed()
486                .saturating_sub(self.machine.host_wait().saturating_sub(waited)),
487            wait: self.machine.host_wait().saturating_sub(waited),
488        });
489        self.summarize_heap();
490        outcome
491    }
492
493    /// What this run's memory did, recorded once as the run ends.
494    ///
495    /// The word half of the event and none of the object half. Issue #240
496    /// decided that `heap_summary` does not choose between the two — an
497    /// inline struct is words here and no object at all on the oracle, so
498    /// neither family's figures can be derived from the other's — and the
499    /// rule that follows is that a machine leaves `None` in what it does not
500    /// count rather than a zero that reads as a measurement.
501    ///
502    /// `live_words` is one of those. It is what the last collection found
503    /// alive, so a run that never collected has nothing that measured it, and
504    /// the figure is absent rather than nought. `capacity_words` is not: the
505    /// heap region occupies what it occupies whether anything has been swept
506    /// or not.
507    ///
508    /// There is no pause here, and that is the same rule again. This
509    /// collector does not time itself yet, and a zero would say it stopped
510    /// the world for no time at all.
511    fn summarize_heap(&self) {
512        self.runtime.trace(TraceEvent::HeapSummary {
513            collections: self.collections(),
514            object_count: None,
515            allocated_bytes: None,
516            live_bytes: None,
517            peak_bytes: None,
518            pause: None,
519            allocated_words: Some(self.allocated_words()),
520            capacity_words: Some(self.heap_words()),
521            live_words: self.live_words(),
522        });
523    }
524
525    /// The arguments in word form.
526    ///
527    /// Each conversion allocates and an allocation can collect, so an
528    /// argument already converted is held as a temporary root until the frame
529    /// that will own it exists. The roots are released here rather than after
530    /// the run because nothing between this line and the write of the entry's
531    /// frame allocates: [`Machine::run`] reserves stack words and copies the
532    /// arguments into them, and the frame is a root from that moment on.
533    /// Holding them for the length of the run instead would retain the
534    /// entry's arguments past the point the lowering cleared their slots,
535    /// which is exactly the retention the static reference map was careful
536    /// not to be.
537    fn words_of(&mut self, function: &Function, args: &[Value]) -> Result<Vec<u64>, RuntimeError> {
538        let params = function.params.clone();
539        let mark = self.machine.temps();
540        let mut words = Vec::with_capacity(args.len());
541        let mut failed = None;
542        for (layout, value) in params.iter().zip(args) {
543            // Each argument's own words, in declaration order, because that
544            // is what the callee's frame is: parameters occupy it from slot 0
545            // at their own widths, and a `(Int, Point, Int)` list is four
546            // words rather than three slots.
547            //
548            // `from_value` releases its own temporary roots when it returns,
549            // so every reference among the words is re-taken here and held
550            // until the frame that will own it exists.
551            match boundary::from_value(&mut self.machine, *layout, value) {
552                Ok(written) => {
553                    let reprs = self.program.layout(*layout).words.clone();
554                    for (repr, word) in reprs.iter().zip(&written) {
555                        if repr.is_ref() && *word != 0 {
556                            self.machine.push_temp(*word);
557                        }
558                    }
559                    words.extend_from_slice(&written);
560                }
561                Err(error) => {
562                    failed = Some(error);
563                    break;
564                }
565            }
566        }
567        self.machine.release_temps(mark);
568        match failed {
569            Some(error) => Err(error),
570            None => Ok(words),
571        }
572    }
573
574    fn lookup(&self, module: &str, name: &str) -> Result<FunctionId, RuntimeError> {
575        self.program.function_named(module, name).ok_or_else(|| {
576            RuntimeError::new(format!("this package does not declare `{module}.{name}`"))
577        })
578    }
579
580    /// The same lookup, for a caller that has already established the package
581    /// declares the function.
582    ///
583    /// [`crate::invoke::check`] has passed by the time this runs, so the
584    /// package *does* declare it and the reader should not be told it does
585    /// not. What is missing is the lowering, and the remedy is the caller's.
586    fn lowered(&self, module: &str, name: &str) -> Result<FunctionId, RuntimeError> {
587        self.program.function_named(module, name).ok_or_else(|| {
588            RuntimeError::new(format!(
589                "this run's lowering does not include `{module}.{name}`"
590            ))
591            .with_rule(
592                "A run executes the functions one entry can reach, because that is what `lower_entry` lowers.",
593            )
594            .with_help(format!(
595                "lower it too, by naming `{module}.{name}` as a root, and build the run on that program"
596            ))
597        })
598    }
599
600    /// Writes the run's terminal event, whichever way in produced it.
601    ///
602    /// Every path into a program passes through here, which is what makes
603    /// "every run has one" true rather than a claim about the paths somebody
604    /// remembered. An entry that answers `Err` is the program saying what it
605    /// was written to say: a failure of the program's work and not of the
606    /// run, which is why it is its own outcome rather than one more kind of
607    /// stop.
608    fn ended(&self, outcome: Result<Value, RuntimeError>) -> Result<Value, RuntimeError> {
609        let (classification, message) = match &outcome {
610            Ok(value) if value.is_err() => (
611                RunOutcome::Error,
612                crate::interp::returned_error_message(value),
613            ),
614            Ok(_) => (RunOutcome::Success, None),
615            Err(error) => (error.outcome, Some(error.message.clone())),
616        };
617        self.runtime.trace(TraceEvent::RunEnded {
618            outcome: classification,
619            message,
620        });
621        outcome
622    }
623}
624
625/// The meter a run charges through, over `hosts`'s budget or over none.
626///
627/// A registry with no budget installed answers `None`, which has always meant
628/// no limit; a meter over default [`Limits`] is that, written down.
629fn meter_of(hosts: &HostRegistry) -> Meter {
630    hosts
631        .budget_meter()
632        .unwrap_or_else(|| Budget::new(Limits::default()).meter())
633}