cove_runtime/vm/exec.rs
1//! The machine, and everything one instruction can reach.
2//!
3//! One `Machine` runs one task's frames over one [`Memory`]. It is a
4//! register machine: every instruction names its operands and its
5//! destination by slot, and a slot is `memory[frame_base + slot]`.
6//!
7//! # The loop is not here
8//!
9//! [`encoded::dispatch`] is the loop, and it is the only one. Issue #245's
10//! Phase 5 cut production execution over to
11//! [ADR 0041](../../../../docs/adr/0041-a-slot-number-fits-in-sixteen-bits.md)'s
12//! fixed-width form and deleted the `Inst` loop that had been beside it, so
13//! the pipeline reads:
14//!
15//! ```text
16//! checked AST -> lowering -> cove_ir::Inst -> encoder -> bytecode -> verify once -> this machine
17//! ```
18//!
19//! [`cove_ir::Inst`] is still the *compiler's* vocabulary — what the lowering
20//! builds, what `cove_ir::print` renders, what a test asserts on, and what
21//! [`crate::vm::debug`] shows a debugger as lowered IR — and it is no longer
22//! anything the machine matches on. What is left in this file is the state
23//! an instruction acts on and every operation one can reach: the frames, the
24//! calls, the host boundary, the builtins, the scopes, the tasks and the
25//! collector's rendezvous. The loop reads sixteen bytes and calls into here.
26//!
27//! The cutover was not taken for speed and did not buy any; the commit that
28//! made it says what it cost and what it bought instead.
29//!
30//! # There is no Rust recursion here
31//!
32//! A Cove call pushes a frame onto [`Machine::frames`] and continues the same
33//! loop. Nothing about a call grows the native stack, so how deep a Cove
34//! program may recurse is decided by [`STACK_WORDS`] alone rather than by how
35//! large a Rust stack frame the dispatch loop happens to compile to — which
36//! is a number that changes when an unrelated instruction is added.
37//!
38//! # There is no `Value` here
39//!
40//! Ordinary Cove-to-Cove execution moves words and heap objects. The public
41//! `Value` is built at the boundary — a Host call, an entry's answer, a trace
42//! capture — and nowhere else. There is no operand `Vec<Value>`, no argument
43//! buffer, no spill area and no fallback path, which is what ADR 0034 asks
44//! for and what the predecessor could not say.
45
46use std::sync::{Arc, Mutex};
47use std::thread::{Scope, ScopedJoinHandle};
48use std::time::Duration;
49
50use cove_diag::Span;
51use cove_ir::{
52 ArgsId, ArithOp, BuiltinId, CmpOp, FunctionId, HostOpId, LayoutId, Program, Repr, Shape, Slot,
53 StrId,
54};
55
56use crate::budget::{Cancellation, Meter, Stopped};
57use crate::error::RuntimeError;
58use crate::host::{HostRegistry, Reentry, ResourceHandle};
59use crate::interp::stopped_here;
60use crate::runtime::{Runtime, ENTRY_TASK};
61use crate::task;
62use crate::trace::TraceEvent;
63use crate::vm::builtins::operand::Operand;
64use crate::vm::debug::{halted, Debugger, Resume, Stop};
65use crate::vm::mem::{Collected, Memory, NoSegment, Overflow, Parked, Rooted, Roots};
66use crate::vm::{boundary, builtins, cell};
67use crate::wallclock::Instant;
68// The one import of the public `Value` outside `boundary`, and the one thing
69// ADR 0034 allows it for: a host call's arguments and its answer exist as
70// `Value`s for the length of the call and nowhere else. Nothing here stores
71// one, and the three places it is named are all in transit — the vector
72// handed to the boundary, the callee a way back is offered, and a callback's
73// arguments and answer, which are the same boundary crossed the other way
74// round and are converted by the same file.
75use crate::value::Value;
76
77/// The dispatch loop, and the refusal that keeps it honest.
78///
79/// A **child** module rather than a sibling, and that is still load-bearing:
80/// it reads `Machine`'s private state without widening any of it to the
81/// crate. The loop over the same machine is exactly as privileged as the
82/// machine, and nothing else is.
83pub(crate) mod encoded;
84
85/// How many instructions run between two budget checks.
86///
87/// A budget check reads an atomic and a clock, and doing that per instruction
88/// would cost more than most instructions do. Doing it per *call* would let a
89/// tight arithmetic loop run unbounded. A fixed stride is the arrangement that
90/// bounds both: the run notices a cancellation within a known number of
91/// instructions, whatever it is doing.
92///
93/// It is public because it is the arithmetic of a contract rather than a
94/// tuning knob.
95/// [ADR 0040](../../../../docs/adr/0040-a-bound-outlives-its-backend.md)
96/// states every bound a stop mode promises in terms of it, and
97/// `crates/cove-runtime/tests/responsiveness.rs` measures each of them, so
98/// moving this number moves a stated maximum and costs both.
99pub const SAFEPOINT_STRIDE: u64 = 1024;
100
101/// How many [`crate::vm::builtins::operand::Operand`]s
102/// [`Machine::call_builtin`] holds inline before it spills to a `Vec`.
103///
104/// Sized for an ordinary fixed-arity builtin — a receiver and a couple of
105/// arguments — which is every call except the handful `cove-schema` declares
106/// `variadic: true` (`Vector.of`, `Map.of`, `Set.of`), and those spill
107/// instead of raising this for everyone else's sake.
108const INLINE_OPERANDS: usize = 8;
109
110/// Payload word 0 of a [`Shape::ByteBuffer`] owner: how many of its store's
111/// bytes are value.
112const BUFFER_LEN: u32 = 0;
113
114/// Payload word 1 of a [`Shape::ByteBuffer`] owner: the [`Shape::Bytes`] store
115/// holding them, whose own header length is the capacity.
116const BUFFER_STORE: u32 = 1;
117
118/// The smallest store [`Inst::AllocBuffer`] asks for, and the floor a growth
119/// doubles up from.
120///
121/// [ADR 0052](../../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)
122/// says "twice the capacity from a small floor" and leaves the floor to the
123/// storage unit. `seq.rs`'s `MIN_CAPACITY` is four *elements*; sixteen is the
124/// byte-sized answer to the same question, and the reason it is not four is
125/// that four bytes is less than one word. A byte store packs eight bytes to a
126/// word and costs a header word whatever it holds, so a capacity below eight
127/// buys nothing at all and a capacity of eight buys one growth's worth of
128/// nothing: sixteen is two payload words, which covers the punctuation-sized
129/// appends a formatter makes between the ones that are worth reallocating for.
130const MIN_BUFFER_BYTES: u64 = 16;
131
132/// A live byte buffer: its owner, its store, and how much of the store is
133/// value rather than spare room.
134///
135/// `seq.rs`'s `Growable` for bytes, and the same three-part reading of ADR
136/// 0052's one growable-run discipline — a stable owner, a replaceable store,
137/// and a live prefix `[0, len)` inside a capacity. `len` and `capacity` are
138/// both byte counts, as the owner's word 0 and the store's own header state
139/// them; there is no stride, because the storage unit is the byte.
140struct ByteBuffer {
141 owner: u64,
142 store: u64,
143 len: u32,
144 capacity: u32,
145}
146
147/// One live call.
148///
149/// The top of [`Machine::frames`] is the frame currently executing, not the
150/// caller of it. That costs a write of `pc` before anything that can collect
151/// or fail, and it buys a collector and an error reporter that need no
152/// special case for "and also the one in the local variables".
153struct Frame {
154 function: FunctionId,
155 /// The linear address of slot 0.
156 base: u64,
157 /// Where this frame resumes: the instruction after the call it is
158 /// suspended at, or the one about to run.
159 pc: u32,
160 /// The slot of the *caller's* frame this call's answer is written to.
161 dst: Slot,
162}
163
164/// What one task is holding, read where the collector asks rather than
165/// gathered in advance.
166///
167/// A safepoint runs every [`SAFEPOINT_STRIDE`] instructions and a collection
168/// is rare, so the walk has to cost nothing when the answer is that nothing
169/// is pending — and [`Memory::poll`] answers that with one relaxed load,
170/// *before* it asks for roots. Gathering into a `Vec` first would have paid
171/// for a pass over every reference slot of every live frame on the common
172/// path in order to save nothing on the rare one.
173///
174/// It borrows the machine immutably and reads its own memory, which is why
175/// every reader of it takes `&self`: a collection and a park are both things
176/// a task asks for about itself, and neither changes a frame.
177struct Live<'m, 'a>(&'m Machine<'a>);
178
179impl Roots for Live<'_, '_> {
180 fn each_root(&self, f: &mut dyn FnMut(u64)) {
181 let machine = self.0;
182 let program = machine.program;
183 for frame in &machine.frames {
184 let function = program.function(frame.function);
185 for slot in function.refs.iter() {
186 let word = machine.mem.slot(frame.base, slot);
187 if word != 0 {
188 f(word);
189 }
190 }
191 }
192 for &addr in &machine.temps {
193 if addr != 0 {
194 f(addr);
195 }
196 }
197 // A cell this task is inside. It is already named by a `Repr::Ref`
198 // slot of the frame the lock region belongs to — the lowering holds
199 // the receiver for the whole region — so this adds nothing to what is
200 // reachable. What it adds is that the claim does not have to be made:
201 // [`Machine::give_cells_back`] *writes* the lock word of every one of
202 // these, on a path taken after the loop has stopped, and a word
203 // written into a run the sweep reclaimed would be a word of whatever
204 // is allocated there next.
205 for &addr in &machine.held {
206 f(addr);
207 }
208 // The scheduler table is a *root provider*, which is the whole of
209 // what keeps it from being the second value store ADR 0034 forbids:
210 // it holds the address of the object a task's answer goes into, and
211 // the object and its words are in the run's one heap like anything
212 // else. Nothing that wanted to dodge a heap representation could be
213 // put here, because an address is all there is room for.
214 for child in &machine.children {
215 if child.answer != 0 {
216 f(child.answer);
217 }
218 if child.closure != 0 {
219 f(child.closure);
220 }
221 }
222 }
223}
224
225/// What a task thread hands back: nothing, or why it stopped.
226///
227/// Nothing, because the value is not carried out — the parent allocated the
228/// object it goes into before the thread existed, and the child wrote its
229/// words there. Handing the words back through the join would have left them
230/// in a Rust `Vec`, which nothing the collector walks names, for as long as
231/// the join took.
232type Outcome = Result<(), RuntimeError>;
233
234/// What a spawned task has done so far.
235///
236/// [`crate::task::TaskState`] is the oracle's, and the four are the same
237/// four: a task ends by finishing, by failing, by being cancelled, or by
238/// breaking an invariant in its own thread — and the last is reported as the
239/// third or the second, exactly as [`crate::task::Task::join`] reports it.
240enum ChildState {
241 /// The body is running on its own thread, which has not been joined.
242 Running,
243 /// The body produced a value, and it is in the answer object.
244 Settled,
245 /// The body raised. Awaiting again raises the same error.
246 Failed(RuntimeError),
247 /// The task's own flag was raised and it stopped at a safepoint rather
248 /// than finishing. Awaiting a cancelled task is an error.
249 Cancelled,
250}
251
252/// One task this machine spawned, and everything about it that is not the
253/// thread.
254///
255/// The thread is not here, and cannot be: a scoped join handle borrows the
256/// scope it was started in, and that is a lifetime one turn of the dispatch
257/// loop owns rather than a field the machine can hold. So the handles live
258/// beside this list, at the same indices, for the length of one
259/// [`Machine::drive`].
260struct Child {
261 /// Trace identity, unique across the run.
262 id: u64,
263 /// Where in its scope's spawn order it is, counting from one.
264 position: usize,
265 /// The name of the scope that owns it.
266 ///
267 /// Both are here for one reason: a diagnostic says *task 2 of scope
268 /// `requests`*, and [`crate::task::describe`] is where both backends read
269 /// that sentence from.
270 scope: Arc<str>,
271 /// The flag the body reads at its own safepoints.
272 cancellation: Cancellation,
273 /// The closure environment the body runs, until it has been joined.
274 ///
275 /// Held as a root, and it has to be. The lowering ends the closure
276 /// temporary's live range at the `spawn` — correctly: the value is a
277 /// temporary and the instruction consumed it — so the moment the
278 /// `Inst::Clear` after the `Inst::Spawn` runs, nothing in the parent's
279 /// frame names the environment. The child has not necessarily read it
280 /// yet: ADR 0008's amendment says a `spawn` orders nothing, so whether
281 /// the thread has run an instruction by then is the operating system's
282 /// answer, and one allocation in the parent could otherwise free the
283 /// object the child is about to enter.
284 ///
285 /// The oracle has no such window because the closure crosses as a
286 /// `Transfer` — a copy the receiving thread owns outright. Here what
287 /// crosses is an address into a heap both tasks share, so the *table*
288 /// holds it, and this is the second thing that makes the scheduler table
289 /// a root provider.
290 ///
291 /// It is dropped at the join rather than at the child's first
292 /// instruction, because that is the earliest moment the parent can know
293 /// the child is done with it — and it retains nothing the child was not
294 /// already retaining, since the captures were copied into the child's
295 /// own frame.
296 closure: u64,
297 /// The object this task's answer is written into.
298 ///
299 /// Allocated by **this** task, before the thread existed, and a root of
300 /// this task from that moment. A child that allocated its own would have
301 /// left it named by nothing the collector walks between its own last
302 /// safepoint and the parent's next one.
303 answer: u64,
304 /// What the words inside that object are.
305 layout: LayoutId,
306 state: ChildState,
307}
308
309impl Child {
310 /// How a diagnostic names this task.
311 fn describe(&self) -> String {
312 task::describe(self.position, &self.scope)
313 }
314}
315
316/// One task scope this machine has entered.
317///
318/// The scope owns every task spawned into it, which is what lets leaving it
319/// wait for or cancel them. It is never removed: a `Repr::Scope` word is an
320/// index, and an index that could be reused would name two scopes over one
321/// run.
322struct ScopeEntry {
323 name: Arc<str>,
324 /// Indices into [`Machine::children`], in spawn order.
325 tasks: Vec<usize>,
326 /// Set once the scope has been left. A handle that outlived its scope can
327 /// no longer spawn into it.
328 closed: bool,
329}
330
331/// One task's execution over one linear memory.
332pub(crate) struct Machine<'a> {
333 program: &'a Program,
334 /// What every thread of this run shares: the trace, and the counter task
335 /// ids are drawn from.
336 ///
337 /// `None` is a machine with no run around it — what this module's own
338 /// tests drive — and it costs exactly two things, both of which are
339 /// reporting rather than execution: nothing is traced, and task ids are
340 /// drawn from a counter of this machine's own. A program still spawns,
341 /// awaits and cancels.
342 runtime: Option<&'a Runtime>,
343 /// The boundary a [`cove_ir::Inst::CallHost`] calls through, if this run
344 /// has one.
345 ///
346 /// `None` is a machine with no host behind it — what a test that runs
347 /// arithmetic drives, and the same state [`crate::host::NoReentry`]
348 /// exists for on the other side of the boundary. A program that reaches
349 /// a host call from one is told what is missing rather than being given a
350 /// registry that answers nothing.
351 hosts: Option<&'a HostRegistry>,
352 mem: Memory,
353 frames: Vec<Frame>,
354 /// The heap address of each [`StrId`], or the refusal
355 /// [`Machine::place_literals`] met trying to build one — held exactly as
356 /// [`Machine::encoded`] holds its own, because both are prepared once,
357 /// before anything runs, by a constructor that itself cannot fail.
358 ///
359 /// Placed by [`Machine::for_run`], once per run, before the first
360 /// instruction executes and therefore before any other heap object can
361 /// exist. [`Machine::for_task`] never places one: it is handed this
362 /// `Arc` and clones it, so every task of a run addresses the same
363 /// objects with no lock between them and no second placement to pay
364 /// for. The objects themselves are never collected — they sit below
365 /// [`Memory::seal_static`]'s floor, which a sweep never walks past — so
366 /// unlike the table this replaces, nothing here roots them: there is
367 /// nothing for a collector to be told. See
368 /// [ADR 0045](../../../../docs/adr/0045-a-literal-is-there-before-the-program-runs.md).
369 literal_addrs: Result<Arc<[u64]>, RuntimeError>,
370 /// The host resources this run has been handed, in the table a
371 /// [`Repr::Host`] word indexes.
372 ///
373 /// [`Repr::Host`]'s own documentation is what fixes the shape — *an index
374 /// into the run's host resource table* — and this is that table. It is
375 /// here and not in the heap because
376 /// [ADR 0031](../../../../docs/adr/0031-a-host-handle-is-not-a-vm-handle.md)
377 /// draws exactly this line: a host resource handle is a name the *host*
378 /// minted for something the host owns, and a heap object is a reference
379 /// into storage this run allocated. Only the second is the VM's. Making a
380 /// resource an object in the traced heap would put a collection in charge
381 /// of a lifetime [ADR 0013](../../../../docs/adr/0013-host-resource-handles.md)
382 /// gives to the host, and would mean sweeping something whose `close` the
383 /// program had not written.
384 ///
385 /// It is not a second value store either, which is the other thing
386 /// ADR 0034 and ADR 0031 forbid. Nothing a Cove program can write down
387 /// may be put in it: an entry is an [`Arc`] of a [`ResourceHandle`] — a
388 /// module, a type name, a number and a flag — and the only two operations
389 /// over it are [`Machine::resource`] and [`Machine::resource_word`],
390 /// neither of which can be handed a Cove value. A value that wanted to
391 /// avoid having a heap representation could not hide here.
392 ///
393 /// **The word is one past the index, so zero is no resource.** Frames are
394 /// zeroed on entry, so a `Host` slot that has not been written yet reads
395 /// zero exactly as a `Ref` slot reads null; a table indexed straight by
396 /// the word would answer an unwritten slot with whichever resource
397 /// happened to be first.
398 ///
399 /// Nothing is ever removed. ADR 0013 says a closed resource's handle
400 /// survives as a name for something that is gone, and that a host never
401 /// reuses an identity — so an entry that outlived its resource is still
402 /// the right answer to give, because the refusal a later call earns is
403 /// the *host's* and can only be reached by handing the host the name.
404 /// What that costs is one name per distinct resource this run was handed,
405 /// which is the size of the table the host is keeping anyway.
406 ///
407 /// It is a field of the machine, which today is the run: a run has one
408 /// machine as it has one [`Memory`]. When a run has task threads this
409 /// moves where the object heap moves, and for the reason ADR 0013 gives
410 /// rather than by analogy — a resource is owned by the *run*, not by the
411 /// task or the scope that opened it, so a handle one task was given is a
412 /// name every task of the run may hold.
413 ///
414 /// Shared by every task of the run, behind a lock, and that is not an
415 /// economy: a task-safe resource **crosses** a task boundary, and what
416 /// crosses is the word. A table of this task's own would make that word
417 /// an index into a list the receiving task does not have — so a handle a
418 /// parent opened would name whatever the child happened to open first,
419 /// or nothing. ADR 0013 says a resource is the *run's*, and this is what
420 /// that sentence costs.
421 resources: Arc<Mutex<Vec<Arc<ResourceHandle>>>>,
422 /// Objects a boundary conversion is holding and no frame names yet.
423 ///
424 /// A frame is a root because a static map says which of its slots are
425 /// references. A half-built object is not: it is reachable only from a
426 /// Rust local, which nothing walks, and the next allocation the
427 /// conversion makes could collect it out from under itself. So the
428 /// conversion says so, explicitly, for exactly as long as that is true —
429 /// [`Machine::push_temp`] to take a root, [`Machine::release_temps`] to
430 /// give every root back that was taken since a mark.
431 ///
432 /// It is a stack rather than a set because the discipline is lexical: a
433 /// conversion that recurses takes a mark on the way in and releases to it
434 /// on the way out, so nothing has to remember which root was whose.
435 temps: Vec<u64>,
436 /// The task scopes this machine has entered, in the order it entered
437 /// them. A `Repr::Scope` word is one past an index into this.
438 ///
439 /// This machine's, not the run's, and that is what the task-safety rule
440 /// buys: neither a `TaskScope` nor a `Task` may cross a task boundary, so
441 /// a word formed here is only ever read here. Two tasks cannot form one
442 /// another's handles, which is the same disjointness by construction that
443 /// keeps two stack segments apart.
444 scopes: Vec<ScopeEntry>,
445 /// The tasks this machine spawned, in spawn order across every scope. A
446 /// `Repr::Task` word is one past an index into this.
447 children: Vec<Child>,
448 /// This task's own cancellation flag, or `None` for the entry, which has
449 /// none.
450 ///
451 /// Separate from the run's, which lives in the [`Meter`] every task
452 /// shares: cancelling one task stops that task, and cancelling the run
453 /// stops everything. [`crate::interp::stopped_here`] is where the two a
454 /// *thread* owns are read, and it is the oracle's own function so that
455 /// neither backend can drift from the other's answer.
456 cancellation: Option<Cancellation>,
457 /// The flags of the bounded host calls this thread is inside, innermost
458 /// last.
459 ///
460 /// [`crate::host::Reentry::call_until`] pushes one: `clock.timeout` bounds
461 /// its body, and *"`stop` bounds this call and everything inside it,
462 /// including a further host call the body makes and any callback that host
463 /// runs in turn"*. So a safepoint reads these beside this task's own flag,
464 /// which is what makes a timeout a timeout rather than a measurement taken
465 /// afterwards. [`crate::interp::stopped_here`] is the oracle's own
466 /// function and asks both, so neither backend can drift from the other's
467 /// answer about which of the two stopped the work.
468 stops: Vec<Cancellation>,
469 /// How many host calls running a Cove callback are stacked on this
470 /// thread.
471 ///
472 /// A Cove call adds no native frame here — that is what the dispatch loop
473 /// is for — but a *reentry* does: the host is a Rust frame, and running
474 /// its callback puts another turn of [`encoded::dispatch`] below it. So
475 /// this is bounded exactly as the oracle bounds it, by
476 /// [`crate::interp::MAX_REENTRY_DEPTH`], and for the same reason.
477 reentry_depth: usize,
478 /// Which task this machine is running, for a trace and for the way back
479 /// a host is offered.
480 task: u64,
481 /// Where the next task id comes from when there is no [`Runtime`] to ask.
482 ///
483 /// Only a test reaches it. A run draws ids from one counter for the whole
484 /// run, because a task id is a trace identity and two tasks spawned at
485 /// the same time on two threads must not share one.
486 next_task: u64,
487 /// Instructions dispatched, exactly.
488 ///
489 /// This is an *observable*: `cove-bench` reports it, so does
490 /// `cove run --profile`, so does the debugger, and
491 /// [`crate::vm::profile`] asserts that its per-opcode totals sum to it.
492 /// It counts opcodes and nothing else, so a `copy-bytes` that moved a
493 /// megabyte is one, the same as an `add.int`.
494 ///
495 /// What a run is *charged* is [`Machine::work`], which is a different
496 /// question and now a different number.
497 instructions: u64,
498 /// Work charged beyond one per instruction.
499 ///
500 /// Only the bulk byte operations of
501 /// [ADR 0052](../../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)
502 /// add to it, one per payload word they move, which is that ADR's
503 /// "charged proportionally to the bytes or words examined".
504 /// [`Machine::work`] is `instructions + bulk_work` and is the coordinate
505 /// fuel, the safepoint schedule and every stop bound are stated in.
506 ///
507 /// It is an *offset* rather than a parallel counter for one measured
508 /// reason: the dispatch loop must keep exactly the one increment and the
509 /// one comparison it already had. Maintaining a second counter beside
510 /// `instructions` in the loop cost 3% on `examples/covefmt` — 4.05s
511 /// against 3.93s, interleaved, four runs an arm twice — which is the same
512 /// order as the 2.4% `docs/VM_ARCHITECTURE.md` measured for a second
513 /// per-instruction branch. Keeping it here means the loop is untouched
514 /// and `next_check` absorbs the offset instead, since it is recomputed
515 /// only when something charges in bulk.
516 bulk_work: u64,
517 /// The instruction count at which the loop next asks a question.
518 ///
519 /// The whole of what a debugger costs the dispatch loop, and it is
520 /// nothing: the loop's one comparison already existed, and this is the
521 /// same comparison against a number that answers *both* questions. With
522 /// no debugger installed it is [`SAFEPOINT_STRIDE`] past the last count
523 /// the run was charged at and the loop behaves exactly as it did while
524 /// every instruction cost one; with one installed it is
525 /// `instructions + 1`, so the machine asks before every instruction and
526 /// the safepoint still fires on its own schedule inside.
527 ///
528 /// `docs/VM_ARCHITECTURE.md`'s "The mechanism that turns a thing off can
529 /// cost more than the thing" is why it is not an `Option` tested per
530 /// instruction: a `bool` guarding the counter measured 2.4% on `arith`,
531 /// and *"the branch costs what the increment costs"*. There is no second
532 /// branch here to pay for, and the substitution was measured to cost
533 /// nothing: the same tree with this comparison put back to the modulo
534 /// measured 82.9 ms on `arith` against 82.7 ms with it. What did cost
535 /// something was writing the question itself into the loop, which is why
536 /// [`Machine::ask`] is a call. [`crate::vm::debug`] has the table.
537 next_check: u64,
538 /// How many of [`Machine::instructions`] have been handed to the run's
539 /// [`Meter`].
540 ///
541 /// The two counters differ by the work this machine has done and not yet
542 /// paid for, and every place that pays hands over exactly that difference
543 /// and sets this to the count it paid up to. There is no second
544 /// accumulator to keep in step with the instruction count, which is what
545 /// makes "the run is charged for every instruction it dispatched" a
546 /// subtraction rather than a claim about the paths somebody remembered.
547 ///
548 /// Three places move it, and between them they cover every way work can
549 /// be done. The periodic safepoint in [`encoded::dispatch`] is the
550 /// ordinary one. [`Machine::charge_at_host_boundary`] is
551 /// [ADR 0030](../../../../docs/adr/0030-a-host-call-asks-the-fuel-limit.md)'s:
552 /// the fuel a run has been charged has to be current before a Host call
553 /// asks whether it may begin. [`Machine::spend_pending_fuel`] is the last
554 /// one, at the end of a run or of a spawned task's thread, because a run
555 /// that raised, ran out of budget, was cancelled or was abandoned by the
556 /// host that bounded it leaves through Rust's `?` rather than through an
557 /// instruction and reaches no further safepoint —
558 /// [ADR 0024](../../../../docs/adr/0024-a-stop-is-a-bound-not-a-point.md)
559 /// says pending fuel is never lost, and this is the counter that makes
560 /// that checkable.
561 charged_work: u64,
562 /// How long this machine has spent inside host calls.
563 ///
564 /// The oracle charges the same measurement against every open timing
565 /// context so that a run can separate its own work from what it spent
566 /// waiting; this machine has one context, which is the run.
567 host_wait: Duration,
568 collected: Collected,
569 /// The `Shared` cells this task is inside, innermost last.
570 ///
571 /// `lock` is two instructions with a call between them, and the release is
572 /// an obligation on every exit path — which the lowering discharges on the
573 /// one it can write. This is the other one: a runtime error is not a jump
574 /// the lowering emits, so a cell a failing task never gave back would be a
575 /// cell no task could ever take again. It is the same division
576 /// [`Machine::stop_all`] is under for a task scope, and it costs a push
577 /// and a pop per `lock`.
578 ///
579 /// A `Vec` rather than a set because the discipline is lexical: two cells
580 /// nest, the refusal is per cell rather than per task, and the lowering
581 /// leaves the regions in the order it entered them.
582 held: Vec<u64>,
583 /// The layout of the value the host call now running was handed back by a
584 /// callback, if it ran one.
585 ///
586 /// This exists for one question the family search in
587 /// [`crate::vm::boundary`] cannot answer. A host answer that crosses in
588 /// at a `Shape::Boxed` position has to be tagged with the family it
589 /// holds, and the tag is what [`cove_ir::Inst::Unbox`] compares against
590 /// the layout
591 /// the checker settled at the use. The search reads the value's own
592 /// description, and a description does not always name one family:
593 /// `Err(Error("no"))` fits `Result<Int, Error>` and
594 /// `Result<http.Response, Error>` equally well, and the two are different
595 /// runs of words. Which one the search returned was then decided by which
596 /// the lowering happened to intern first.
597 ///
598 /// A callback's answer needs no search, because it is a value that just
599 /// left this machine. `clock.timeout` declares `Result<Any, Error>` and
600 /// wraps whatever its body answered, so what goes in the box is the
601 /// callback's return value and the family it belongs to is the callback's
602 /// declared return layout — a static fact, recorded here on the way out
603 /// so that the way back in does not have to guess at it.
604 ///
605 /// Cleared when a host call begins ([`Back::parked`]) and written when a
606 /// callback returns ([`Machine::call_from_host`]), so it names the
607 /// innermost call in progress and never an older one: a host call the
608 /// callback itself makes clears it on the way in and the callback's own
609 /// return writes it afterwards.
610 callback_answer: Option<LayoutId>,
611 /// Where the most recent assertion failed, and the message it produced.
612 ///
613 /// Written only by [`cove_ir::Inst::AssertFailed`], which the failing arm
614 /// of a
615 /// lowered assertion carries. A failed assertion is an ordinary `Err`
616 /// from here on — the machine does not stop, and a program that handles
617 /// it goes on running — so this is a record of what was seen and not a
618 /// state the run is in. A test runner reads it to point at the assertion
619 /// rather than at the test, and keeps the message so that it can tell
620 /// the `Err` it is holding from a later, unrelated one.
621 assertion_failure: Option<(Span, String)>,
622 /// The debugger this run asks before every instruction, if one is
623 /// installed.
624 ///
625 /// It is the *machine* that calls, never the other way round: nothing
626 /// here hands out a suspended machine, and [`Stop`] is a shared borrow
627 /// that lasts one call. That is not a style choice. [`encoded::dispatch`]
628 /// holds a `&'s Scope<'s, 'a>` — the thread scope a `spawn` starts its
629 /// children in — and that borrow cannot outlive [`Machine::drive`], so a
630 /// handle to a paused machine could not be a value a debugger keeps.
631 /// Inverting the call is what makes the borrow expressible.
632 ///
633 /// `Send + Sync`, because a spawned task's machine is handed the same
634 /// reference and asks it from its own thread.
635 debugger: Option<&'a (dyn Debugger + Send + Sync)>,
636 /// **The instructions this machine executes**, or why this program has
637 /// none.
638 ///
639 /// Issue #245's Phase 5. There is one execution form and this is it: the
640 /// program is encoded and verified once, when the machine is built, and
641 /// from then on the loop reads operands out of sixteen bytes without
642 /// asking whether they are in range. [`cove_ir::Inst`] is what the
643 /// *lowering* produces and what a listing and the debugger show; nothing
644 /// below
645 /// [`encoded::dispatch`] matches on one.
646 ///
647 /// A `Result` rather than a refusal raised where it is discovered,
648 /// because a machine is built by an infallible constructor and a program
649 /// with no encoding must fail the same way however the machine was built.
650 /// [`Machine::run`] and [`Machine::enter_closure`] raise it **before a
651 /// frame is pushed**, so such a program has no observable effect at all
652 /// rather than stopping partway through one. A machine that only ever
653 /// answers builtin questions — several of this crate's tests build one —
654 /// never asks, and pays nothing for a program it will not run.
655 ///
656 /// An `Arc` because a run's tasks share one: [`Machine::for_task`] is
657 /// handed the parent's rather than encoding again, which is what makes
658 /// one spawn cost a pointer instead of a second pass over the program.
659 encoded: Result<Arc<cove_ir::bytecode::Encoded>, RuntimeError>,
660 /// [`Machine::call_builtin`]'s scratch buffer of argument words, taken
661 /// out for the duration of one call and put back afterwards.
662 ///
663 /// A builtin call reads every argument's words into one buffer before it
664 /// can hand out an [`crate::vm::builtins::operand::Operand`] pointing
665 /// into it, and a fresh `Vec` for that on every single call is exactly
666 /// the allocation issue #268's stage 1 is about. Keeping one here and
667 /// moving it in and out with [`std::mem::take`], instead of borrowing it
668 /// in place, is what lets the call stay `&mut self` without a second
669 /// borrow of `self` alive at the same time for the body that fills it —
670 /// and it is also what makes the following safe to be wrong about.
671 ///
672 /// **The reentrancy question this answers.** Nothing reachable from
673 /// [`crate::vm::builtins::call`] — not the ~100-arm dispatch in
674 /// `builtins.rs`, nor `seq.rs`, `keyed.rs`, `key.rs`, `text.rs`,
675 /// `scalar.rs`, `make.rs`, or `equal.rs` — calls [`Machine::call_host`],
676 /// [`Machine::call_resource`], [`Machine::call_from_host`], or anything
677 /// else that runs the dispatch loop again: a builtin is a leaf call. The
678 /// AST interpreter's own builtin table
679 /// ([`crate::builtins::Callable`]) says the same thing of itself and
680 /// cites `docs/LINEAR_VM.md` for why, and the VM's higher-order
681 /// operations (`Result.mapError`, `map`, `sorted`) are lowered to
682 /// ordinary Cove-level loops that call back through `Inst::Call`, not
683 /// through `CallBuiltin`, so they never reach here at all. The one
684 /// genuinely reentrant path in this file, [`Machine::call_resource`] /
685 /// [`Machine::call_host`] parking the machine so a host call can run a
686 /// callback back through [`Machine::call_from_host`], is a different
687 /// method with its own local `values: Vec<Value>` and is never invoked
688 /// from inside [`Machine::call_builtin`].
689 ///
690 /// So today, taking this field's `Vec` out and putting it back is a
691 /// no-op around a leaf call. But it costs nothing to be wrong about
692 /// safely: because the buffer is moved out with `mem::take` rather than
693 /// borrowed, a `call_builtin` that somehow nested inside another one
694 /// finds `self.builtin_words` already emptied by the outer call and
695 /// allocates its own `Vec` rather than aliasing or clobbering the outer
696 /// call's words. And the restore keeps the *whole run* allocation-free
697 /// in that case too, not just the outer call: whichever of the outer
698 /// call's buffer and the inner call's buffer has the larger capacity is
699 /// the one left in this field, so the next call — nested or not — still
700 /// finds a buffer large enough not to grow.
701 builtin_words: Vec<u64>,
702 /// The same, for the words a builtin *answers* with.
703 ///
704 /// Every builtin used to build a fresh `Vec` for its answer, including
705 /// the ones that answer a single word: `Array.length` allocated a
706 /// one-element `Vec` and dropped it a few instructions later, once per
707 /// call. On `examples/covefmt` that was **39 ns of an 86 ns call** —
708 /// measured by adding a second such allocation to the path and watching
709 /// the call get 39 ns dearer — which is to say that nearly half of what
710 /// a builtin cost was the container its answer travelled home in.
711 ///
712 /// So a builtin writes into a buffer instead, and this is the one it
713 /// writes into. It is taken out and put back the way `builtin_words` is,
714 /// for the reasons that field's note gives at length; the two are
715 /// separate buffers because a builtin reads its operands out of the
716 /// first while it is filling the second.
717 builtin_answer: Vec<u64>,
718 /// The last case index each enum wrapper resolved to, and for which
719 /// layout.
720 ///
721 /// `make::some` and its three siblings find their case by *name* —
722 /// `Shape::Enum`'s cases are a run and `Layout::case` scans it comparing
723 /// strings — and they do it once per call. That was 11 ns of a 110 ns
724 /// `String.codePointAtByte`, which is a builtin the lexer in
725 /// `examples/covefmt` calls once per byte of the source it reads.
726 ///
727 /// A memo and not a table, because there is nothing to invalidate: a
728 /// [`Program`]'s layouts are fixed before its first instruction runs, so
729 /// a `(layout, name)` pair has one answer for the whole run. And one
730 /// entry per wrapper rather than a map, because the shape of the miss is
731 /// known — a loop calls one builtin with one result layout over and over,
732 /// so the entry it wants is the one it left there.
733 cases: [Option<(LayoutId, u32)>; 4],
734 /// How many words a value of each layout occupies, by [`LayoutId`].
735 ///
736 /// [`Machine::width`] was `program.layout(id).width()` — an index into
737 /// `Program::layouts`, then the length of that `Layout`'s `words` — and
738 /// the dispatch loop asks it fifteen times over, once per instruction
739 /// that names a value location. `Machine::call_builtin` asks it *twice
740 /// per argument*: once to copy the words out of the frame and once to
741 /// slice the buffer back into operands.
742 ///
743 /// Two chases became one index, which measured about 2.5 ns each — 10 ns
744 /// of a 98 ns `String.codePointAtByte`, a builtin the lexer in
745 /// `examples/covefmt` calls once per byte it reads.
746 ///
747 /// Built once, before the first instruction, because a [`Program`]'s
748 /// layouts are fixed by then: there is no invalidation to get wrong and
749 /// no entry that can be missing.
750 widths: Arc<[u32]>,
751}
752
753/// Which of [`Machine::cases`] a wrapper memoises into.
754///
755/// Four constants rather than a hash of the name: the callers are the four
756/// functions in [`crate::vm::builtins::make`] and nothing else, so the set is
757/// closed and naming it costs nothing at run time.
758#[derive(Clone, Copy)]
759pub(crate) enum Wrapper {
760 Some = 0,
761 None = 1,
762 Ok = 2,
763 Err = 3,
764}
765
766impl<'a> Machine<'a> {
767 /// A machine with no host boundary, for a program that calls none.
768 ///
769 /// Production always goes through [`Machine::for_run`], which a `Vm`
770 /// calls with the runtime it is part of; this and [`Machine::with_hosts`]
771 /// are the two narrower forms this crate's own tests build a bare
772 /// `Machine` from when a fixture has no runtime to hand one.
773 #[cfg_attr(not(test), allow(dead_code))]
774 pub(crate) fn new(program: &'a Program, heap_words: usize) -> Machine<'a> {
775 Machine::with_hosts(program, heap_words, None)
776 }
777
778 /// A machine that calls hosts through `hosts`, with nothing above it.
779 ///
780 /// See [`Machine::new`]: reached only from this crate's own tests, which
781 /// use it for a fixture that has hosts to call but no runtime.
782 #[cfg_attr(not(test), allow(dead_code))]
783 pub(crate) fn with_hosts(
784 program: &'a Program,
785 heap_words: usize,
786 hosts: Option<&'a HostRegistry>,
787 ) -> Machine<'a> {
788 Machine::for_run(program, heap_words, hosts, None)
789 }
790
791 /// The entry task of one run.
792 ///
793 /// Places every program literal into the heap before answering — see
794 /// [`Machine::place_literals`] and
795 /// [ADR 0045](../../../../docs/adr/0045-a-literal-is-there-before-the-program-runs.md).
796 /// A failure there is held on [`Machine::literal_addrs`] exactly as a
797 /// failure to encode the program is held on [`Machine::encoded`]: this
798 /// constructor stays infallible, and [`Machine::run`] and
799 /// [`Machine::enter_closure`] are what turn either into a refusal,
800 /// before a frame exists.
801 pub(crate) fn for_run(
802 program: &'a Program,
803 heap_words: usize,
804 hosts: Option<&'a HostRegistry>,
805 runtime: Option<&'a Runtime>,
806 ) -> Machine<'a> {
807 let mut machine = Machine {
808 program,
809 runtime,
810 hosts,
811 mem: Memory::new(heap_words),
812 frames: Vec::new(),
813 // Overwritten below, once the machine that places them exists.
814 literal_addrs: Ok(Arc::from([])),
815 resources: Arc::new(Mutex::new(Vec::new())),
816 temps: Vec::new(),
817 scopes: Vec::new(),
818 children: Vec::new(),
819 cancellation: None,
820 stops: Vec::new(),
821 reentry_depth: 0,
822 task: ENTRY_TASK,
823 next_task: 1,
824 instructions: 0,
825 charged_work: 0,
826 bulk_work: 0,
827 host_wait: Duration::ZERO,
828 collected: Collected::default(),
829 held: Vec::new(),
830 callback_answer: None,
831 assertion_failure: None,
832 debugger: None,
833 next_check: SAFEPOINT_STRIDE,
834 // Encoded and verified here, once, for every machine — because
835 // this is where a run's program arrives and because a refusal
836 // that happened later would happen after a frame was pushed. See
837 // the field.
838 encoded: encoded::prepare(program),
839 builtin_words: Vec::new(),
840 builtin_answer: Vec::new(),
841 cases: [None; 4],
842 widths: program
843 .layouts
844 .iter()
845 .map(|layout| layout.width())
846 .collect(),
847 };
848 machine.literal_addrs = machine.place_literals();
849 machine
850 }
851
852 /// A machine for a spawned task, over a stack segment of its own and the
853 /// run's one heap.
854 ///
855 /// Everything a run owns is shared and everything a task owns is fresh,
856 /// and the split is the whole of ADR 0008 here. Shared: the program, the
857 /// hosts, the trace, the heap, the run's budget, the resource table, and
858 /// — since [ADR 0045](../../../../docs/adr/0045-a-literal-is-there-before-the-program-runs.md)
859 /// — every literal's address. Fresh: the stack segment, the frames, and
860 /// the scheduler table it spawns its own children into.
861 ///
862 /// `literal_addrs` is handed over rather than rebuilt, and that is the
863 /// point: the entry's machine placed every literal, in the run's one
864 /// heap, before this task could have been spawned, so there is nothing
865 /// left for this constructor to allocate, copy or place. A literal in a
866 /// loop this task runs costs one load, exactly as it does in the task
867 /// that spawned it — no lock, because there is nothing left to
868 /// synchronise.
869 #[allow(clippy::too_many_arguments)]
870 fn for_task(
871 program: &'a Program,
872 hosts: Option<&'a HostRegistry>,
873 runtime: Option<&'a Runtime>,
874 resources: Arc<Mutex<Vec<Arc<ResourceHandle>>>>,
875 mem: Memory,
876 cancellation: Cancellation,
877 task: u64,
878 encoded: Arc<cove_ir::bytecode::Encoded>,
879 literal_addrs: Arc<[u64]>,
880 widths: Arc<[u32]>,
881 ) -> Machine<'a> {
882 Machine {
883 program,
884 runtime,
885 hosts,
886 mem,
887 frames: Vec::new(),
888 literal_addrs: Ok(literal_addrs),
889 resources,
890 temps: Vec::new(),
891 scopes: Vec::new(),
892 children: Vec::new(),
893 cancellation: Some(cancellation),
894 stops: Vec::new(),
895 reentry_depth: 0,
896 task,
897 next_task: 1,
898 instructions: 0,
899 charged_work: 0,
900 bulk_work: 0,
901 host_wait: Duration::ZERO,
902 collected: Collected::default(),
903 held: Vec::new(),
904 callback_answer: None,
905 assertion_failure: None,
906 debugger: None,
907 next_check: SAFEPOINT_STRIDE,
908 // The parent's, not a second encoding of the same program: a run
909 // executes one form, and encoding again per spawn would be a
910 // second pass over the whole program for a pointer's worth of
911 // sharing.
912 encoded: Ok(encoded),
913 builtin_words: Vec::new(),
914 builtin_answer: Vec::new(),
915 cases: [None; 4],
916 // The parent's, for the reason `encoded` is: a table derived from
917 // a program the whole run shares is the same table in every task.
918 widths,
919 }
920 }
921
922 /// The family of the value a callback answered during the host call in
923 /// progress, if one ran. See [`Machine::callback_answer`].
924 pub(crate) fn callback_answer(&self) -> Option<LayoutId> {
925 self.callback_answer
926 }
927
928 /// How many instructions this machine has run.
929 pub(crate) fn instructions(&self) -> u64 {
930 self.instructions
931 }
932
933 /// Which task this machine is running.
934 ///
935 /// The same number a trace event carries, because there is only one
936 /// counter: [`crate::trace::Event`] writes this field and so does
937 /// [`crate::vm::debug::Stop::task`], so a debugger and a trace name a
938 /// task the same way instead of each inventing an identity for it.
939 pub(crate) fn task(&self) -> u64 {
940 self.task
941 }
942
943 /// Installs `debugger`, to be asked before every instruction this machine
944 /// runs from here on.
945 ///
946 /// Installed rather than passed, because the question is asked in the
947 /// dispatch loop and the loop takes its arguments once per call. A task
948 /// this machine spawns is handed the same reference, which is why the
949 /// trait is `Send + Sync`.
950 pub(crate) fn watch(&mut self, debugger: Option<&'a (dyn Debugger + Send + Sync)>) {
951 self.debugger = debugger;
952 self.next_check = self.next_question();
953 }
954
955 /// The instructions this machine runs, or the refusal every run of this
956 /// program answers with.
957 ///
958 /// Cloning an `Arc`, because the loop borrows the code while the machine
959 /// is borrowed mutably, and a run's tasks share the one encoding anyway.
960 fn code(&self) -> Result<Arc<cove_ir::bytecode::Encoded>, RuntimeError> {
961 self.encoded.clone()
962 }
963
964 /// The address of every program literal, or the refusal placing them
965 /// met. See [`Machine::literal_addrs`].
966 ///
967 /// Cloning an `Arc`, for the same reason [`Machine::code`] does: a run's
968 /// tasks share the one placement.
969 fn literals(&self) -> Result<Arc<[u64]>, RuntimeError> {
970 self.literal_addrs.clone()
971 }
972
973 /// The instruction count at which the loop next asks its one question.
974 ///
975 /// The two questions folded into one comparison. Without a debugger it is
976 /// [`SAFEPOINT_STRIDE`] past the last count the run was charged at; with
977 /// a debugger it is the very next instruction, and the safepoint's own
978 /// schedule is unchanged underneath it.
979 ///
980 /// This used to be the next *multiple* of [`SAFEPOINT_STRIDE`], and while
981 /// every instruction cost one it was the same number: `charged` is set to
982 /// `instructions` at every safepoint, so a stride past it is the stride's
983 /// next multiple. The two part company only when something charges more
984 /// than one, which is what
985 /// [ADR 0052](../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)
986 /// asks for and what the multiple could not survive — see the safepoint
987 /// condition in [`crate::vm::exec::encoded`].
988 /// What this run has been charged for: one per instruction, plus the
989 /// words the bulk operations moved.
990 #[inline]
991 fn work(&self) -> u64 {
992 self.instructions + self.bulk_work
993 }
994
995 /// Cancellation, fuel and the collector's rendezvous, at `pc`.
996 ///
997 /// Lifted out of [`crate::vm::exec::encoded`]'s loop so that a bulk
998 /// operation can reach one *while it is running*. That is
999 /// [ADR 0052](../../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)'s
1000 /// requirement and
1001 /// [ADR 0040](../../../../docs/adr/0040-a-bound-outlives-its-backend.md)'s
1002 /// arithmetic: a copy that charged for a megabyte only when it had
1003 /// finished copying it would overshoot a fuel or cancellation bound by a
1004 /// megabyte, however promptly the loop polled afterwards.
1005 ///
1006 /// The order is the loop's order and may not be rearranged — cancellation
1007 /// before fuel, because a run that was asked to stop is not out of fuel;
1008 /// then the collector, which must see a frame this caller has already
1009 /// `sync`ed.
1010 fn safepoint(&mut self, budget: &Meter, id: FunctionId, pc: usize) -> Result<(), RuntimeError> {
1011 stopped_here(self.cancellation.as_ref(), &self.stops, self.span(id, pc))?;
1012 let gathered = self.work() - self.charged_work;
1013 self.charged_work = self.work();
1014 if let Err(stopped) = budget.safepoint(gathered) {
1015 return Err(budget.to_runtime_error(stopped).at(self.span(id, pc)));
1016 }
1017 let live = Live(self);
1018 self.mem.poll(&live);
1019 Ok(())
1020 }
1021
1022 #[inline]
1023 fn next_question(&self) -> u64 {
1024 match self.debugger {
1025 Some(_) => self.instructions + 1,
1026 // Instruction coordinates, because that is what the loop
1027 // compares: the count at which `work()` will have reached a
1028 // stride past the last charge. It saturates because a bulk charge
1029 // can already have passed it, and a check that is due now is
1030 // exactly what zero asks for.
1031 None => (self.charged_work + SAFEPOINT_STRIDE).saturating_sub(self.bulk_work),
1032 }
1033 }
1034
1035 /// What every collection so far has done.
1036 pub(crate) fn collected(&self) -> Collected {
1037 self.collected
1038 }
1039
1040 /// Words the heap region occupies, free blocks included.
1041 pub(crate) fn heap_words(&self) -> u64 {
1042 self.mem.heap_words()
1043 }
1044
1045 /// Words handed out over the whole run, reuse counted each time.
1046 pub(crate) fn allocated_words(&self) -> u64 {
1047 self.mem.allocated_words()
1048 }
1049
1050 /// Objects handed out over the whole run, reuse counted each time.
1051 pub(crate) fn allocations(&self) -> u64 {
1052 self.mem.allocations()
1053 }
1054
1055 /// How long this machine has waited on hosts.
1056 pub(crate) fn host_wait(&self) -> Duration {
1057 self.host_wait
1058 }
1059
1060 /// Where the most recent failed assertion was written, and the message
1061 /// it produced, or `None` when none has failed.
1062 pub(crate) fn assertion_failure(&self) -> Option<(Span, &str)> {
1063 self.assertion_failure
1064 .as_ref()
1065 .map(|(span, message)| (*span, message.as_str()))
1066 }
1067
1068 /// Runs `entry` with `args` already in word form, answering the words of
1069 /// its result.
1070 ///
1071 /// The caller converts: this is below the boundary, and nothing here
1072 /// knows what a public `Value` is. `args` is the parameters' words
1073 /// flattened in declaration order — a `(Int, Point, Int)` list is four
1074 /// words — because that is what the frame they are written into is.
1075 pub(crate) fn run(
1076 &mut self,
1077 entry: FunctionId,
1078 args: &[u64],
1079 budget: &Meter,
1080 ) -> Result<Vec<u64>, RuntimeError> {
1081 // Before anything: a program with no encoding is refused here, with
1082 // the stacks and the cells exactly as this call found them. There is
1083 // one execution form, so this is not a fallback — it is the whole of
1084 // what "verified once and then trusted" costs a caller.
1085 let code = self.code()?;
1086 let program = self.program;
1087 let function = program.function(entry);
1088 // And before anything else: a heap that could not hold every
1089 // literal is refused here too, at the entry's own span rather than
1090 // a literal's — the program is what could not be started, not the
1091 // string. See ADR 0045.
1092 self.literals().map_err(|error| error.at(function.span))?;
1093 debug_assert_eq!(
1094 args.len(),
1095 function.param_words(&program.layouts) as usize,
1096 "an entry is called with its parameters' words"
1097 );
1098
1099 // On an empty stack, and that has to be said rather than assumed. A
1100 // machine is built once and called many times — `Vm::invoke_within`
1101 // exists so that one bounded invocation can be stopped without
1102 // damaging the session that made it — and a call that was stopped
1103 // where it stood left its frames standing, because a runtime error is
1104 // not a jump the lowering emits and nothing unwound them. Building
1105 // this call on top of those would give `Inst::Return` a caller to
1106 // resume that belongs to a call that is over: the answer would be
1107 // written into an abandoned frame and the run would continue in it.
1108 //
1109 // The cells go back for the same reason and in the same breath. A
1110 // `lock` region the stopped call was inside was left by no
1111 // `SharedUnlock`, and a cell nobody gives back is a cell no task can
1112 // ever take again.
1113 self.give_cells_back(0);
1114 self.frames.clear();
1115 self.mem.reset_stack();
1116 self.temps.clear();
1117
1118 let base = self
1119 .mem
1120 .push_frame(function.frame_size())
1121 .map_err(|Overflow| self.too_deep(function.span))?;
1122 for (slot, word) in args.iter().enumerate() {
1123 self.mem.set_slot(base, slot as u32, *word);
1124 }
1125 self.frames.push(Frame {
1126 function: entry,
1127 base,
1128 pc: 0,
1129 dst: 0,
1130 });
1131 self.drive(&code, budget)
1132 }
1133
1134 /// Runs the frame already on the stack, inside the thread scope its
1135 /// `spawn`s start their children in.
1136 ///
1137 /// The scope is here rather than around the whole run because it is what
1138 /// bounds a task's threads to a task: nothing this body starts can outlive
1139 /// this call, so the borrow a child holds of the program, the hosts and
1140 /// the run is a borrow the compiler can check rather than one this module
1141 /// has to promise. A child that spawns children of its own opens a scope
1142 /// of its own, nested, and leaves it before it answers.
1143 ///
1144 /// Whatever the body did, no thread leaves here with work to do. A
1145 /// runtime error is not a jump the lowering emits, so there is no
1146 /// `ScopeCancel` on that path and nothing would otherwise cancel the
1147 /// children of a scope the error left — and `std::thread::scope` would
1148 /// then wait for a task that is waiting for nobody. [`Machine::stop_all`]
1149 /// is that path, and on the ordinary one it has nothing to do because
1150 /// every scope was already left where it was written.
1151 ///
1152 /// It is `#[inline(never)]` for what `a78a8ad` measured one phase ago:
1153 /// two large dispatch calls in one closure cost the loop 2.5% on `arith`
1154 /// even though the branch between them was taken once per run. There is
1155 /// one call here now and one loop to call, so the measurement no longer
1156 /// has a second body to be about — but what it established is that this
1157 /// closure's contents are a cost every instruction pays, and the
1158 /// attribute is what keeps that from being re-decided by an inliner.
1159 #[inline(never)]
1160 fn drive(
1161 &mut self,
1162 code: &cove_ir::bytecode::Encoded,
1163 budget: &Meter,
1164 ) -> Result<Vec<u64>, RuntimeError> {
1165 std::thread::scope(|threads| {
1166 let mut running: Vec<Option<ScopedJoinHandle<'_, Outcome>>> = Vec::new();
1167 let answer = encoded::dispatch(self, code, budget, threads, &mut running, 0);
1168 // The frames are not unwound on this path — see the module-level
1169 // note beside `Machine::frames` — so they are still exactly what
1170 // was live when the error was raised. This is the one place that
1171 // reads them for it: every error leaves the machine through here,
1172 // whichever instruction or safepoint raised it, so a chain
1173 // attached at every `.at()` site instead would be the same work
1174 // repeated at every one of them for no error that reaches two.
1175 let answer = answer.map_err(|error| error.with_chain(self.call_chain()));
1176 debug_assert!(
1177 answer.is_err() || !self.anything_running(),
1178 "a body that answered left every scope it opened, so nothing is still running"
1179 );
1180 debug_assert!(
1181 answer.is_err() || self.held.is_empty(),
1182 "a body that answered left every cell it took"
1183 );
1184 self.give_cells_back(0);
1185 self.stop_all(&mut running);
1186 // Last, after the answer is settled and after the children are
1187 // joined, so that what is put back is everything this thread
1188 // dispatched and nothing is added to the run's total once the
1189 // total has been read.
1190 //
1191 // [ADR 0024](../../../../docs/adr/0024-a-stop-is-a-bound-not-a-point.md)
1192 // says pending fuel is never lost, and a loop that lost it would
1193 // report a `fuel_spent` below the instructions it dispatched.
1194 self.spend_pending_fuel(budget);
1195 answer
1196 })
1197 }
1198
1199 /// Hands the run's [`Meter`] whatever this thread has dispatched and not
1200 /// yet paid for, at the end of a run or of a spawned task's thread.
1201 ///
1202 /// The ordinary way out of a body pays on its way: a run long enough to
1203 /// reach a periodic safepoint has handed over every whole stride of it,
1204 /// and a Host call has handed over the part of the stride that preceded
1205 /// it. What pays for nothing is the remainder — the instructions after
1206 /// the last hand-over — and every way a run can end without dispatching
1207 /// another instruction is a way that remainder would be dropped with the
1208 /// stacks: a raised error, an exhausted budget, a cancelled task, a
1209 /// bounded call the host abandoned. Each of those leaves through Rust's
1210 /// `?` rather than through an instruction.
1211 ///
1212 /// The work was really done, so the run is charged for it.
1213 /// [ADR 0024](../../../../docs/adr/0024-a-stop-is-a-bound-not-a-point.md)
1214 /// decides that pending fuel is never lost, and a `fuel_spent` below the
1215 /// instructions the run dispatched is the observable form of losing it.
1216 ///
1217 /// [`Meter::spend`] rather than [`Meter::safepoint`], and the difference
1218 /// is the whole reason this is its own function: this runs after the
1219 /// answer is settled, and a stop raised here would replace the reason the
1220 /// run actually ended. A run that raised would report that it was out of
1221 /// fuel.
1222 ///
1223 /// A spawned task's thread reaches this through its own [`Machine::drive`]
1224 /// and pays into the same accounting, because ADR 0008 draws a task's
1225 /// fuel from the run's budget rather than giving each task one of its
1226 /// own.
1227 fn spend_pending_fuel(&mut self, budget: &Meter) {
1228 let pending = self.work() - self.charged_work;
1229 if pending != 0 {
1230 self.charged_work = self.work();
1231 budget.spend(pending);
1232 }
1233 }
1234
1235 /// Hands over what this thread has dispatched and not yet paid for, and
1236 /// asks the run's accounting whether it may continue — at a Host call,
1237 /// before the call is dispatched.
1238 ///
1239 /// # The contract
1240 ///
1241 /// **No Host call begins once the fuel a run has been charged has reached
1242 /// its limit.**
1243 /// [ADR 0030](../../../../docs/adr/0030-a-host-call-asks-the-fuel-limit.md)
1244 /// decides that, and it is a statement about the bound rather than about
1245 /// the count: what the two backends share is the property, not the number
1246 /// that satisfies it. The oracle satisfies it by holding no pending fuel
1247 /// at all — `Interpreter::charge_safepoint` hands `SAFEPOINT_FUEL` over in
1248 /// the same call that charges it, so its charged total cannot move while
1249 /// a straight line runs. This machine holds pending fuel by construction,
1250 /// because it charges on a fixed instruction stride, so it satisfies it
1251 /// the other way ADR 0030 allows: by flushing here.
1252 ///
1253 /// Without this, a Host call is just another instruction the stride
1254 /// counts, and a straight line of them shorter than one
1255 /// [`SAFEPOINT_STRIDE`] is not stopped at any fuel limit whatever —
1256 /// forty effects under a limit of one, which is the shape ADR 0030 was
1257 /// written to refuse.
1258 ///
1259 /// # Why this is not a safepoint
1260 ///
1261 /// It asks the budget and nothing else. The two flags a thread owns are
1262 /// read by the caller one line above, so repeating them here would be
1263 /// asking a question that has just been answered.
1264 ///
1265 /// The collector is the interesting half. The predecessor's argument for
1266 /// leaving it out was that its arguments had already been drained into a
1267 /// `Vec<Value>` and were therefore rooted by their own references rather
1268 /// than by the walk. **That argument does not hold here, and the
1269 /// conclusion still does.** The arguments this boundary converts are
1270 /// [`Value`]s built by [`crate::vm::boundary::to_value`], which copies
1271 /// out of the heap rather than naming it, and the words they were read
1272 /// from are still in the slots of a frame this machine has not left — so
1273 /// a collection here would be as sound as one anywhere else, and
1274 /// [`Machine::park`] is about to publish exactly those roots for the
1275 /// length of the call. The reason not to collect is therefore the second
1276 /// half of the predecessor's: this machine's collection point is a
1277 /// rendezvous poll, and putting one in front of every Host call would
1278 /// make an unpredictable sweep part of the cost of reaching the outside
1279 /// world, for a reason the budget never asked for.
1280 fn charge_at_host_boundary(&mut self, budget: &Meter, span: Span) -> Result<(), RuntimeError> {
1281 let pending = self.work() - self.charged_work;
1282 self.charged_work = self.work();
1283 if let Err(stopped) = budget.safepoint(pending) {
1284 return Err(budget.to_runtime_error(stopped).at(span));
1285 }
1286 Ok(())
1287 }
1288
1289 /// Runs the body of a spawned closure, which is this machine's whole
1290 /// task.
1291 ///
1292 /// The closure takes no parameters — `scope.spawn { ... }` is written
1293 /// with none and an `async fn`'s handle carries none — so its frame is
1294 /// its captures and its locals. The captures are copied out of the
1295 /// environment exactly as [`cove_ir::Inst::CallClosure`] copies them,
1296 /// because it
1297 /// is the same object read the same way; what differs is only that the
1298 /// caller is a thread rather than an instruction.
1299 fn enter_closure(
1300 &mut self,
1301 object: u64,
1302 budget: &Meter,
1303 span: Span,
1304 ) -> Result<Vec<u64>, RuntimeError> {
1305 // A task machine was handed its parent's encoding and its parent's
1306 // placed literals, so both of these are `Ok`; each is asked rather
1307 // than assumed because the alternative is an `expect` in the one
1308 // place a task's body starts.
1309 let code = self.code()?;
1310 self.literals().map_err(|error| error.at(span))?;
1311 let program = self.program;
1312 let callee = self.callee_of(object)?;
1313 let target = program.function(callee);
1314 if !target.params.is_empty() {
1315 return Err(wrong_arity(target.qualified(), target.params.len(), 0).at(span));
1316 }
1317 let base = self
1318 .mem
1319 .push_frame(target.frame_size())
1320 .map_err(|Overflow| self.too_deep(span))?;
1321 let mut held = 1;
1322 for capture in &target.captures {
1323 let width = program.layout(capture.layout).width();
1324 self.mem.copy_words(
1325 base + capture.slot as u64,
1326 self.mem.payload_addr(object, held),
1327 width,
1328 );
1329 held += width;
1330 }
1331 self.frames.push(Frame {
1332 function: callee,
1333 base,
1334 pc: 0,
1335 dst: 0,
1336 });
1337 self.drive(&code, budget)
1338 }
1339
1340 /// The debugger's question, out of line.
1341 ///
1342 /// `pc` has been synced by the caller, which is what makes the frame it
1343 /// reads truthful, and this runs once per instruction while a debugger is
1344 /// installed and never otherwise.
1345 ///
1346 /// `#[inline(never)]` is a measurement rather than a preference.
1347 /// `docs/VM_ARCHITECTURE.md` has established more than once that a change
1348 /// altering nothing a program executes can still move `arith` by several
1349 /// percent, *"because the dispatch body's footprint and its branch-target
1350 /// alignment are costs every program pays"* — and building the [`Stop`]
1351 /// and the indirect call inside [`encoded::dispatch`] cost 4.3% on
1352 /// `arith` against the same tree with this line, which is more than the
1353 /// per-instruction branch this whole arrangement was shaped to avoid.
1354 /// Out of line, the change measures at or below the base on both `arith`
1355 /// and `field`.
1356 #[inline(never)]
1357 fn ask(&mut self, id: FunctionId, pc: usize) -> Result<(), RuntimeError> {
1358 if let Some(debugger) = self.debugger {
1359 if let Resume::Halt = debugger.at(&Stop::new(self, id, pc)) {
1360 return Err(halted(self.span(id, pc)));
1361 }
1362 }
1363 Ok(())
1364 }
1365
1366 /// Writes the local program counter back into the top frame.
1367 ///
1368 /// Called before anything that reads the frames: a collection, which
1369 /// walks them for roots, and a failure, which reads a span out of one.
1370 fn sync(&mut self, pc: usize) {
1371 if let Some(frame) = self.frames.last_mut() {
1372 frame.pc = pc as u32;
1373 }
1374 }
1375
1376 fn span(&self, id: FunctionId, pc: usize) -> Span {
1377 self.program.function(id).span_at(pc)
1378 }
1379
1380 fn repr(&self, id: FunctionId, slot: Slot) -> Option<Repr> {
1381 self.program.function(id).repr(slot)
1382 }
1383
1384 fn too_deep(&self, span: Span) -> RuntimeError {
1385 self.too_deep_error().at(span)
1386 }
1387
1388 fn too_deep_error(&self) -> RuntimeError {
1389 RuntimeError::new("this call nests too deeply")
1390 .with_rule("A recursion that does not terminate is stopped rather than left to run.")
1391 }
1392
1393 /// Refuses a frame that would take this task past the embedder's
1394 /// [`Limits::max_call_depth`].
1395 ///
1396 /// This is a *budget*, and the difference from
1397 /// [`Machine::too_deep_error`] is the whole reason it is a second check.
1398 /// A frame that would leave this task's stack segment is a stack
1399 /// overflow: a fact about the memory the run was built with, reported as
1400 /// a runtime error and classified as one. `max_call_depth` is a number an
1401 /// embedder chose, reported as a stop, and classified as
1402 /// [`RunOutcome::CallDepth`] — so a tool that reads a trace can tell a
1403 /// program that recursed too far from a limit the embedder set, which is
1404 /// what makes either bound worth acting on. Both stay, and they are asked
1405 /// in the order the oracle asks them: the unconditional limit first, then
1406 /// the configured one.
1407 ///
1408 /// Counted against *this* task's frames rather than against a total the
1409 /// run shares, for the reason [`crate::budget::Budget`] gives for not
1410 /// enforcing this limit itself: ADR 0008 gives each task a stack of its
1411 /// own, and a shared count would stop a shallow task because a sibling
1412 /// was deep.
1413 ///
1414 /// The limit is read off the meter at the call rather than cached in a
1415 /// field, and that is a decision about where the answer lives. A
1416 /// `Machine` holds no [`Meter`] — the run's accounting is passed into
1417 /// [`encoded::dispatch`], which is what lets one machine serve a session
1418 /// of invocations each bounded by a budget of its own. A field would have
1419 /// to be re-bound every time [`crate::Vm::invoke_within`] installed one,
1420 /// and a stale one would enforce the previous request's limit on this
1421 /// request. Reading it here cannot go stale, because it comes from the
1422 /// very meter the safepoints of this run are charging. It costs one
1423 /// pointer chase through an `Arc` to a field that does not change while a
1424 /// run lasts, on a path that is already writing a frame.
1425 ///
1426 /// [`Limits::max_call_depth`]: crate::budget::Limits::max_call_depth
1427 /// [`RunOutcome::CallDepth`]: crate::trace::RunOutcome::CallDepth
1428 fn admit_frame(&self, budget: &Meter, span: Span) -> Result<(), RuntimeError> {
1429 if let Some(limit) = budget.limits().max_call_depth {
1430 if self.frames.len() + 1 > limit {
1431 // The error names the value the limit was configured with, so
1432 // it is built where that value is rather than here.
1433 return Err(budget.to_runtime_error(Stopped::CallDepth).at(span));
1434 }
1435 }
1436 Ok(())
1437 }
1438
1439 /// How many words a value of `layout` occupies.
1440 ///
1441 /// The one question every move in the machine asks now: a value location
1442 /// is a base slot and a width, and this is the width. It is a table read
1443 /// rather than a walk, because [`cove_ir::Layout`] caches the flattened
1444 /// words for exactly the readers that are on this path.
1445 #[inline]
1446 /// The index of `case` in the enum `layout`, remembered.
1447 ///
1448 /// Nothing is searched for. Which `Option` or `Result` a builtin answers
1449 /// is carried by [`cove_ir::Inst::CallBuiltin`] and passed down from
1450 /// `vm::builtins::call`, because the alternative — looking for an enum of
1451 /// that name whose carrying case holds the right payload — cannot tell
1452 /// `Result<String, Error>` from `Result<String, cq.diag.Detail>`. Both are
1453 /// named `Result` and both carry a `String` in `Ok`, and they are two
1454 /// words and four; answering the wrong one is a word run written into a
1455 /// destination sized for the other.
1456 ///
1457 /// What *is* remembered is which index the name resolves to; see
1458 /// [`Machine::cases`]. `family` and `case` are the names a diagnostic uses
1459 /// when `layout` is not the enum it was expected to be.
1460 pub(crate) fn case_index(
1461 &mut self,
1462 layout: LayoutId,
1463 wrapper: Wrapper,
1464 family: &str,
1465 case: &str,
1466 ) -> Result<u32, RuntimeError> {
1467 if let Some((held, index)) = self.cases[wrapper as usize] {
1468 if held == layout {
1469 return Ok(index);
1470 }
1471 }
1472 let index = self
1473 .program
1474 .layouts
1475 .get(layout.index())
1476 .filter(|held| matches!(held.shape, Shape::Enum { .. }))
1477 .and_then(|held| held.case(case))
1478 .ok_or_else(|| crate::vm::builtins::operand::unknown_family(family))?;
1479 self.cases[wrapper as usize] = Some((layout, index));
1480 Ok(index)
1481 }
1482
1483 fn width(&self, layout: LayoutId) -> u32 {
1484 self.widths[layout.index()]
1485 }
1486
1487 /// Allocates, collecting once if the first attempt does not fit.
1488 ///
1489 /// `len` is `i64` rather than `u32` because this is the one place every
1490 /// `Inst::Alloc` operand converges on, and one of its three `Len` forms —
1491 /// `Len::Slot` — is a slot the running program computed, not a count this
1492 /// compiler chose. `cove_ir::bytecode::verify`'s own module doc says why
1493 /// that boundary cannot be trusted ahead of time: *"this must be safe
1494 /// against arbitrary bytes, because a verifier that is only safe against
1495 /// its own encoder is not a verifier"* — and `Len::Count`'s encoded form,
1496 /// `Half::Count`, is one of the two halves that same verifier explicitly
1497 /// does not range-check, because there is no table for a raw count to be
1498 /// an index into. So every caller here, trusted or not, is checked the
1499 /// same way: a negative `len`, a `len` past what the header's length
1500 /// field can hold, and a `count * stride` that would not fit `u32` are
1501 /// all rejected before anything is allocated.
1502 ///
1503 /// They are rejected through the same error an exhausted heap already
1504 /// raises below rather than a new one of their own, because from the
1505 /// machine's point of view a request nothing could ever satisfy is not a
1506 /// different failure than one this run's budget happens not to satisfy
1507 /// today.
1508 fn allocate(&mut self, layout: LayoutId, len: i64) -> Result<u64, RuntimeError> {
1509 let exhausted = || RuntimeError::new("this run has no memory left");
1510 let len = u32::try_from(len).map_err(|_| exhausted())?;
1511 let words = self
1512 .program
1513 .layout(layout)
1514 .try_payload_words(len, &self.program.layouts)
1515 .ok_or_else(exhausted)?;
1516 if let Some(addr) = self.mem.alloc(layout, len, words) {
1517 return Ok(addr);
1518 }
1519 self.collect();
1520 self.mem.alloc(layout, len, words).ok_or_else(exhausted)
1521 }
1522
1523 /// Stops the world and reclaims what nothing this run's tasks hold
1524 /// reaches.
1525 ///
1526 /// This task's own roots are [`Live`]; every other task's are what it
1527 /// published at the safepoint it parked at. The host resource table is
1528 /// among neither and could not be: it holds names rather than addresses,
1529 /// so there is nothing in it for a mark to follow — and a `Repr::Host`
1530 /// word in a frame is not gathered either, because `Function::refs` is
1531 /// `RefMap::of` the `Repr`s and `Repr::Host::is_ref` is false. A
1532 /// `Repr::Task` and a `Repr::Scope` are outside it for the same reason,
1533 /// and what a task's table *does* name is reached through [`Live`].
1534 pub(crate) fn collect(&mut self) {
1535 let done = self.mem.collect(&self.program.layouts, &Live(self));
1536 self.collected = done;
1537 }
1538
1539 /// How many temporary roots are held, for a caller about to take more.
1540 ///
1541 /// The mark to hand back to [`Machine::release_temps`]. Taking it and
1542 /// releasing to it is the whole discipline: a conversion that recurses
1543 /// nests marks, and a conversion that fails releases on the way out
1544 /// because the caller that took the mark is the one that releases it.
1545 pub(crate) fn temps(&self) -> usize {
1546 self.temps.len()
1547 }
1548
1549 /// Holds `addr` as a root until the mark it was taken after is released.
1550 ///
1551 /// What this is for is the window in which an object exists and nothing
1552 /// the collector walks names it: between the allocation of a struct and
1553 /// the write of its last field, the object is reachable only from a Rust
1554 /// local, and building one of those fields can allocate. Without a root
1555 /// here the collector would be right to free it, and the write that
1556 /// followed would land in a free block.
1557 pub(crate) fn push_temp(&mut self, addr: u64) {
1558 self.temps.push(addr);
1559 }
1560
1561 /// Releases every temporary root taken since `mark`.
1562 ///
1563 /// The object is not freed by this; it stops being a root, which is what
1564 /// a root has to do the moment something else names it. Releasing rather
1565 /// than leaving them is what keeps this from becoming the retention the
1566 /// static reference map was careful not to be.
1567 pub(crate) fn release_temps(&mut self, mark: usize) {
1568 self.temps.truncate(mark);
1569 }
1570
1571 /// The resource a [`Repr::Host`] word names, or `None` for a word that
1572 /// names none.
1573 ///
1574 /// `None` is two things, and the caller is what tells them apart: the
1575 /// zero a frame's own zeroing leaves in a slot nothing has written, and a
1576 /// word from somewhere that is not this table. Both are questions about a
1577 /// value crossing rather than about the machine, so both are reported by
1578 /// [`crate::vm::boundary`] and neither is decided here.
1579 pub(crate) fn resource(&self, word: u64) -> Option<Arc<ResourceHandle>> {
1580 self.held_resources()
1581 .get(word.checked_sub(1)? as usize)
1582 .cloned()
1583 }
1584
1585 /// The run's resource table, recovering from a lock a panicking task
1586 /// left poisoned.
1587 ///
1588 /// A table of names is not a state anything recovers *from*: what it held
1589 /// before the panic is exactly what it holds after, because nothing here
1590 /// is ever removed. Refusing every later resource operation of the run
1591 /// because one task panicked would turn a task's failure into the run's,
1592 /// which is the opposite of what a task boundary is for — the same
1593 /// reasoning `Space::allocator` gives about the heap's own lock.
1594 fn held_resources(&self) -> std::sync::MutexGuard<'_, Vec<Arc<ResourceHandle>>> {
1595 self.resources
1596 .lock()
1597 .unwrap_or_else(|held| held.into_inner())
1598 }
1599
1600 /// The word naming `handle`, writing it into the table the first time
1601 /// this run is handed it.
1602 ///
1603 /// Interned rather than appended, so that one resource is one word for
1604 /// the length of a run. ADR 0013 says two handles are equal when they
1605 /// name the same resource, and a table that gave one resource two words
1606 /// would be a table on which comparing the words was not comparing the
1607 /// resources — which is the one thing an untagged word naming a resource
1608 /// has to get right. `task_safe` is not part of the comparison for the
1609 /// same reason it is not part of [`ResourceHandle::names_same`]: it is a
1610 /// fact about the kind, copied onto every handle of it, so two handles
1611 /// naming one resource cannot disagree about it.
1612 ///
1613 /// The scan is linear over the resources this run has been handed. That
1614 /// is the table the host is keeping too, at the size the host keeps it.
1615 pub(crate) fn resource_word(&mut self, handle: &ResourceHandle) -> u64 {
1616 let mut held = self.held_resources();
1617 if let Some(at) = held.iter().position(|kept| kept.names_same(handle)) {
1618 return at as u64 + 1;
1619 }
1620 held.push(Arc::new(handle.clone()));
1621 // One past the index, because a zeroed slot has to mean no resource.
1622 held.len() as u64
1623 }
1624
1625 /// Materialises the arguments, calls the host, and writes its answer
1626 /// back as a word.
1627 ///
1628 /// This follows [`crate::interp::Interpreter::call_host`] rather than
1629 /// inventing an order of its own, because what a host call does is a fact
1630 /// about the language and not about a backend. The registry is what
1631 /// charges [`crate::Budget::charge_host_call`], refuses an ungranted
1632 /// capability, holds the arguments and the answer to the operation's
1633 /// schema, and writes the `HostCall` trace event; a backend that repeated
1634 /// any of that would be a second opinion about a question that already has
1635 /// one. What is left for the machine is the three things only it can do:
1636 /// read the words out as the `Repr`s of the slots they came from, wait,
1637 /// and write the answer back.
1638 ///
1639 /// The run's own cancellation is not checked here. The oracle checks a
1640 /// *task's* flag and the flag of every bounded call its thread is inside,
1641 /// neither of which this machine has yet; the run's flag is read inside
1642 /// the boundary by `charge_host_call`, which is where it is read on every
1643 /// backend.
1644 #[allow(clippy::too_many_arguments)]
1645 fn call_host<'s>(
1646 &mut self,
1647 base: u64,
1648 op: HostOpId,
1649 args: ArgsId,
1650 budget: &Meter,
1651 span: Span,
1652 threads: &'s Scope<'s, 'a>,
1653 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
1654 ) -> Result<Vec<u64>, RuntimeError> {
1655 let program = self.program;
1656 let op = program.host_op(op);
1657 let list = program.arg_list(args);
1658
1659 // An argument names a value location, so each one materialises as
1660 // the whole of what is at it. That is why a struct or an enum
1661 // reaches a host as itself: it used to be boxed on the way in — a
1662 // slot said where an operand began and never how wide it was — and
1663 // the host was handed an erased value where the schema declared a
1664 // concrete one.
1665 let mut values = Vec::with_capacity(list.len());
1666 for arg in list {
1667 let words = self
1668 .mem
1669 .read_words(base + arg.slot as u64, self.width(arg.layout));
1670 values.push(
1671 boundary::to_value(self, arg.layout, &words).map_err(|error| error.at(span))?,
1672 );
1673 }
1674
1675 let hosts = self.hosts.ok_or_else(|| {
1676 RuntimeError::new(format!(
1677 "`{}.{}` cannot be called, because this run has no host boundary",
1678 op.module, op.operation
1679 ))
1680 .at(span)
1681 })?;
1682 stopped_here(self.cancellation.as_ref(), &self.stops, span)?;
1683 self.charge_at_host_boundary(budget, span)?;
1684 let started = Instant::now();
1685 let answer = {
1686 // A task inside a host call is not running Cove and its frames do
1687 // not change, so the snapshot it leaves stays true for the whole
1688 // call — and a collection that waited for it instead would be
1689 // waiting for something outside the run altogether. A callback is
1690 // the exception, and [`Back::call`] is where it says so: the
1691 // moment Cove runs again the snapshot stops being true, so the
1692 // park is dropped for exactly as long as the callback runs.
1693 let mut back = Back::parked(self, budget, span, threads, running);
1694 hosts.call_with(&op.module, &op.operation, values, &mut back)
1695 };
1696 self.host_wait += started.elapsed();
1697 let answer = answer.map_err(|error| error.at(span))?;
1698 let result = op.result;
1699 boundary::from_value(self, result, &answer).map_err(|error| error.at(span))
1700 }
1701
1702 /// The same, addressed to the resource the [`Repr::Host`] word in
1703 /// `receiver` names.
1704 ///
1705 /// Everything [`Machine::call_host`] does, through the one seam that
1706 /// differs: `HostRegistry::call_resource` rather than
1707 /// `HostRegistry::call_with`. The grant, the schema on both sides, the
1708 /// budget and the trace are the registry's on this path too — a resource
1709 /// operation is a Host API call and is charged and recorded as one — and
1710 /// this follows `crate::interp::Interpreter::call_host_resource`, which
1711 /// is the same three lines around the same call.
1712 ///
1713 /// The handle is looked up rather than materialised. ADR 0013 makes it a
1714 /// name the host minted, `Machine::resource` is the table that word
1715 /// indexes, and the registry takes it as the thing being addressed — so
1716 /// it is never one of `args`, and the arguments are what the host is
1717 /// handed.
1718 ///
1719 /// A zero word is refused rather than read through. `docs/LINEAR_VM.md`
1720 /// is explicit that the word is one past the index so that a slot nothing
1721 /// has written names no resource, and that zero *"earns the same refusal
1722 /// a null reference does"* — which is this one, because a `Host` slot
1723 /// read before it was given a handle is the same lowering bug reaching
1724 /// the machine.
1725 #[allow(clippy::too_many_arguments)]
1726 fn call_resource<'s>(
1727 &mut self,
1728 base: u64,
1729 receiver: Slot,
1730 op: HostOpId,
1731 args: ArgsId,
1732 budget: &Meter,
1733 span: Span,
1734 threads: &'s Scope<'s, 'a>,
1735 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
1736 ) -> Result<Vec<u64>, RuntimeError> {
1737 let program = self.program;
1738 let op = program.host_op(op);
1739 let list = program.arg_list(args);
1740
1741 let word = self.mem.slot(base, receiver);
1742 let Some(handle) = self.resource(word) else {
1743 return Err(null_object().at(span));
1744 };
1745
1746 let mut values = Vec::with_capacity(list.len());
1747 for arg in list {
1748 let words = self
1749 .mem
1750 .read_words(base + arg.slot as u64, self.width(arg.layout));
1751 values.push(
1752 boundary::to_value(self, arg.layout, &words).map_err(|error| error.at(span))?,
1753 );
1754 }
1755
1756 let hosts = self.hosts.ok_or_else(|| {
1757 RuntimeError::new(format!(
1758 "`{}` cannot be called, because this run has no host boundary",
1759 op.qualified()
1760 ))
1761 .at(span)
1762 })?;
1763 stopped_here(self.cancellation.as_ref(), &self.stops, span)?;
1764 // A resource operation is a Host API call and is bounded as one, so
1765 // ADR 0030's boundary is here for the reason it is in
1766 // [`Machine::call_host`] and in the same order.
1767 self.charge_at_host_boundary(budget, span)?;
1768 let started = Instant::now();
1769 let answer = {
1770 let mut back = Back::parked(self, budget, span, threads, running);
1771 hosts.call_resource(&handle, &op.operation, values, &mut back)
1772 };
1773 self.host_wait += started.elapsed();
1774 let answer = answer.map_err(|error| error.at(span))?;
1775 let result = op.result;
1776 boundary::from_value(self, result, &answer).map_err(|error| error.at(span))
1777 }
1778
1779 /// Reads the operand words out of the frame and hands them to the
1780 /// builtin.
1781 ///
1782 /// An operand is a value location: the layout the argument names and the
1783 /// words at its slot. Both halves are read here rather than in
1784 /// [`crate::vm::builtins`] for the reason the boundary takes them here
1785 /// too — a word is untagged and where it came from is a fact about this
1786 /// frame, which a builtin has no business knowing about.
1787 ///
1788 /// The words are copied into one buffer and the operands point into it,
1789 /// so a builtin reads a whole `Point` without holding a frame and
1790 /// without the argument list having to promise that consecutive operands
1791 /// are adjacent, which it never could: the lowering places each argument
1792 /// where a run of the right shape was free.
1793 fn call_builtin(
1794 &mut self,
1795 base: u64,
1796 dst: Slot,
1797 builtin: BuiltinId,
1798 args: ArgsId,
1799 ) -> Result<(), RuntimeError> {
1800 let program = self.program;
1801 let list = program.arg_list(args);
1802
1803 // The word buffer is [`Machine::builtin_words`], taken out for this
1804 // call and put back at the end — see the field for why that is safe
1805 // and what it costs if it is ever wrong.
1806 let mut words = std::mem::take(&mut self.builtin_words);
1807 words.clear();
1808 words.reserve(list.len());
1809 for arg in list {
1810 let width = self.width(arg.layout);
1811 for at in 0..width {
1812 words.push(self.mem.slot(base, arg.slot + at));
1813 }
1814 }
1815
1816 // The operands point into `words`, so they cannot themselves live in
1817 // the machine beside it: a field borrowed here would have to stay
1818 // borrowed across `builtins::call(self, ...)`, which takes `&mut
1819 // Machine`. [`INLINE_OPERANDS`] is sized for an ordinary fixed-arity
1820 // call — a receiver and a couple of arguments — and only the
1821 // builtins `cove-schema` declares `variadic: true` (`Vector.of`,
1822 // `Map.of`, `Set.of`) can exceed it, so they spill to a `Vec` sized
1823 // to the call instead of paying for a larger array on every call.
1824 let mut inline: [Operand; INLINE_OPERANDS] = [Operand {
1825 layout: LayoutId(0),
1826 words: &[],
1827 }; INLINE_OPERANDS];
1828 let mut spill: Vec<Operand>;
1829 let operands: &[Operand] = if list.len() <= INLINE_OPERANDS {
1830 let mut offset = 0usize;
1831 for (slot, arg) in inline.iter_mut().zip(list) {
1832 let width = self.width(arg.layout) as usize;
1833 *slot = Operand {
1834 layout: arg.layout,
1835 words: &words[offset..offset + width],
1836 };
1837 offset += width;
1838 }
1839 &inline[..list.len()]
1840 } else {
1841 let mut offset = 0usize;
1842 spill = Vec::with_capacity(list.len());
1843 for arg in list {
1844 let width = self.width(arg.layout) as usize;
1845 spill.push(Operand {
1846 layout: arg.layout,
1847 words: &words[offset..offset + width],
1848 });
1849 offset += width;
1850 }
1851 &spill
1852 };
1853
1854 let mut out = std::mem::take(&mut self.builtin_answer);
1855 out.clear();
1856 let answered = builtins::call(self, program.builtin(builtin), operands, &mut out);
1857
1858 // Written into the frame here rather than by the dispatch loop,
1859 // because the buffer has to come back: handing the answer out as a
1860 // `Vec` would hand out the allocation with it, and this field would
1861 // find itself empty on the next call and allocate again — which is
1862 // the whole thing it exists not to do.
1863 if answered.is_ok() {
1864 for (at, word) in out.iter().enumerate() {
1865 self.mem.set_slot(base, dst + at as u32, *word);
1866 }
1867 }
1868
1869 // Both buffers may have grown past what was already here — keep
1870 // whichever of each pair has the larger capacity, per the fields'
1871 // doc comments.
1872 if words.capacity() >= self.builtin_words.capacity() {
1873 self.builtin_words = words;
1874 }
1875 if out.capacity() >= self.builtin_answer.capacity() {
1876 self.builtin_answer = out;
1877 }
1878 answered
1879 }
1880
1881 /// Places every entry of [`Program::strings`] into the heap, in
1882 /// [`StrId`] order, before this machine's first instruction can run and
1883 /// therefore before any other object exists. Called once, by
1884 /// [`Machine::for_run`]; [`Machine::for_task`] is handed the `Arc` this
1885 /// builds rather than calling it again. See
1886 /// [ADR 0045](../../../../docs/adr/0045-a-literal-is-there-before-the-program-runs.md).
1887 ///
1888 /// Not [`Machine::allocate`], on purpose: that collects and retries when
1889 /// an allocation does not fit, and every object a collection could find
1890 /// at this point *is* a literal this very call is still placing —
1891 /// nothing else has run yet, so there is nothing else to reclaim.
1892 /// Collecting here would sweep an already-placed literal the moment
1893 /// before [`Memory::seal_static`] could tell the collector to leave it
1894 /// alone, and a later literal reusing the freed run would land on the
1895 /// address an earlier one already handed out. A heap that does not fit
1896 /// them once will not fit them a second time, so this fails immediately
1897 /// instead of trying twice for the same answer.
1898 fn place_literals(&mut self) -> Result<Arc<[u64]>, RuntimeError> {
1899 let exhausted = || RuntimeError::new("this run has no memory left");
1900 let mut addrs = Vec::with_capacity(self.program.strings.len());
1901 for index in 0..self.program.strings.len() {
1902 let bytes = Arc::clone(self.program.string(StrId(index as u32)));
1903 let len = u32::try_from(bytes.len()).map_err(|_| exhausted())?;
1904 let words = self
1905 .program
1906 .layout(self.program.str_layout)
1907 .try_payload_words(len, &self.program.layouts)
1908 .ok_or_else(exhausted)?;
1909 let addr = self
1910 .mem
1911 .alloc(self.program.str_layout, len, words)
1912 .ok_or_else(exhausted)?;
1913 self.write_bytes(addr, bytes.as_bytes());
1914 addrs.push(addr);
1915 }
1916 self.mem.seal_static();
1917 Ok(addrs.into())
1918 }
1919
1920 /// The address of the literal `text` names.
1921 ///
1922 /// Reached only from the dispatch loop's `STR` arm, after
1923 /// [`Machine::run`] or [`Machine::enter_closure`] has already turned a
1924 /// placement failure into a refusal before any frame existed — so by
1925 /// the time an instruction asks, the answer is always there, and this
1926 /// is a load rather than a question.
1927 #[inline]
1928 fn literal_addr(&self, text: StrId) -> u64 {
1929 self.literal_addrs
1930 .as_ref()
1931 .expect("a placement failure is refused before this machine's first frame")
1932 [text.index()]
1933 }
1934
1935 /// The layout a [`cove_ir::Inst::Box`] allocates its object as.
1936 ///
1937 /// The program says, rather than this searching for a `Shape::Boxed`.
1938 /// A search has to answer something when it fails, and the answer it
1939 /// used to give — `LayoutId::FREE` — sized the object by the wrong
1940 /// shape, so a box of a two-word value was allocated one word short and
1941 /// the copy into it ran off the end of the heap.
1942 fn boxed_layout(&self) -> LayoutId {
1943 self.program.boxed_layout
1944 }
1945
1946 /// Checks that `addr` is an object with a payload word `at`.
1947 ///
1948 /// A reference slot carries no layout, so the object is the only thing
1949 /// that can say how wide it is. The lowering computed `at` from the type
1950 /// the checker settled, so this should never refuse — and it is here
1951 /// because "should never" is not "cannot", and reading past an object
1952 /// into whatever follows it would be a silent wrong answer rather than a
1953 /// loud one.
1954 fn checked(&self, addr: u64, at: u32, width: u32) -> Result<(), RuntimeError> {
1955 if addr == 0 {
1956 return Err(null_object());
1957 }
1958 let layout = self.program.layout(self.mem.object_layout(addr));
1959 let words = layout.payload_words(self.mem.object_len(addr), &self.program.layouts);
1960 if at + width > words {
1961 return Err(RuntimeError::new(format!(
1962 "this reads word {at} of a `{}`, which has {words}",
1963 layout.name
1964 )));
1965 }
1966 Ok(())
1967 }
1968
1969 /// The function the closure object at `addr` calls.
1970 ///
1971 /// Three things have to hold before a frame is pushed, and none of them
1972 /// is something a program can get wrong: the object has to be a closure's,
1973 /// the callee it names has to be one this program has, and the captures it
1974 /// holds have to be the ones that callee reads. The checker resolved the
1975 /// callee's type and the verifier holds the slot to `Repr::Ref`, so each
1976 /// of the three is a lowering bug — reported for the reason
1977 /// [`Machine::checked`] is, because the alternative is a frame whose
1978 /// capture slots hold whatever followed the object in the heap.
1979 ///
1980 /// The id comes from the object rather than from the layout, which carries
1981 /// one too. They agree — a layout is one per lowered lambda — and the
1982 /// object's word is the one [`cove_ir::Inst::CallClosure`] is defined in
1983 /// terms of.
1984 pub(crate) fn callee_of(&self, addr: u64) -> Result<FunctionId, RuntimeError> {
1985 if addr == 0 {
1986 return Err(null_object());
1987 }
1988 let program = self.program;
1989 let layout = program.layout(self.mem.object_layout(addr));
1990 let Shape::Closure { captures, .. } = &layout.shape else {
1991 // The oracle's words for a call of something that is not a
1992 // function, with the name the layout carries — which is the name
1993 // the declaration wrote, and so the one a `Value` of this object
1994 // would answer.
1995 return Err(RuntimeError::new(format!(
1996 "`{}` is not callable",
1997 layout.name
1998 )));
1999 };
2000 let word = self.mem.payload(addr, 0);
2001 let callee = u32::try_from(word)
2002 .ok()
2003 .map(FunctionId)
2004 .filter(|id| id.index() < program.functions.len())
2005 .ok_or_else(|| {
2006 RuntimeError::new(format!(
2007 "this closure names function {word}, which this program has not"
2008 ))
2009 })?;
2010 let target = program.function(callee);
2011 if target.captures.len() != captures.len() {
2012 return Err(RuntimeError::new(format!(
2013 "this closure and `{}` disagree about its captures: {} held, {} read",
2014 target.qualified(),
2015 captures.len(),
2016 target.captures.len()
2017 )));
2018 }
2019 Ok(callee)
2020 }
2021
2022 /// Turns a language-level index into a payload offset, at a stride of
2023 /// `width`.
2024 ///
2025 /// The header's length counts *elements*, not words, so an index is
2026 /// checked against it and then multiplied — which is what makes an
2027 /// `Array<Point>` a run of two-word elements and an out-of-range index on
2028 /// one say the same thing it says on an `Array<Int>`.
2029 fn element(&self, addr: u64, at: i64, width: u32) -> Result<u32, RuntimeError> {
2030 if addr == 0 {
2031 return Err(null_object());
2032 }
2033 let len = self.mem.object_len(addr) as i64;
2034 if at < 0 || at >= len {
2035 return Err(
2036 RuntimeError::new(format!("index {at} is outside a collection of {len}"))
2037 .with_rule("An index outside a collection is a broken invariant."),
2038 );
2039 }
2040 Ok(at as u32 * width)
2041 }
2042
2043 /// Orders two string objects by their bytes.
2044 fn compare_strings(&self, a: u64, b: u64) -> std::cmp::Ordering {
2045 self.string_bytes(a).cmp(&self.string_bytes(b))
2046 }
2047
2048 /// The bytes of the string object at `addr`.
2049 ///
2050 /// A null address answers the empty string rather than failing: the one
2051 /// caller that can see one is the comparison, and two strings one of
2052 /// which does not exist is a lowering bug the verifier will catch
2053 /// elsewhere, not something to unwind a comparison for.
2054 pub(crate) fn string_bytes(&self, addr: u64) -> Vec<u8> {
2055 if addr == 0 {
2056 return Vec::new();
2057 }
2058 let len = self.mem.object_len(addr) as usize;
2059 let mut out = Vec::with_capacity(len);
2060 for at in 0..len.div_ceil(8) {
2061 let word = self.mem.payload(addr, at as u32);
2062 for byte in 0..8 {
2063 if out.len() == len {
2064 break;
2065 }
2066 out.push((word >> (byte * 8)) as u8);
2067 }
2068 }
2069 out
2070 }
2071
2072 /// The byte at `at` of the packed run at `addr`.
2073 ///
2074 /// The payload holds eight bytes to a word, least-significant byte first —
2075 /// the inverse of [`Machine::write_bytes`] — so one byte is one payload read
2076 /// and a shift, and no part of the object is copied. The caller owns the
2077 /// bound, as every reader of a packed run here does.
2078 pub(crate) fn byte_of(&self, addr: u64, at: usize) -> u8 {
2079 (self.mem.payload(addr, (at / 8) as u32) >> ((at % 8) * 8)) as u8
2080 }
2081
2082 /// A new string object holding `text`.
2083 ///
2084 /// Unlike [`Machine::place_literals`] this allocates every time, and is
2085 /// never collected out from under it either: a literal is retained
2086 /// because the program named it statically and can name it again; a
2087 /// string that arrived from outside has no such name, and retaining
2088 /// every one a host ever answered would be a leak with a table in front
2089 /// of it.
2090 pub(crate) fn new_string(&mut self, text: &str) -> Result<u64, RuntimeError> {
2091 let addr = self.allocate(self.program.str_layout, text.len() as i64)?;
2092 self.write_bytes(addr, text.as_bytes());
2093 Ok(addr)
2094 }
2095
2096 fn write_bytes(&mut self, addr: u64, bytes: &[u8]) {
2097 for (at, chunk) in bytes.chunks(8).enumerate() {
2098 let mut word = 0u64;
2099 for (byte, value) in chunk.iter().enumerate() {
2100 word |= (*value as u64) << (byte * 8);
2101 }
2102 self.mem.set_payload(addr, at as u32, word);
2103 }
2104 }
2105
2106 /// A new string object of `len` zero bytes, for a caller that will fill
2107 /// it itself.
2108 ///
2109 /// The payload is zero on both of the paths that can answer it — a
2110 /// reused free block is filled and a fresh chunk is committed zeroed —
2111 /// which is what lets a caller write only the bytes it has and leave the
2112 /// tail of the last word alone. That tail matters: `eq.str` compares
2113 /// payload words rather than bytes, so a string whose final word held
2114 /// anything past its length would be unequal to the same text written
2115 /// some other way.
2116 ///
2117 /// Nothing roots the answer, so a caller must fill it without allocating
2118 /// again. That is not a restriction in practice — the point of asking for
2119 /// an exact length is to have counted first.
2120 pub(crate) fn new_string_of(&mut self, len: i64) -> Result<u64, RuntimeError> {
2121 self.allocate(self.program.str_layout, len)
2122 }
2123
2124 // ---- ADR 0052's byte buffer -------------------------------------------
2125
2126 /// [`Inst::AllocBuffer`]: a new, empty byte buffer whose store has room for
2127 /// `capacity` bytes.
2128 ///
2129 /// Two objects, because [ADR 0052](../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)'s
2130 /// growable value is a stable owner over a replaceable store. Neither
2131 /// layout is a choice: the owner is `Program::buffer_layout` and its
2132 /// payload is a fixed two words, and the store is `Program::bytes_layout`
2133 /// with the capacity as its header length.
2134 ///
2135 /// # Which is allocated first, and why nothing is lost
2136 ///
2137 /// Two allocations means a collection may happen between them, and
2138 /// whichever object exists first is reachable from nothing the collector
2139 /// walks — a Rust local is not a root. So the **store is allocated first
2140 /// and held by [`Machine::push_temp`]** for exactly the window in which
2141 /// the owner is allocated, which is the discipline
2142 /// [`crate::vm::boundary`] already uses for a half-built object and the
2143 /// reason `push_temp` exists.
2144 ///
2145 /// The other order was available and is worse. Allocating the owner first
2146 /// is safe only if something names it, which means writing it into the
2147 /// destination slot before the object it owns exists — leaving a buffer
2148 /// with a null store visible to a debugger stopped at the allocation, and
2149 /// making the arm's correctness depend on the frame slot rather than on
2150 /// this method. One temporary root costs a push and a truncate.
2151 ///
2152 /// A negative or oversized `capacity` fails through the same "this run has
2153 /// no memory left" refusal every other allocation does, and it fails
2154 /// before the owner exists rather than leaving one behind.
2155 pub(crate) fn alloc_buffer(&mut self, capacity: i64) -> Result<u64, RuntimeError> {
2156 // A capacity below the floor is raised to it, because a capacity is a
2157 // hint and a small one is still a hint. A *negative* capacity is not
2158 // raised: it is nonsense rather than a small number, and clamping it
2159 // would turn the one arithmetic a caller could not have meant into a
2160 // silent success. `Machine::allocate` already has the answer for a
2161 // length nothing could satisfy.
2162 let capacity = if capacity < 0 {
2163 capacity
2164 } else {
2165 capacity.max(MIN_BUFFER_BYTES as i64)
2166 };
2167 let mark = self.temps();
2168 let store = self.allocate(self.program.bytes_layout, capacity)?;
2169 self.push_temp(store);
2170 // The owner's payload is two words and its header length is zero: a
2171 // fixed-size object's length field says nothing, exactly as a
2172 // `Shape::Vector` header's does not.
2173 let owner = self.allocate(self.program.buffer_layout, 0);
2174 self.release_temps(mark);
2175 let owner = owner?;
2176 // Zeroed by the allocator, so word 0 is already the empty length; it is
2177 // written anyway rather than relied upon, because the one word that
2178 // says how much of the store is value should be set where it is decided.
2179 self.set_payload(owner, BUFFER_LEN, 0);
2180 self.set_payload(owner, BUFFER_STORE, store);
2181 Ok(owner)
2182 }
2183
2184 /// A live buffer at `owner`: its store, its logical length and its
2185 /// capacity.
2186 ///
2187 /// Every one of the three checks is one this machine must make rather than
2188 /// one the verifier could. A slot's `Repr` is `Ref` and nothing static says
2189 /// which family the object behind it belongs to, so reading an arbitrary
2190 /// object's payload word 1 as an address is how a wrong program becomes a
2191 /// write into the middle of the heap. `Inst::CopyBytes` checks its
2192 /// destination's shape for the same reason.
2193 ///
2194 /// A null store is a buffer [`Inst::FinishBuffer`] already consumed. For a
2195 /// checked program that is unreachable — the uniqueness proof is
2196 /// `cove_sema`'s — so reaching it means the proof let one through, and an
2197 /// internal invariant that reports is better than one that reads a null
2198 /// store as an empty buffer. It is `Vector`'s `operand::frozen` in the
2199 /// vocabulary of a buffer.
2200 fn buffer(&self, shown: &str, owner: u64) -> Result<ByteBuffer, RuntimeError> {
2201 if owner == 0 {
2202 return Err(null_object());
2203 }
2204 if !matches!(
2205 self.program.layout(self.mem.object_layout(owner)).shape,
2206 Shape::ByteBuffer
2207 ) {
2208 return Err(RuntimeError::new(format!(
2209 "`{shown}` needs a byte buffer under construction, and this is not one"
2210 )));
2211 }
2212 let store = self.mem.payload(owner, BUFFER_STORE);
2213 if store == 0 {
2214 return Err(RuntimeError::new(format!(
2215 "`{shown}` was called on a byte buffer that `finish()` already consumed"
2216 ))
2217 .with_rule("`finish()` consumes its buffer; the source buffer is no longer usable.")
2218 .with_help("use the `String` that `finish()` returned, or build a new buffer"));
2219 }
2220 let len = self.mem.payload(owner, BUFFER_LEN);
2221 let capacity = self.mem.object_len(store);
2222 if len > u64::from(capacity) {
2223 return Err(RuntimeError::new(format!(
2224 "`{shown}` found a byte buffer of {len} byte(s) in a store of {capacity}"
2225 )));
2226 }
2227 Ok(ByteBuffer {
2228 owner,
2229 store,
2230 len: len as u32,
2231 capacity,
2232 })
2233 }
2234
2235 /// The store of `buffer`, grown if `needed` bytes will not fit in it.
2236 ///
2237 /// The new capacity is `max(needed, capacity * 2, MIN_BUFFER_BYTES)`, which
2238 /// is ADR 0052's "growth uses the existing Vector policy initially" with
2239 /// the one addition a bulk append needs: a doubling that still would not
2240 /// hold the range is not two growths, it is one growth to the length that
2241 /// fits.
2242 ///
2243 /// Arithmetic overflow is rejected before anything is mutated, and it is
2244 /// rejected by the allocator rather than here. `needed` is a `u64` and the
2245 /// doubling is `saturating_mul`, so the only value that can reach
2246 /// [`Machine::allocate`] out of range is one too large to be a header
2247 /// length or too large for `try_payload_words` to size — and both of those
2248 /// answer "this run has no memory left" *before* the owner's store word or
2249 /// its length word is touched. So a refused growth leaves the buffer
2250 /// exactly as it was.
2251 ///
2252 /// The old store is reachable from the owner, which this read out of a
2253 /// frame slot, so the allocation below cannot free it — `seq.rs`'s `grow`
2254 /// makes the same argument for a `Vector`. The new store is unrooted for
2255 /// exactly the copy, which allocates nothing.
2256 ///
2257 /// The copy is the live prefix and nothing else. That is what keeps the
2258 /// spare tail zero: a fresh store is zeroed, and `copy_string_bytes` blends
2259 /// masked bytes rather than whole words, so the bytes of the last partial
2260 /// word above `len` are left as the allocator left them.
2261 fn reserve_bytes(&mut self, buffer: &ByteBuffer, needed: u64) -> Result<u64, RuntimeError> {
2262 if needed <= u64::from(buffer.capacity) {
2263 return Ok(buffer.store);
2264 }
2265 let want = needed
2266 .max(u64::from(buffer.capacity).saturating_mul(2))
2267 .max(MIN_BUFFER_BYTES);
2268 let store = self.allocate(
2269 self.program.bytes_layout,
2270 i64::try_from(want).unwrap_or(i64::MAX),
2271 )?;
2272 self.copy_string_bytes(store, 0, buffer.store, 0, buffer.len as usize);
2273 self.set_payload(buffer.owner, BUFFER_STORE, store);
2274 Ok(store)
2275 }
2276
2277 /// [`Inst::AppendByte`]: one checked byte onto the end of a buffer.
2278 pub(crate) fn append_byte(&mut self, owner: u64, value: i64) -> Result<(), RuntimeError> {
2279 let buffer = self.buffer("appendByte", owner)?;
2280 if !(0..=255).contains(&value) {
2281 return Err(RuntimeError::new(format!(
2282 "`appendByte`'s value is `{value}`, and a byte is 0 to 255"
2283 )));
2284 }
2285 let store = self.reserve_bytes(&buffer, u64::from(buffer.len) + 1)?;
2286 self.put_bytes(store, buffer.len as usize, 1, value as u64);
2287 self.set_payload(buffer.owner, BUFFER_LEN, u64::from(buffer.len) + 1);
2288 Ok(())
2289 }
2290
2291 /// [`Inst::FinishBuffer`]: the buffer's live prefix, validated and
2292 /// relabelled into a `String`, and the owner emptied.
2293 ///
2294 /// The prefix is `[0, len)` and the store may be longer, so only the prefix
2295 /// is validated: the bytes above the logical length are spare room the
2296 /// program never appended and must not be asked to account for.
2297 ///
2298 /// Then ADR 0052's "finishing reuses the store". A `Shape::Bytes` run and a
2299 /// `Shape::Str` object of the same byte length occupy the same number of
2300 /// words, so the store *is* the answer — relabelled down from the capacity
2301 /// to the logical length, with the words in between released as a free
2302 /// block the next sweep folds back in. `spare` is the difference in
2303 /// *payload words* rather than in bytes, because a free block is measured
2304 /// in words; `vector_freeze` computes the same difference in elements
2305 /// times a stride.
2306 ///
2307 /// The tail of the last partial word is zero, which is what makes the
2308 /// answer equal word-for-word to the same text written by
2309 /// [`Machine::new_string`]: allocation zeroes, every append writes only the
2310 /// live prefix, and a growth copies only the live prefix into another
2311 /// zeroed store. `eq.str` compares payload words, so a dirty tail would be
2312 /// a string unequal to itself written another way.
2313 ///
2314 /// Finally the owner is emptied — length zero, store null — because
2315 /// finishing *consumes*, exactly as `Vector.freeze()` empties the vector it
2316 /// consumed.
2317 pub(crate) fn finish_buffer(&mut self, owner: u64) -> Result<u64, RuntimeError> {
2318 let buffer = self.buffer("finishBuffer", owner)?;
2319 let text = self.buffer_bytes(&buffer);
2320 if std::str::from_utf8(&text).is_err() {
2321 return Err(RuntimeError::new("this string's bytes are not valid UTF-8"));
2322 }
2323 let spare = self.payload_words(self.program.bytes_layout, buffer.capacity)
2324 - self.payload_words(self.program.str_layout, buffer.len);
2325 self.relabel(buffer.store, self.program.str_layout, buffer.len, spare);
2326 self.set_payload(buffer.owner, BUFFER_LEN, 0);
2327 self.set_payload(buffer.owner, BUFFER_STORE, 0);
2328 Ok(buffer.store)
2329 }
2330
2331 /// The live prefix of `buffer`, as bytes.
2332 ///
2333 /// [`Machine::string_bytes`]' read bounded by the *owner's* length rather
2334 /// than the store's, which is the whole difference between a buffer and a
2335 /// run: the store's header length is its capacity.
2336 fn buffer_bytes(&self, buffer: &ByteBuffer) -> Vec<u8> {
2337 let len = buffer.len as usize;
2338 let mut out = Vec::with_capacity(len);
2339 for at in 0..len.div_ceil(8) {
2340 let word = self.mem.payload(buffer.store, at as u32);
2341 for byte in 0..8 {
2342 if out.len() == len {
2343 break;
2344 }
2345 out.push((word >> (byte * 8)) as u8);
2346 }
2347 }
2348 out
2349 }
2350
2351 /// The eight bytes of the string object at `addr` beginning at byte `at`,
2352 /// least-significant byte first, and zero past `len`.
2353 ///
2354 /// One payload read when `at` is word-aligned and two when it is not.
2355 /// The second read is guarded by `len` rather than by the object's
2356 /// payload width because a string's last word is the last word it has:
2357 /// reading past it would read whatever the heap put there next.
2358 fn bytes_word(&self, addr: u64, at: usize, len: usize) -> u64 {
2359 let shift = (at % 8) * 8;
2360 let word = self.mem.payload(addr, (at / 8) as u32) >> shift;
2361 if shift == 0 {
2362 return word;
2363 }
2364 let next = at - (at % 8) + 8;
2365 if next >= len {
2366 return word;
2367 }
2368 word | (self.mem.payload(addr, (next / 8) as u32) << (64 - shift))
2369 }
2370
2371 /// Writes the low `count` bytes of `bytes` into payload word `word` of
2372 /// the object at `addr`, at byte `offset`, leaving the rest of the word
2373 /// as it was.
2374 ///
2375 /// A whole aligned word is one store and no load, which is the case a
2376 /// copy between two strings spends nearly all of its time in.
2377 fn blend(&mut self, addr: u64, word: u32, offset: usize, count: usize, bytes: u64) {
2378 debug_assert!(count > 0 && offset + count <= 8);
2379 if offset == 0 && count == 8 {
2380 self.mem.set_payload(addr, word, bytes);
2381 return;
2382 }
2383 let mask = ((1u64 << (count * 8)) - 1) << (offset * 8);
2384 let held = self.mem.payload(addr, word);
2385 self.mem.set_payload(
2386 addr,
2387 word,
2388 (held & !mask) | ((bytes << (offset * 8)) & mask),
2389 );
2390 }
2391
2392 /// Copies `len` bytes of the string object at `src`, from byte `src_at`,
2393 /// into the one at `dst`, from byte `dst_at`.
2394 ///
2395 /// Eight bytes a turn rather than one. The byte-at-a-time version this
2396 /// replaced read a word and shifted it for every byte it copied, which
2397 /// made a copy of *n* bytes *n* payload reads and *n* stores; this makes
2398 /// it *n*/8 of each when both ends are aligned, and at most twice that
2399 /// when neither is.
2400 ///
2401 /// The caller owns the bounds. Every caller here has already established
2402 /// them — a slice from [`crate::vm::builtins::text`]'s `byte_range`, a
2403 /// join from the lengths it summed to size the answer — and an
2404 /// out-of-range write would be a payload write past the object, which is
2405 /// the one thing this must not be asked to check per byte if it is to be
2406 /// worth writing at all.
2407 pub(crate) fn copy_string_bytes(
2408 &mut self,
2409 dst: u64,
2410 dst_at: usize,
2411 src: u64,
2412 src_at: usize,
2413 len: usize,
2414 ) {
2415 // A range copy answers the source as it *was*, which is `memmove` and
2416 // not `memcpy`. The two only differ when the ranges overlap, which
2417 // they can: `Inst::CopyBytes` admits a `Shape::Bytes` source, so `src`
2418 // and `dst` may be the same run — a builder shifting its own bytes
2419 // along is the obvious use and there is no reason for it to be the one
2420 // shape of copy that corrupts.
2421 //
2422 // Overlap only matters within one object, and only in one direction:
2423 // writing *forward* into a range that begins later than the source
2424 // overwrites bytes the copy has not read yet. Everything else — two
2425 // different objects, or a destination at or before the source — is
2426 // safe read-then-write in ascending order.
2427 if dst == src && dst_at > src_at {
2428 self.copy_bytes_descending(dst, dst_at, src, src_at, len);
2429 } else {
2430 self.copy_bytes_ascending(dst, dst_at, src, src_at, len);
2431 }
2432 }
2433
2434 /// Eight bytes a turn, from the front.
2435 fn copy_bytes_ascending(
2436 &mut self,
2437 dst: u64,
2438 dst_at: usize,
2439 src: u64,
2440 src_at: usize,
2441 len: usize,
2442 ) {
2443 let src_len = self.mem.object_len(src) as usize;
2444 let mut done = 0;
2445 while done < len {
2446 let take = (len - done).min(8);
2447 let bytes = self.bytes_word(src, src_at + done, src_len);
2448 self.put_bytes(dst, dst_at + done, take, bytes);
2449 done += take;
2450 }
2451 }
2452
2453 /// Eight bytes a turn, from the back, for a copy that shifts a run's bytes
2454 /// to a higher offset in itself.
2455 fn copy_bytes_descending(
2456 &mut self,
2457 dst: u64,
2458 dst_at: usize,
2459 src: u64,
2460 src_at: usize,
2461 len: usize,
2462 ) {
2463 let src_len = self.mem.object_len(src) as usize;
2464 let mut done = len;
2465 while done > 0 {
2466 let take = done.min(8);
2467 done -= take;
2468 let bytes = self.bytes_word(src, src_at + done, src_len);
2469 self.put_bytes(dst, dst_at + done, take, bytes);
2470 }
2471 }
2472
2473 /// Writes the low `take` bytes of `bytes` at byte `at` of the run at
2474 /// `dst`, which may straddle two payload words.
2475 fn put_bytes(&mut self, dst: u64, at: usize, take: usize, bytes: u64) {
2476 let word = (at / 8) as u32;
2477 let offset = at % 8;
2478 let first = take.min(8 - offset);
2479 self.blend(dst, word, offset, first, bytes);
2480 if first < take {
2481 self.blend(dst, word + 1, 0, take - first, bytes >> (first * 8));
2482 }
2483 }
2484
2485 /// The program this machine runs.
2486 pub(crate) fn program(&self) -> &'a Program {
2487 self.program
2488 }
2489
2490 /// This task's live calls, innermost first, as
2491 /// [`crate::vm::debug`] projects them: which function, where its words
2492 /// begin, and where it is.
2493 ///
2494 /// `pc` is truthful only after a [`Machine::sync`], which is the
2495 /// dispatch loop's obligation and not this function's — it is the whole
2496 /// of what makes a debug stop a stop rather than a guess.
2497 pub(crate) fn calls(&self) -> Vec<(FunctionId, u64, u32)> {
2498 self.frames
2499 .iter()
2500 .rev()
2501 .map(|frame| (frame.function, frame.base, frame.pc))
2502 .collect()
2503 }
2504
2505 /// The call-site spans [`RuntimeError::with_chain`] wants, innermost
2506 /// first: every live frame above the one that is failing.
2507 ///
2508 /// The innermost frame contributes no call site of its own: its `pc` is
2509 /// the error's own span, already `RuntimeError::span` — read there by
2510 /// whatever `fail!` or `.at()` this error passed through, not here. It
2511 /// does contribute the *expanded bodies* that `pc` sits inside, because
2512 /// each of those is a frame that would have been here and is not; see
2513 /// [`Inlined`](cove_ir::program::Inlined).
2514 ///
2515 /// Every frame above it is suspended at *the instruction after* the call
2516 /// that led one level deeper — [`Frame::pc`] says so, and
2517 /// [`crate::vm::debug::Stop::frame`] reads that same `pc` for the
2518 /// debugger's own backtrace. A resume address is not a call site: the
2519 /// instruction it names is whatever runs next, which is frequently the
2520 /// next statement's rather than anything to do with the call, and a
2521 /// label built from it points at the wrong line as often as the right
2522 /// one. `- 1` is always the call itself — `entered!` only ever syncs a
2523 /// `pc` that has already moved past the instruction it just dispatched —
2524 /// so that is what this reads instead. The debugger's own view is left
2525 /// alone; nothing here changes what a resume address is used to display
2526 /// there.
2527 ///
2528 /// Lazy in the depth, so a bound below [`RuntimeError::with_chain`]'s
2529 /// [`crate::error::MAX_CALL_CHAIN`] never walks past it: nothing here
2530 /// builds a `Vec` sized to the recursion depth on its way to being
2531 /// truncated back down. One frame at a time does build a small one — an
2532 /// expansion's sites come out of `inlined_at` outermost first and a
2533 /// chain wants them the other way — and that one is bounded by how
2534 /// deeply expansions nest at a single program counter, not by how deep
2535 /// the recursion is.
2536 fn call_chain(&self) -> impl Iterator<Item = Span> + '_ {
2537 // The bodies that were expanded into the frame this failed in come
2538 // first, innermost outwards. `lower::inline` removes a frame that
2539 // would otherwise be here, and `Function::inlined` is the record of
2540 // what it removed: without it an error inside an expanded body named
2541 // where it happened and nothing about where it was called from, and
2542 // the oracle — which pushes a real frame — named both.
2543 //
2544 // Read at `frame.pc` and not one before it: `fail!` syncs the failing
2545 // instruction's own program counter into the frame before it raises,
2546 // where every frame above is left at the instruction it will *resume*
2547 // at. The two are one apart and this is the one place both are read.
2548 let innermost = self.frames.last().into_iter().flat_map(|frame| {
2549 let function = self.program.function(frame.function);
2550 let mut held: Vec<Span> = function
2551 .inlined_at(frame.pc)
2552 .map(|held| held.site)
2553 .collect();
2554 held.reverse();
2555 held
2556 });
2557 // Then the frames above it, each read at the call it is waiting on —
2558 // and each of *those* through the same record, because a call may
2559 // stand inside an expanded body too.
2560 let outer = self.frames.iter().rev().skip(1).flat_map(|frame| {
2561 let function = self.program.function(frame.function);
2562 let at = frame.pc.saturating_sub(1);
2563 let mut held: Vec<Span> = function.inlined_at(at).map(|held| held.site).collect();
2564 held.reverse();
2565 held.insert(0, function.span_at(at as usize));
2566 held
2567 });
2568 innermost.chain(outer)
2569 }
2570
2571 /// `words` words of the frame based at `base`, from `at`.
2572 pub(crate) fn frame_run(&self, base: u64, at: u32, words: u32) -> Vec<u64> {
2573 self.mem.read_words(base + at as u64, words)
2574 }
2575
2576 /// Whether `words` words at `addr` are words this run's memory has.
2577 ///
2578 /// The question a lossy reader has to ask and a boundary conversion does
2579 /// not: every address the boundary follows came out of a value location
2580 /// the lowering wrote, and a debugger is handed words by whoever is
2581 /// looking. Reading past the end of a region is what this refuses.
2582 pub(crate) fn readable(&self, addr: u64, words: u32) -> bool {
2583 self.mem.holds(addr, words)
2584 }
2585
2586 /// What the object at `addr` is, for a boundary that has to name it.
2587 pub(crate) fn object_layout(&self, addr: u64) -> LayoutId {
2588 self.mem.object_layout(addr)
2589 }
2590
2591 /// The length field of the object at `addr`: elements, or a string's
2592 /// bytes.
2593 pub(crate) fn object_len(&self, addr: u64) -> u32 {
2594 self.mem.object_len(addr)
2595 }
2596
2597 /// Payload word `at` of the object at `addr`.
2598 pub(crate) fn payload(&self, addr: u64, at: u32) -> u64 {
2599 self.mem.payload(addr, at)
2600 }
2601
2602 /// Writes payload word `at` of the object at `addr`.
2603 pub(crate) fn set_payload(&mut self, addr: u64, at: u32, word: u64) {
2604 self.mem.set_payload(addr, at, word);
2605 }
2606
2607 /// Re-labels the object at `addr`, releasing the `spare` words it gives
2608 /// up. See [`Memory::relabel`].
2609 pub(crate) fn relabel(&mut self, addr: u64, layout: LayoutId, len: u32, spare: u32) {
2610 let payload = self.payload_words(layout, len);
2611 self.mem.relabel(addr, layout, len, payload, spare);
2612 }
2613
2614 /// The `words` payload words of the object at `addr`, from `at`.
2615 ///
2616 /// What a boundary reads when a value is inline in a payload: an array
2617 /// element, a capture, a struct field, the value inside a box. Nothing in
2618 /// ordinary execution calls it — a move inside the machine never leaves
2619 /// the memory.
2620 pub(crate) fn payload_run(&self, addr: u64, at: u32, words: u32) -> Vec<u64> {
2621 self.mem.read_words(self.mem.payload_addr(addr, at), words)
2622 }
2623
2624 /// Writes `words` into the payload of the object at `addr`, from `at`.
2625 pub(crate) fn set_payload_run(&mut self, addr: u64, at: u32, words: &[u64]) {
2626 for (offset, word) in words.iter().enumerate() {
2627 self.mem.set_payload(addr, at + offset as u32, *word);
2628 }
2629 }
2630
2631 /// How many words a value of `layout` occupies, for a caller outside the
2632 /// dispatch loop.
2633 pub(crate) fn words_of(&self, layout: LayoutId) -> u32 {
2634 self.width(layout)
2635 }
2636
2637 /// How many payload words an object of `layout` with header length `len`
2638 /// occupies.
2639 pub(crate) fn payload_words(&self, layout: LayoutId, len: u32) -> u32 {
2640 self.program
2641 .layout(layout)
2642 .payload_words(len, &self.program.layouts)
2643 }
2644
2645 // ---- the scheduler -----------------------------------------------------
2646
2647 /// The scope a `Repr::Scope` word names.
2648 fn scope_at(&self, word: u64, span: Span) -> Result<usize, RuntimeError> {
2649 word.checked_sub(1)
2650 .map(|at| at as usize)
2651 .filter(|at| *at < self.scopes.len())
2652 .ok_or_else(|| no_such_handle("task scope").at(span))
2653 }
2654
2655 /// The task a `Repr::Task` word names.
2656 fn child_at(&self, word: u64, span: Span) -> Result<usize, RuntimeError> {
2657 word.checked_sub(1)
2658 .map(|at| at as usize)
2659 .filter(|at| *at < self.children.len())
2660 .ok_or_else(|| no_such_handle("task").at(span))
2661 }
2662
2663 /// `scope.spawn { ... }`: a thread for the closure, and the handle the
2664 /// scope now owns.
2665 ///
2666 /// This follows `crate::task::spawn_into` step for step, because what a
2667 /// `spawn` decides is a fact about the language rather than about a
2668 /// backend: the scope has to still be open, the run's concurrency limit
2669 /// is charged **before** the task is given an id, an event or a thread,
2670 /// the trace records the spawn before the thread starts so that a task is
2671 /// never seen completing before it was seen spawning, and a place charged
2672 /// for a task that never got a thread goes back.
2673 ///
2674 /// What differs is the two things only this backend can do. The answer's
2675 /// object is allocated here, before the thread exists, so that it is a
2676 /// root of this task from the moment it can hold anything; and the child
2677 /// is handed a [`Memory`] over a stack segment of its own and the run's
2678 /// one heap, which is the whole of issue #240's Q1.
2679 ///
2680 /// It returns once the thread exists and orders nothing else. ADR 0008's
2681 /// amendment refuses a rendezvous here, and so does this.
2682 #[allow(clippy::too_many_arguments)]
2683 fn spawn<'s>(
2684 &mut self,
2685 scope_word: u64,
2686 object: u64,
2687 answer: LayoutId,
2688 budget: &Meter,
2689 span: Span,
2690 threads: &'s Scope<'s, 'a>,
2691 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
2692 ) -> Result<u64, RuntimeError> {
2693 let at = self.scope_at(scope_word, span)?;
2694 if self.scopes[at].closed {
2695 return Err(task::scope_already_left(&self.scopes[at].name, span));
2696 }
2697 if object == 0 {
2698 return Err(null_object().at(span));
2699 }
2700 // Everything above is about the program and is decided the same way
2701 // on both backends. Everything below needs a thread, and
2702 // `task::no_threads_here` says what it means for there not to be one.
2703 if cfg!(target_arch = "wasm32") {
2704 return Err(task::no_threads_here(span));
2705 }
2706
2707 // Charged before this task is given an id, an event or a thread: a
2708 // thread that has started is a resource already taken, which no later
2709 // safepoint could refuse.
2710 if let Some(hosts) = self.hosts {
2711 if let Some(Err(error)) = hosts.with_budget(|held| {
2712 held.charge_task()
2713 .map_err(|stopped| held.to_runtime_error(stopped))
2714 }) {
2715 return Err(error.at(span));
2716 }
2717 }
2718
2719 match self.launch(at, object, answer, budget, span, threads, running) {
2720 Ok(word) => Ok(word),
2721 Err(error) => {
2722 // A task the machine refused is not a task the run holds, so
2723 // the place charged for it above goes back.
2724 if let Some(hosts) = self.hosts {
2725 hosts.with_budget(|held| held.release_task());
2726 }
2727 Err(error)
2728 }
2729 }
2730 }
2731
2732 /// Everything after the concurrency limit has been charged.
2733 ///
2734 /// Split out so that every way of failing after the charge gives the
2735 /// place back, in one place rather than at each way out.
2736 #[allow(clippy::too_many_arguments)]
2737 fn launch<'s>(
2738 &mut self,
2739 at: usize,
2740 object: u64,
2741 answer: LayoutId,
2742 budget: &Meter,
2743 span: Span,
2744 threads: &'s Scope<'s, 'a>,
2745 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
2746 ) -> Result<u64, RuntimeError> {
2747 // The segment first, because taking one can wait: a task joining a
2748 // run whose collection has already begun waits it out, and a task
2749 // that waited without publishing its roots would be a task the
2750 // collector waits for while it waits for the collector. So this task
2751 // parks for the length of the wait, exactly as it does around a host
2752 // call and around a join.
2753 let segment = {
2754 let parked = self.mem.blocking(&Live(self));
2755 let taken = self.mem.for_task();
2756 drop(parked);
2757 taken
2758 };
2759 let segment = segment
2760 .map_err(|NoSegment| no_segment_left().at(span).with_rule(crate::budget::RULE))?;
2761
2762 // Then the answer's home, allocated before the thread exists so that
2763 // it is a root of this task from the moment it can hold anything. The
2764 // closure is in a `Repr::Ref` slot of a live frame, so a collection
2765 // here finds it — which is the one thing this allocation could
2766 // otherwise have taken away.
2767 let width = self.width(answer);
2768 let home = self.allocate(self.boxed_layout(), width as i64)?;
2769 self.mem.set_payload(home, 0, answer.0 as u64);
2770
2771 let id = match self.runtime {
2772 Some(runtime) => runtime.next_task_id(),
2773 None => {
2774 self.next_task += 1;
2775 self.next_task - 1
2776 }
2777 };
2778 let scope = self.scopes[at].name.clone();
2779 let position = self.scopes[at].tasks.len() + 1;
2780 // Traced before the thread starts, so a task is never seen completing
2781 // before it was seen spawning.
2782 if let Some(runtime) = self.runtime {
2783 runtime.trace(TraceEvent::TaskSpawned {
2784 id,
2785 parent: (self.task != ENTRY_TASK).then_some(self.task),
2786 scope: scope.to_string(),
2787 });
2788 }
2789
2790 let cancellation = Cancellation::new();
2791 let program = self.program;
2792 let hosts = self.hosts;
2793 let runtime = self.runtime;
2794 let resources = Arc::clone(&self.resources);
2795 let meter = budget.clone();
2796 let flag = cancellation.clone();
2797 // The same debugger, asked from the child's thread. That is what
2798 // `Send + Sync` on the trait buys, and it is why a debugger sees a
2799 // spawned task's instructions rather than only the entry's.
2800 let watcher = self.debugger;
2801 // And the same instructions, handed over rather than encoded again:
2802 // one program is encoded once per run, however many threads run it.
2803 let form = self.code()?;
2804 // And the same literal addresses, for the reason ADR 0045 gives:
2805 // every task of a run addresses the objects the entry placed, so
2806 // this is a clone of the `Arc` rather than a second placement.
2807 let literals = self.literals()?;
2808 // And the same widths, for the reason the field gives: a table
2809 // derived from a program the whole run shares is one table.
2810 let widths = Arc::clone(&self.widths);
2811 let handle = threads.spawn(move || {
2812 run_task(
2813 program, hosts, runtime, resources, segment, meter, flag, id, object, home, span,
2814 watcher, form, literals, widths,
2815 )
2816 });
2817
2818 let index = self.children.len();
2819 self.children.push(Child {
2820 id,
2821 position,
2822 scope,
2823 cancellation,
2824 closure: object,
2825 answer: home,
2826 layout: answer,
2827 state: ChildState::Running,
2828 });
2829 running.push(Some(handle));
2830 self.scopes[at].tasks.push(index);
2831 // One past the index, because a zeroed slot has to mean no task.
2832 Ok(index as u64 + 1)
2833 }
2834
2835 /// The handle a call to an `async fn` answers, around words the call has
2836 /// already produced.
2837 ///
2838 /// [`crate::task::Task::settled`] is the oracle and this is the same
2839 /// thing in a table: a task with no thread, whose value is known before
2840 /// the handle exists. Everything downstream then works without being
2841 /// told which kind it has — [`Machine::join`] returns at once because the
2842 /// state is not `Running`, [`Machine::settle`] reads the answer object
2843 /// the way it reads a spawned task's, and an `Inst::Cancel` does nothing
2844 /// to a task that is not running, exactly as `Task::cancel` does nothing.
2845 ///
2846 /// Three things it deliberately does not do, each because the oracle does
2847 /// not do it either. It takes **no place under the concurrency limit**:
2848 /// nothing was started, and a limit on how many tasks run at once is not
2849 /// a limit on how many `async fn` calls a program makes. It is **not put
2850 /// in any scope**, so leaving a scope neither waits for it nor cancels
2851 /// it — there is nothing left to wait for. And it is **not traced**: it
2852 /// is `id` zero, the identity `crate::task::Task` gives a handle that
2853 /// "never appears in a trace because it never ran as a task".
2854 ///
2855 /// What it costs is one table entry and one object, kept for the rest of
2856 /// the run. That is the price of a `Repr::Task` word being a name rather
2857 /// than an address: the collector reads a static per-slot map and never
2858 /// inspects a word, so the answer object has to be reachable from
2859 /// somewhere the collector walks, and the table is that somewhere. The
2860 /// oracle's `Rc<Task>` is freed when the last handle goes; a handle here
2861 /// can be inside a `Vector<Task<T>>` or a struct field, and no static map
2862 /// can say when the last one died.
2863 fn settled(
2864 &mut self,
2865 words: &[u64],
2866 answer: LayoutId,
2867 running: &mut Vec<Option<ScopedJoinHandle<'_, Outcome>>>,
2868 ) -> Result<u64, RuntimeError> {
2869 // The same object a spawned task's answer goes into, so that
2870 // `Machine::settle` reads one shape rather than two.
2871 let home = self.allocate(self.boxed_layout(), words.len() as i64)?;
2872 self.mem.set_payload(home, 0, answer.0 as u64);
2873 for (at, word) in words.iter().enumerate() {
2874 self.mem.set_payload(home, 1 + at as u32, *word);
2875 }
2876 let index = self.children.len();
2877 self.children.push(Child {
2878 // Position zero and this name are what `crate::task::describe`
2879 // renders as *this task*: a handle with no place in a spawn
2880 // order, because there was no spawn.
2881 id: 0,
2882 position: 0,
2883 scope: Arc::from("this call"),
2884 cancellation: Cancellation::new(),
2885 closure: 0,
2886 answer: home,
2887 layout: answer,
2888 state: ChildState::Settled,
2889 });
2890 // The two lists are one list at two indices, so a task with no thread
2891 // still takes its place in both.
2892 running.push(None);
2893 Ok(index as u64 + 1)
2894 }
2895
2896 /// `await task`: waits for the thread and answers the words its body
2897 /// produced.
2898 ///
2899 /// A body runs at most once and is waited for at most once, so awaiting
2900 /// the same handle twice answers the same value and repeats no effect —
2901 /// which falls out of the state rather than being arranged, exactly as it
2902 /// does in `crate::task::settle`.
2903 fn settle(
2904 &mut self,
2905 word: u64,
2906 answer: LayoutId,
2907 running: &mut [Option<ScopedJoinHandle<'_, Outcome>>],
2908 span: Span,
2909 ) -> Result<Vec<u64>, RuntimeError> {
2910 let at = self.child_at(word, span)?;
2911 // A task blocked on an `await` is standing where a safepoint would
2912 // be, so it is owed the answer one gives: a cancelled task does not
2913 // wait for a sibling it will never read.
2914 stopped_here(self.cancellation.as_ref(), &[], span)?;
2915 self.join(at, running);
2916 match &self.children[at].state {
2917 ChildState::Settled => {
2918 let width = self.width(answer);
2919 let home = self.children[at].answer;
2920 Ok(self.mem.read_words(self.mem.payload_addr(home, 1), width))
2921 }
2922 ChildState::Failed(error) => Err(error.clone()),
2923 ChildState::Cancelled => Err(task::awaiting_a_cancelled(
2924 &self.children[at].describe(),
2925 span,
2926 )),
2927 ChildState::Running => {
2928 unreachable!("joining a task leaves it settled, failed, or cancelled")
2929 }
2930 }
2931 }
2932
2933 /// Waits for one task's thread and records what it produced.
2934 ///
2935 /// This is `crate::task::join` and `crate::task::Task::join` together,
2936 /// and the three things they decide are decided here in the same order: a
2937 /// task that stopped after its own cancellation was requested is
2938 /// *cancelled* rather than failed, because that is the stop the program
2939 /// asked for; the place it held under the concurrency limit goes back at
2940 /// the join rather than on the task's own thread, so that what a `spawn`
2941 /// is refused for does not depend on how quickly a sibling finished; and
2942 /// `TaskCancelled` is traced here, because this is the only place that
2943 /// knows a cancellation stopped work rather than arriving after it.
2944 fn join(&mut self, at: usize, running: &mut [Option<ScopedJoinHandle<'_, Outcome>>]) {
2945 if !matches!(self.children[at].state, ChildState::Running) {
2946 return;
2947 }
2948 let Some(handle) = running[at].take() else {
2949 return;
2950 };
2951 let outcome = {
2952 // Published for the whole wait. A task waiting for a sibling
2953 // cannot reach a safepoint of its own, and a collector that
2954 // waited for it would be waiting for a task that is waiting for a
2955 // task that is waiting for the collector.
2956 let parked = self.mem.blocking(&Live(self));
2957 let outcome = handle.join();
2958 drop(parked);
2959 outcome
2960 };
2961 let outcome = match outcome {
2962 Ok(outcome) => outcome,
2963 // A panic is a broken invariant in the task's own thread. The
2964 // message has already reached stderr; what this task needs is an
2965 // error rather than a value that never arrived.
2966 Err(_) => Err(task::broken_invariant(&self.children[at].describe())),
2967 };
2968 let cancelled = self.children[at].cancellation.is_cancelled();
2969 self.children[at].state = match outcome {
2970 Ok(()) => ChildState::Settled,
2971 Err(_) if cancelled => ChildState::Cancelled,
2972 Err(error) => ChildState::Failed(error),
2973 };
2974 // The body is over, so the environment it was entered through is no
2975 // longer anything's to keep: the captures it held were copied into
2976 // the child's frame before its first instruction.
2977 self.children[at].closure = 0;
2978 if let Some(hosts) = self.hosts {
2979 hosts.with_budget(|held| held.release_task());
2980 }
2981 if matches!(self.children[at].state, ChildState::Cancelled) {
2982 if let Some(runtime) = self.runtime {
2983 runtime.trace(TraceEvent::TaskCancelled {
2984 id: self.children[at].id,
2985 });
2986 }
2987 }
2988 }
2989
2990 /// Waits for every child of a scope the body reached the end of **that
2991 /// the body did not await**, and answers the first that failed in a way
2992 /// the enclosing function has to pass on.
2993 ///
2994 /// `crate::task::wait_for_children` is the oracle and this is its
2995 /// translation. Waiting is in spawn order, which is an order of
2996 /// *observation* only — the tasks ran at the same time on threads of
2997 /// their own — and a task the program itself cancelled is neither a
2998 /// failure nor a success, because the program asked for that stop.
2999 ///
3000 /// # Why an awaited child is skipped
3001 ///
3002 /// `if !task.is_running() { continue }` is the oracle's first line and it
3003 /// is a decision rather than an optimisation: a child the body awaited
3004 /// has already handed its value to the program, and the program has
3005 /// already done whatever it does with one. Reporting it again here would
3006 /// overwrite the answer the body computed *from* that failure with the
3007 /// failure itself, so
3008 ///
3009 /// ```cove
3010 /// let answer = task.await()
3011 /// match answer { Ok(n) => n, Err(_) => fallback() }
3012 /// ```
3013 ///
3014 /// could not recover from a failed child at all — leaving the scope would
3015 /// throw the recovery away. What is left to wait for is what nothing has
3016 /// read, which is the case the rule exists for: a failure sitting unread
3017 /// in a handle nobody awaited reaches the caller rather than vanishing.
3018 ///
3019 /// A child is "awaited" here for the same reason it is there: joining is
3020 /// what settles a child's state, and [`Machine::settle`] joins. So a
3021 /// state that is no longer [`ChildState::Running`] is exactly a child
3022 /// something has already waited for.
3023 ///
3024 /// `Ok(Some(child))` is a child whose value was `Err(...)`; `Err` is a
3025 /// child that raised, which propagates as itself. Either way the tasks
3026 /// still running are cancelled and waited for before this answers.
3027 fn leave_scope(
3028 &mut self,
3029 word: u64,
3030 running: &mut [Option<ScopedJoinHandle<'_, Outcome>>],
3031 span: Span,
3032 ) -> Result<Option<usize>, RuntimeError> {
3033 let at = self.scope_at(word, span)?;
3034 let mut index = 0;
3035 let mut failure = None;
3036 let mut raised = None;
3037 // Read by index rather than from a snapshot, so a scope that grew
3038 // while it was being left is still waited for to the end.
3039 while let Some(&child) = self.scopes[at].tasks.get(index) {
3040 index += 1;
3041 if !matches!(self.children[child].state, ChildState::Running) {
3042 continue;
3043 }
3044 self.join(child, running);
3045 match &self.children[child].state {
3046 ChildState::Settled => {
3047 if self.child_failed(child) {
3048 failure = Some(child);
3049 break;
3050 }
3051 }
3052 ChildState::Failed(error) => {
3053 raised = Some(error.clone());
3054 break;
3055 }
3056 ChildState::Cancelled | ChildState::Running => {}
3057 }
3058 }
3059 if failure.is_some() || raised.is_some() {
3060 self.cancel_scope(at, running);
3061 }
3062 self.scopes[at].closed = true;
3063 match raised {
3064 Some(error) => Err(error),
3065 None => Ok(failure),
3066 }
3067 }
3068
3069 /// Cancels every running child of a scope and waits for it to stop.
3070 ///
3071 /// Every child is asked first and waited for afterwards, so they stop at
3072 /// the same time rather than one after another —
3073 /// `crate::task::cancel_children`, in the same two passes.
3074 fn cancel_scope(&mut self, at: usize, running: &mut [Option<ScopedJoinHandle<'_, Outcome>>]) {
3075 for &child in &self.scopes[at].tasks {
3076 if matches!(self.children[child].state, ChildState::Running) {
3077 self.children[child].cancellation.cancel();
3078 }
3079 }
3080 let mut index = 0;
3081 while let Some(&child) = self.scopes[at].tasks.get(index) {
3082 index += 1;
3083 self.join(child, running);
3084 }
3085 self.scopes[at].closed = true;
3086 }
3087
3088 /// Whether any task this machine spawned has not been joined.
3089 ///
3090 /// Asked only by a debug assertion, and what it is asserting is the
3091 /// reason the answer's words are safe to carry out of the frame that
3092 /// produced them: [`Machine::stop_all`] can block, and a task that blocks
3093 /// publishes its roots — which no longer name a popped frame. On the path
3094 /// that answers words there is nothing to block for, because every scope
3095 /// was left where it was written and leaving one joins its children.
3096 fn anything_running(&self) -> bool {
3097 self.children
3098 .iter()
3099 .any(|child| matches!(child.state, ChildState::Running))
3100 }
3101
3102 /// Cancels and joins every task still running, whatever scope it is in.
3103 ///
3104 /// The unwind path, and the one exit a scope has that the lowering cannot
3105 /// write: a runtime error is not a jump, so no `ScopeCancel` stands
3106 /// between it and the end of the run. Without this the thread scope would
3107 /// wait for a task nothing had asked to stop.
3108 ///
3109 /// On the ordinary path it has nothing to do, because every scope was
3110 /// left where it was written.
3111 fn stop_all(&mut self, running: &mut [Option<ScopedJoinHandle<'_, Outcome>>]) {
3112 for child in &self.children {
3113 if matches!(child.state, ChildState::Running) {
3114 child.cancellation.cancel();
3115 }
3116 }
3117 for at in 0..self.children.len() {
3118 self.join(at, running);
3119 }
3120 }
3121
3122 /// Gives back every cell this task took above `mark`, innermost first.
3123 ///
3124 /// The unwind path for a `lock`, and the exact analogue of
3125 /// [`Machine::stop_all`]: a runtime error is not a jump, so no
3126 /// `Inst::SharedUnlock` stands between it and the end of the run, and a
3127 /// cell nobody gave back is a cell no task can ever take. On the ordinary
3128 /// path it has nothing to do, because every lock region was left where it
3129 /// was written.
3130 ///
3131 /// Innermost first, because that is the order the regions would have
3132 /// ended in.
3133 fn give_cells_back(&mut self, mark: usize) {
3134 while self.held.len() > mark {
3135 let addr = self.held.pop().expect("the length is above the mark");
3136 cell::unlock(&self.mem, addr);
3137 }
3138 }
3139
3140 /// Whether a settled child's value was `Err(...)`.
3141 ///
3142 /// The answer object holds the layout of what it carries in its first
3143 /// payload word and the value's words after it, so the discriminant is
3144 /// the second. A child whose answer is not an enum with an `Err` case did
3145 /// not fail this way and cannot: `crate::task::failure_of` asks the same
3146 /// question of a materialised value and answers `None` for the same
3147 /// values.
3148 fn child_failed(&self, at: usize) -> bool {
3149 self.err_part(at)
3150 .is_some_and(|(index, _, _)| self.mem.payload(self.children[at].answer, 1) == index)
3151 }
3152
3153 /// Where a child's `Err` payload is in its answer object: the case index,
3154 /// the payload word it begins at, and its layout.
3155 fn err_part(&self, at: usize) -> Option<(u64, u32, LayoutId)> {
3156 let layout = self.program.layout(self.children[at].layout);
3157 let Shape::Enum { cases, .. } = &layout.shape else {
3158 return None;
3159 };
3160 let index = cases.iter().position(|case| &*case.name == "Err")?;
3161 let part = cases[index].parts.first()?;
3162 // Word 0 of the object is the held layout and word 1 is the value's
3163 // discriminant, so the payload region begins at word 2.
3164 Some((index as u64, 2 + part.at, part.layout))
3165 }
3166
3167 /// Copies a failing child's `Err` payload into the location the enclosing
3168 /// function will wrap and return.
3169 ///
3170 /// The two layouts are held to being one. They are not the same fact —
3171 /// one is what the child answered and the other is what the function the
3172 /// scope was written in fails with — and the checker never had to unify
3173 /// them, because the oracle wraps whatever it finds in a `Value::err` and
3174 /// asks nothing. Here a run of words copied at the wrong width is the one
3175 /// fault this backend must not have quietly, so a disagreement is
3176 /// reported rather than truncated.
3177 fn write_child_error(
3178 &mut self,
3179 at: usize,
3180 into: u64,
3181 layout: LayoutId,
3182 ) -> Result<(), RuntimeError> {
3183 let Some((_, word, held)) = self.err_part(at) else {
3184 return Err(RuntimeError::new(
3185 "this task failed with a value that is not an error the enclosing function can \
3186 answer with",
3187 ));
3188 };
3189 if held != layout {
3190 let found = self.program.layout(held).name.clone();
3191 let wanted = self.program.layout(layout).name.clone();
3192 return Err(RuntimeError::new(format!(
3193 "this task failed with a `{found}`, and the function its scope was written in \
3194 answers a `{wanted}`"
3195 )));
3196 }
3197 let width = self.width(layout);
3198 let from = self.mem.payload_addr(self.children[at].answer, word);
3199 self.mem.copy_words(into, from, width);
3200 Ok(())
3201 }
3202
3203 /// Publishes this task's roots and stands at a safepoint until the
3204 /// answer is dropped.
3205 ///
3206 /// [`crate::vm::mem::Parked`] rather than the borrowed guard, because
3207 /// the one caller that needs it is [`Back`], which holds this machine
3208 /// mutably and so cannot also hold a guard borrowing its memory.
3209 fn park(&self) -> Parked {
3210 self.mem.park(&Live(self))
3211 }
3212
3213 /// Runs a Cove callable from outside the dispatch loop, which is what a
3214 /// host callback needs and the only thing that does.
3215 ///
3216 /// # The convention
3217 ///
3218 /// **The call opens its frame at the top of the stack region as it
3219 /// stands, and leaves it exactly as it found it.** The callee's frame
3220 /// begins where the deepest live one ends, which is what
3221 /// [`Memory::push_frame`] answers and what a [`cove_ir::Inst::Call`] would
3222 /// have
3223 /// got; the arguments are written into it as the parameters' words, the
3224 /// captures follow them out of the environment object, and the frame is
3225 /// popped by the return that answers. The frame stack grows above the
3226 /// frame the interrupted instruction belongs to and comes back down to
3227 /// it, which is what `floor` means in [`encoded::dispatch`].
3228 ///
3229 /// That the outer frames are *left* rather than unwound is the whole
3230 /// reason this is another turn of the loop rather than a jump: the
3231 /// instruction that made the host call has not finished, and its frame's
3232 /// slots are live — including, in every case that matters, the slot
3233 /// holding the closure that is being called.
3234 ///
3235 /// # What a failure leaves
3236 ///
3237 /// Nothing. A host may catch what a callback failed with and carry on —
3238 /// `clock.timeout` is written to — so the frames, the stack region, the
3239 /// task scopes the callback opened and the tasks it spawned are all put
3240 /// back the way they were found. The outer run has no unwinding because
3241 /// an abandoned frame's slots stay on the stack until the run ends, which
3242 /// is sound only because the run is ending, and that reasoning does not
3243 /// reach here.
3244 ///
3245 /// # What is still accounted
3246 ///
3247 /// Everything the loop accounts, because it is the loop. Fuel is charged
3248 /// every [`SAFEPOINT_STRIDE`] instructions, a frame that would leave this
3249 /// task's stack segment is a stack overflow, and every safepoint the
3250 /// callee reaches asks what a safepoint asks — including
3251 /// [`Machine::stops`], which [`Reentry::call_until`] pushes onto.
3252 ///
3253 /// [`Reentry::call_until`]: crate::host::Reentry::call_until
3254 fn call_from_host<'s>(
3255 &mut self,
3256 callee: &Value,
3257 args: Vec<Value>,
3258 budget: &Meter,
3259 span: Span,
3260 threads: &'s Scope<'s, 'a>,
3261 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
3262 ) -> Result<Value, RuntimeError> {
3263 // Raised for as long as the callback runs and dropped when it
3264 // returns, so a host that runs its callback twice pays for one level
3265 // twice over rather than for two levels at once. What is bounded is
3266 // how many are stacked on this thread, because that is what is
3267 // spending the native stack.
3268 if self.reentry_depth >= crate::interp::MAX_REENTRY_DEPTH {
3269 return Err(crate::interp::reentry_too_deep(span));
3270 }
3271 let (target_id, object) =
3272 boundary::callback_target(self, callee).map_err(|error| error.at(span))?;
3273 let program = self.program;
3274 let target = program.function(target_id);
3275 if args.len() != target.params.len() {
3276 return Err(wrong_arity(target.qualified(), target.params.len(), args.len()).at(span));
3277 }
3278
3279 // A callback's own frame is an ordinary Cove frame and counts as
3280 // one, which is the answer the oracle gives: the one frame a reentry
3281 // adds is enough to cross a limit the same recursion fits under when
3282 // it is called directly.
3283 self.admit_frame(budget, span)?;
3284
3285 let floor = self.frames.len();
3286 let children = self.children.len();
3287 let scopes = self.scopes.len();
3288 let cells = self.held.len();
3289 let base = self
3290 .mem
3291 .push_frame(target.frame_size())
3292 .map_err(|Overflow| self.too_deep(span))?;
3293 // The frame is on the stack *before* an argument is converted, and
3294 // that is what roots the ones already converted: a `Repr::Ref` slot
3295 // of a live frame is a root, `boundary::from_value` allocates, and a
3296 // value built into a Rust vector first would have been named by
3297 // nothing the collector walks while the next one was built.
3298 self.frames.push(Frame {
3299 function: target_id,
3300 base,
3301 pc: 0,
3302 dst: 0,
3303 });
3304 let answer = self.enter_callback(
3305 object, target_id, args, base, floor, budget, threads, running, span,
3306 );
3307 match answer {
3308 Ok(words) => {
3309 // Nothing between here and the conversion allocates, so the
3310 // objects these words name are still the ones they named when
3311 // the frame that produced them was popped — the same reason
3312 // `Machine::run` may carry an answer out of a frame.
3313 let returns = self.program.function(target_id).returns;
3314 let answer =
3315 boundary::to_value(self, returns, &words).map_err(|error| error.at(span))?;
3316 // The value has left, and its family goes with it: a host
3317 // that wraps this answer in a result it declared `Any` hands
3318 // it straight back, and the box that is built for it there is
3319 // tagged with what it was here. See
3320 // [`Machine::callback_answer`].
3321 self.callback_answer = Some(returns);
3322 Ok(answer)
3323 }
3324 Err(error) => {
3325 self.unwind_to(floor, children, scopes, cells, base, running);
3326 Err(error)
3327 }
3328 }
3329 }
3330
3331 /// The callback's frame, filled and run. See [`Machine::call_from_host`].
3332 #[allow(clippy::too_many_arguments)]
3333 fn enter_callback<'s>(
3334 &mut self,
3335 object: u64,
3336 target_id: FunctionId,
3337 args: Vec<Value>,
3338 base: u64,
3339 floor: usize,
3340 budget: &Meter,
3341 threads: &'s Scope<'s, 'a>,
3342 running: &mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
3343 span: Span,
3344 ) -> Result<Vec<u64>, RuntimeError> {
3345 let program = self.program;
3346 let mut at = 0;
3347 for (value, layout) in args.iter().zip(&program.function(target_id).params) {
3348 let layout = *layout;
3349 let words = boundary::from_value(self, layout, value).map_err(|e| e.at(span))?;
3350 self.mem.write_words(base + at as u64, &words);
3351 at += self.width(layout);
3352 }
3353 // The captures, out of the environment object and into the slots
3354 // `Function::captures` names — the same read `Inst::CallClosure`
3355 // makes, of the same object, at the same widths.
3356 let mut held = 1;
3357 for capture in &program.function(target_id).captures {
3358 let width = self.width(capture.layout);
3359 self.mem.copy_words(
3360 base + capture.slot as u64,
3361 self.mem.payload_addr(object, held),
3362 width,
3363 );
3364 held += width;
3365 }
3366 // The one other place a dispatch loop is entered. `Machine::run`'s
3367 // way back in is `drive`, and this is the other; both read the same
3368 // encoding, because a host that calls back into a run must find
3369 // itself in the run it called out of.
3370 let code = self.code()?;
3371 self.reentry_depth += 1;
3372 let answer = encoded::dispatch(self, &code, budget, threads, running, floor);
3373 self.reentry_depth -= 1;
3374 answer
3375 }
3376
3377 /// Puts the frames, the stack region, the scopes and the tasks back the
3378 /// way a failed callback found them.
3379 ///
3380 /// The children first and while their frames still stand: cancelling and
3381 /// joining blocks, a task that blocks publishes its roots, and roots that
3382 /// named frames this had already truncated would be addresses of words
3383 /// nothing owns.
3384 fn unwind_to(
3385 &mut self,
3386 frames: usize,
3387 children: usize,
3388 scopes: usize,
3389 cells: usize,
3390 base: u64,
3391 running: &mut [Option<ScopedJoinHandle<'_, Outcome>>],
3392 ) {
3393 // Before anything is joined or truncated: a host that catches what a
3394 // callback failed with carries on, and a cell the callback took and
3395 // this did not give back would be held for the rest of the run by a
3396 // frame that no longer exists.
3397 self.give_cells_back(cells);
3398 for child in &self.children[children..] {
3399 if matches!(child.state, ChildState::Running) {
3400 child.cancellation.cancel();
3401 }
3402 }
3403 for at in children..self.children.len() {
3404 self.join(at, running);
3405 }
3406 // A scope the callback opened and did not leave is closed here rather
3407 // than at the end of the run, for the reason its children were joined
3408 // here: its threads would otherwise outlive every frame that could
3409 // name them.
3410 for scope in &mut self.scopes[scopes..] {
3411 scope.closed = true;
3412 }
3413 self.frames.truncate(frames);
3414 self.mem.pop_frame(base);
3415 }
3416
3417 /// Whether `rooted` names an object of this run's memory.
3418 pub(crate) fn holds(&self, rooted: &Rooted) -> bool {
3419 self.mem.is_mine(rooted)
3420 }
3421
3422 /// Makes the object at `addr` a root for as long as the answer lives.
3423 ///
3424 /// The one thing a `Value` crossing out of here can need that a frame
3425 /// cannot give it. See [`crate::vm::mem::Rooted`].
3426 pub(crate) fn pin(&self, addr: u64) -> Rooted {
3427 self.mem.pin(addr)
3428 }
3429
3430 /// A new object of `layout` with header length `len`, collecting once if
3431 /// the first attempt does not fit.
3432 /// A new object of `layout` with header length `len`, collecting once if
3433 /// the first attempt does not fit.
3434 ///
3435 /// The payload is zeroed, so a reference field reads as null until it is
3436 /// written — which is what makes a half-built object safe to collect
3437 /// *through* once [`Machine::push_temp`] has made it safe to collect
3438 /// *around*.
3439 pub(crate) fn new_object(&mut self, layout: LayoutId, len: u32) -> Result<u64, RuntimeError> {
3440 self.allocate(layout, len as i64)
3441 }
3442}
3443
3444/// The way back a host is offered while the linear-memory backend runs.
3445///
3446/// A host that was handed a Cove callback calls it through this. The callback
3447/// is an ordinary frame of this machine — [`Machine::call_from_host`] pushes
3448/// it exactly as [`cove_ir::Inst::CallClosure`] would — and the difference from a call
3449/// the loop made is only in who is waiting for it: a Rust frame rather than
3450/// an instruction. So it holds the machine mutably, which is what makes the
3451/// rest of [`Reentry`]'s contract true rather than merely stated. There can
3452/// be one of these per host call and it cannot be moved to another thread, so
3453/// a host cannot use its way back concurrently; it borrows the machine, so a
3454/// host cannot keep it; and every level of nesting is another one further
3455/// down the same native stack, which is what
3456/// [`crate::interp::MAX_REENTRY_DEPTH`] counts.
3457///
3458/// # What re-entry costs here, and why the bound is the oracle's
3459///
3460/// `docs/LINEAR_VM.md` says **a builtin never calls back into Cove**, and the
3461/// reason it gives is the property the loop exists to have: how deep a Cove
3462/// program may nest is decided by the reserved stack region and not by how
3463/// large a Rust frame the interpreter compiled to. A builtin that ran a
3464/// closure itself would put a Rust frame under every Cove frame the closure
3465/// made, so a `map` over a `map` over a `map` would be three Rust frames deep
3466/// before the program did anything — and a builtin has an alternative, which
3467/// is to be lowered to a loop in the IR.
3468///
3469/// A host callback is the other case, and it differs in both halves. The host
3470/// is *already* a Rust frame: it was reached through
3471/// `HostRegistry::dispatch`, which the machine called, and nothing about
3472/// lowering anything would remove it. And the reentry is the language's own —
3473/// ADR 0013 gives the host the resource and the `Reentry` contract gives it
3474/// the callback — so there is no loop to lower it to. `clock.timeout(500ms)
3475/// { .. }` cannot become a `CallClosure` in the caller's body, because what
3476/// decides whether the body runs at all, and what stops it, is on the host's
3477/// side.
3478///
3479/// So the rule holds where it was aimed and does not reach this. What it
3480/// leaves is that one thing is no longer bounded by the stack region: between
3481/// the callback's frame and the frame that called the host sit
3482/// `HostRegistry::dispatch`, however much native stack the host itself uses,
3483/// and one more turn of [`encoded::dispatch`]. That is exactly the situation
3484/// [`crate::interp::MAX_REENTRY_DEPTH`] was calibrated for, in the oracle,
3485/// where its documentation says the depth limit's promise *"holds for Cove
3486/// calling Cove and stops holding exactly where a third party controls the
3487/// multiplier"*. The sentence is true of this backend word for word — it is
3488/// true of it *more* narrowly, because Cove calling Cove costs no native
3489/// stack here at all — so the bound is the same bound and the refusal is the
3490/// oracle's own, from [`crate::interp::reentry_too_deep`].
3491///
3492/// [`Reentry`]: crate::host::Reentry
3493struct Back<'m, 's, 'a> {
3494 machine: &'m mut Machine<'a>,
3495 budget: &'m Meter,
3496 /// Where the host call that is running this was written, so a failure
3497 /// inside it points at the call rather than at nothing.
3498 span: Span,
3499 /// The thread scope a `spawn` inside a callback starts its children in.
3500 ///
3501 /// The *caller's*, not one of this call's own. A callback is a frame of
3502 /// this machine and its tasks are this machine's tasks: they go into
3503 /// [`Machine::children`] at indices `running` is parallel to, and a scope
3504 /// of this call's own would have made those two disagree — a task the
3505 /// outer level spawned would be at an index a nested `running` had no
3506 /// handle at. One scope per task, which is what [`Machine::drive`] opens,
3507 /// is also what bounds every thread of the task to the task.
3508 threads: &'s Scope<'s, 'a>,
3509 running: &'m mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
3510 /// The safepoint the calling task stands at while the host runs.
3511 ///
3512 /// Taken here and dropped for exactly as long as a callback runs. A task
3513 /// inside a host call is not running Cove, so the roots it published stay
3514 /// true and a collection need not wait for it; a task running a callback
3515 /// *is* running Cove, its frames change between two instructions, and a
3516 /// snapshot left standing would be telling the collector to trace a frame
3517 /// that has moved.
3518 parked: Option<Parked>,
3519}
3520
3521impl<'m, 's, 'a> Back<'m, 's, 'a> {
3522 /// The way back for one host call, with the calling task parked.
3523 fn parked(
3524 machine: &'m mut Machine<'a>,
3525 budget: &'m Meter,
3526 span: Span,
3527 threads: &'s Scope<'s, 'a>,
3528 running: &'m mut Vec<Option<ScopedJoinHandle<'s, Outcome>>>,
3529 ) -> Back<'m, 's, 'a> {
3530 let parked = machine.park();
3531 // This call has run no callback yet, so nothing it answers may be
3532 // tagged with the family an earlier one left behind.
3533 machine.callback_answer = None;
3534 Back {
3535 machine,
3536 budget,
3537 span,
3538 threads,
3539 running,
3540 parked: Some(parked),
3541 }
3542 }
3543
3544 /// Runs `callee`, off the safepoint and back onto it.
3545 fn run(&mut self, callee: &Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
3546 drop(self.parked.take());
3547 let answer = self.machine.call_from_host(
3548 callee,
3549 args,
3550 self.budget,
3551 self.span,
3552 self.threads,
3553 self.running,
3554 );
3555 // Whatever the callback did, this task is inside a host call again
3556 // and the host may go on to wait, to call again, or to answer.
3557 self.parked = Some(self.machine.park());
3558 answer
3559 }
3560}
3561
3562impl Reentry for Back<'_, '_, '_> {
3563 fn call(&mut self, callee: &Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
3564 self.run(callee, args)
3565 }
3566
3567 /// The same call, with `stop` added to what its safepoints stop on.
3568 ///
3569 /// The flag bounds this call *and everything inside it*, which is why it
3570 /// stands on the machine rather than being handed to the frame: a further
3571 /// host call the body makes, and any callback that host runs in turn, are
3572 /// reached through the same [`Machine::stops`].
3573 fn call_until(
3574 &mut self,
3575 callee: &Value,
3576 args: Vec<Value>,
3577 stop: &Cancellation,
3578 ) -> Result<Value, RuntimeError> {
3579 self.machine.stops.push(stop.clone());
3580 let result = self.run(callee, args);
3581 self.machine.stops.pop();
3582 result
3583 }
3584
3585 /// Everything a safepoint would stop on, asked from outside the loop.
3586 ///
3587 /// A host that is waiting is standing where a safepoint would be, so it
3588 /// is owed the same answer one gets: the calling task's own flag, the
3589 /// flags of the bounded calls this thread is inside, and the run's
3590 /// cancellation. The middle one is what tells a host blocked inside a
3591 /// `clock.timeout` body that something is wrong, and it could not be
3592 /// answered until a callback could run here at all.
3593 fn is_cancelled(&self) -> bool {
3594 self.machine
3595 .cancellation
3596 .as_ref()
3597 .is_some_and(Cancellation::is_cancelled)
3598 || self.machine.stops.iter().any(Cancellation::is_cancelled)
3599 || self.budget.is_cancelled()
3600 }
3601
3602 fn time_left(&self) -> Option<Duration> {
3603 self.budget
3604 .limits()
3605 .deadline
3606 .map(|deadline| deadline.saturating_sub(self.budget.elapsed()))
3607 }
3608
3609 /// The task whose stack this call is standing on, which is the task the
3610 /// boundary records the call against.
3611 fn task(&self) -> u64 {
3612 self.machine.task
3613 }
3614}
3615
3616/// One spawned task's thread, from the closure in to the answer written.
3617///
3618/// `crate::interp::run_task` is the oracle's, and the shape is the same: an
3619/// evaluator of the receiving task's own, the body, and then the trace event
3620/// a finished task writes. What is not here is the conversion. ADR 0008 says
3621/// *"the runtime's `Rc`-based value representation is not `Send`, so the
3622/// values that cross must be converted at the boundary"* — and in this model
3623/// there is nothing to convert, because a crossing value is a run of words in
3624/// a heap both tasks already address. The closure crossed as its address, and
3625/// the answer goes back into an object the parent allocated.
3626///
3627/// A task stopped by its own cancellation did not run to completion, so it is
3628/// **not** traced as completed here; it is traced as cancelled by whoever
3629/// waits for it, which is the only place that knows it stopped rather than
3630/// finished. That is `crate::task::finished`'s rule, kept.
3631#[allow(clippy::too_many_arguments)]
3632fn run_task(
3633 program: &Program,
3634 hosts: Option<&HostRegistry>,
3635 runtime: Option<&Runtime>,
3636 resources: Arc<Mutex<Vec<Arc<ResourceHandle>>>>,
3637 segment: Memory,
3638 budget: Meter,
3639 cancellation: Cancellation,
3640 id: u64,
3641 closure: u64,
3642 answer: u64,
3643 span: Span,
3644 debugger: Option<&(dyn Debugger + Send + Sync)>,
3645 encoded: Arc<cove_ir::bytecode::Encoded>,
3646 literal_addrs: Arc<[u64]>,
3647 widths: Arc<[u32]>,
3648) -> Outcome {
3649 let mut machine = Machine::for_task(
3650 program,
3651 hosts,
3652 runtime,
3653 resources,
3654 segment,
3655 cancellation.clone(),
3656 id,
3657 encoded,
3658 literal_addrs,
3659 widths,
3660 );
3661 machine.watch(debugger);
3662 let started = Instant::now();
3663 let result = machine.enter_closure(closure, &budget, span);
3664 if !(result.is_err() && cancellation.is_cancelled()) {
3665 if let Some(runtime) = runtime {
3666 runtime.trace(TraceEvent::TaskCompleted {
3667 id,
3668 // What the body spent rather than what the clock did: a task
3669 // that waited on a host was not working while it waited, and
3670 // a trace that could not tell the two apart is what ADR 0008
3671 // lists as the thing phase 1 could not validate.
3672 cpu: started.elapsed().saturating_sub(machine.host_wait()),
3673 });
3674 }
3675 }
3676 let words = result?;
3677 // Into the object the parent allocated, whose address it has held as a
3678 // root since before this thread existed. Nothing between here and the
3679 // last frame's `Return` allocates or reaches a safepoint, so no
3680 // collection can run in the window where these words are only in a Rust
3681 // `Vec`.
3682 for (at, word) in words.iter().enumerate() {
3683 machine.mem.set_payload(answer, 1 + at as u32, *word);
3684 }
3685 Ok(())
3686}
3687
3688fn compare(op: CmpOp, ordering: std::cmp::Ordering) -> bool {
3689 use std::cmp::Ordering::*;
3690 match op {
3691 CmpOp::Eq => ordering == Equal,
3692 CmpOp::Ne => ordering != Equal,
3693 CmpOp::Lt => ordering == Less,
3694 CmpOp::Le => ordering != Greater,
3695 CmpOp::Gt => ordering == Greater,
3696 CmpOp::Ge => ordering != Less,
3697 }
3698}
3699
3700/// `Int` arithmetic, with the language's messages.
3701///
3702/// The messages are the interpreter's, word for word, because overflow and
3703/// division by zero are rules of the language rather than of a backend. The
3704/// differential corpus compares them.
3705fn int_arith(op: ArithOp, a: i64, b: i64, duration: bool) -> Result<i64, RuntimeError> {
3706 let named = |what: &'static str| -> &'static str {
3707 if duration {
3708 "duration arithmetic"
3709 } else {
3710 what
3711 }
3712 };
3713 match op {
3714 ArithOp::Add => a
3715 .checked_add(b)
3716 .ok_or_else(|| overflowed(named("addition"))),
3717 ArithOp::Sub => a
3718 .checked_sub(b)
3719 .ok_or_else(|| overflowed(named("subtraction"))),
3720 ArithOp::Mul => a
3721 .checked_mul(b)
3722 .ok_or_else(|| overflowed(named("multiplication"))),
3723 ArithOp::Div => {
3724 if b == 0 {
3725 Err(divided_by_zero("division"))
3726 } else {
3727 a.checked_div(b).ok_or_else(|| overflowed("division"))
3728 }
3729 }
3730 ArithOp::Rem => {
3731 if b == 0 {
3732 Err(divided_by_zero("remainder"))
3733 } else {
3734 a.checked_rem(b).ok_or_else(|| overflowed("remainder"))
3735 }
3736 }
3737 }
3738}
3739
3740fn float_arith(op: ArithOp, a: f64, b: f64) -> f64 {
3741 match op {
3742 ArithOp::Add => a + b,
3743 ArithOp::Sub => a - b,
3744 ArithOp::Mul => a * b,
3745 ArithOp::Div => a / b,
3746 ArithOp::Rem => a % b,
3747 }
3748}
3749
3750fn overflowed(operation: &str) -> RuntimeError {
3751 RuntimeError::new(format!("`Int` {operation} overflowed"))
3752 .with_rule("Integer overflow is a broken invariant, not a wrapped result.")
3753}
3754
3755fn divided_by_zero(operation: &str) -> RuntimeError {
3756 RuntimeError::new(format!("`Int` {operation} by zero"))
3757 .with_rule("Division and remainder by zero are broken invariants.")
3758}
3759
3760/// A call passed a number of arguments the callee does not declare.
3761///
3762/// The verifier checks it, so this is a lowering bug that got past it. It is
3763/// reported rather than assumed because the alternative is a callee whose
3764/// remaining parameters hold whatever the frame was zeroed with — which is a
3765/// silent wrong answer instead of a loud one.
3766fn wrong_arity(callee: String, declared: usize, given: usize) -> RuntimeError {
3767 RuntimeError::new(format!(
3768 "this call passes {given} argument(s) to `{callee}`, which declares {declared}"
3769 ))
3770}
3771
3772/// A reference slot held null where an object was needed.
3773///
3774/// This is not a language-level `nil`: Cove has none. It is a lowering bug
3775/// reaching the machine, reported rather than read through.
3776fn null_object() -> RuntimeError {
3777 RuntimeError::new("this value was read before it was given one")
3778}
3779
3780/// A `lock` taken by a task that already holds the same cell.
3781///
3782/// The oracle's, word for word: `crate::shared::reentrant_lock` is where these
3783/// three sentences are written, and a program refused by one backend and not
3784/// the other in different words would be two languages. What
3785/// [ADR 0037](../../../../docs/adr/0037-a-cycle-through-a-cell-is-an-ordinary-cycle.md)
3786/// removed is the *other* refusal `lock` used to make; this one it kept, and
3787/// gave the reason: locking the same cell twice from one task is a live lock
3788/// state, and no collector can answer one.
3789fn reentrant_lock() -> RuntimeError {
3790 RuntimeError::new("this task already holds this `Shared`, so `lock` would wait for itself")
3791 .with_rule(
3792 "`lock` holds the value for the whole of the closure it is given, so a `lock` on the same `Shared` inside it can never be granted.",
3793 )
3794 .with_help("do the whole read-modify-write in one `lock`")
3795}
3796
3797/// A `Repr::Task` or `Repr::Scope` word that names no entry of this task's
3798/// scheduler table.
3799///
3800/// Zero is what a zeroed frame leaves in a slot nothing has written, which is
3801/// the same lowering bug a null reference is and earns the same refusal.
3802fn no_such_handle(what: &str) -> RuntimeError {
3803 RuntimeError::new(format!("this {what} was read before it was given one"))
3804}
3805
3806/// A `spawn` this run has no stack segment left for.
3807///
3808/// The reserved stack region divides into a fixed number of segments and a
3809/// task owns one, so a run with more tasks executing at once than there are
3810/// segments has nowhere to put the next one's frames. It is reported as what
3811/// it is — a limit on how many tasks may run at once — rather than as a
3812/// second, differently worded ceiling standing beside the one
3813/// `[run.*] max_tasks` configures, because a program that hits either has hit
3814/// the same wall.
3815///
3816/// The tree-walking oracle has no equivalent: it puts a task's frames on a
3817/// thread's own stack and is bounded by what the operating system will give
3818/// it. Every corpus program stays far below both.
3819fn no_segment_left() -> RuntimeError {
3820 RuntimeError::new(
3821 "execution stopped: this run has no stack segment left for another task, so no more tasks may run at once",
3822 )
3823}
3824
3825#[cfg(test)]
3826pub(crate) mod tests {
3827 use super::*;
3828 use cove_ir::{
3829 Arg, ArgsId, Capture, Compare, Function, Inst, Layout, Len, Num, RefMap, Table, TableId,
3830 };
3831 use std::sync::Arc;
3832
3833 /// Builds a program by hand.
3834 ///
3835 /// The lowering is a separate piece and a separate test suite. What is
3836 /// under test here is the machine, so its programs are written in the IR
3837 /// directly: a failure is then unambiguously the loop's, and a change to
3838 /// the lowering cannot quietly stop exercising an instruction.
3839 ///
3840 /// `pub(crate)` so that the boundary's and the builtins' tests write
3841 /// their fixtures the same way. A hand-written program is the only kind
3842 /// any of them uses, and having one builder is what keeps a fixture from
3843 /// being the thing under test.
3844 #[derive(Default)]
3845 pub(crate) struct Build {
3846 pub(crate) program: Program,
3847 }
3848
3849 impl Build {
3850 pub(crate) fn strings(mut self, texts: &[&str]) -> Build {
3851 self.program.strings = texts.iter().map(|text| Arc::from(*text)).collect();
3852 self
3853 }
3854
3855 /// A family that lives in the heap, so a value of it is one address.
3856 pub(crate) fn layout(&mut self, name: &str, shape: Shape) -> LayoutId {
3857 self.push(Layout::object(name, shape))
3858 }
3859
3860 /// A one-word family: the width-one case of the whole model.
3861 pub(crate) fn word(&mut self, name: &str, repr: Repr) -> LayoutId {
3862 self.push(Layout::word(name, repr))
3863 }
3864
3865 /// A struct, laid out inline from its fields' layouts.
3866 ///
3867 /// The offsets are computed by `cove_ir::struct_layout` rather than
3868 /// written out, because they are not a choice a fixture gets to make:
3869 /// a fixture free to say where a field is could agree with a machine
3870 /// that had it wrong.
3871 pub(crate) fn structure(&mut self, name: &str, fields: &[(&str, LayoutId)]) -> LayoutId {
3872 let named: Vec<(Arc<str>, LayoutId)> = fields
3873 .iter()
3874 .map(|(name, id)| (Arc::from(*name), *id))
3875 .collect();
3876 let (fields, words) = cove_ir::struct_layout(&named, &self.program.layouts);
3877 self.push(Layout::inline(
3878 name,
3879 Shape::Struct {
3880 fields,
3881 opaque: false,
3882 },
3883 words,
3884 ))
3885 }
3886
3887 /// An enum, laid out under the payload-agreement rule.
3888 pub(crate) fn enumeration(
3889 &mut self,
3890 name: &str,
3891 cases: &[(&str, Vec<LayoutId>)],
3892 ) -> LayoutId {
3893 let named: Vec<(Arc<str>, Vec<LayoutId>)> = cases
3894 .iter()
3895 .map(|(name, parts)| (Arc::from(*name), parts.clone()))
3896 .collect();
3897 let (cases, payload) = cove_ir::enum_layout(&named, &self.program.layouts);
3898 let mut words = vec![Repr::Int];
3899 words.extend_from_slice(&payload);
3900 self.push(Layout::inline(name, Shape::Enum { cases, payload }, words))
3901 }
3902
3903 /// The layout a `Box` allocates, reserved the way the lowering
3904 /// reserves it: a fixture that had to remember to declare one would
3905 /// be a fixture that could forget, and forgetting sizes the object
3906 /// by the wrong shape.
3907 pub(crate) fn boxed(&mut self) -> LayoutId {
3908 self.seed();
3909 self.program.boxed_layout
3910 }
3911
3912 /// `LayoutId(0)` is the sweeper's free block and `LayoutId(1)` is the
3913 /// box, exactly as `cove_ir::lower` reserves them.
3914 fn seed(&mut self) {
3915 if self.program.layouts.is_empty() {
3916 self.program.layouts.push(Layout::free());
3917 self.program
3918 .layouts
3919 .push(Layout::object("Any", Shape::Boxed));
3920 self.program.boxed_layout = LayoutId(1);
3921 }
3922 }
3923
3924 fn push(&mut self, layout: Layout) -> LayoutId {
3925 self.seed();
3926 self.program.layouts.push(layout);
3927 LayoutId(self.program.layouts.len() as u32 - 1)
3928 }
3929
3930 /// An argument list: where each value is, and the layout that says
3931 /// how wide it is.
3932 pub(crate) fn args(&mut self, args: &[(Slot, LayoutId)]) -> ArgsId {
3933 self.program.args.push(
3934 args.iter()
3935 .map(|(slot, layout)| Arg {
3936 slot: *slot,
3937 layout: *layout,
3938 })
3939 .collect(),
3940 );
3941 ArgsId(self.program.args.len() as u32 - 1)
3942 }
3943
3944 pub(crate) fn table(&mut self, targets: &[u32], default: u32) -> TableId {
3945 self.program.tables.push(Table {
3946 targets: targets.to_vec(),
3947 default,
3948 });
3949 TableId(self.program.tables.len() as u32 - 1)
3950 }
3951
3952 pub(crate) fn function(
3953 &mut self,
3954 name: &str,
3955 params: &[LayoutId],
3956 reprs: &[Repr],
3957 returns: LayoutId,
3958 code: Vec<Inst>,
3959 ) -> FunctionId {
3960 let nowhere = Span::new(cove_diag::FileId(0), 0, 0);
3961 let spans = vec![nowhere; code.len()];
3962 self.program.functions.push(Function {
3963 module: Arc::from("t"),
3964 name: Arc::from(name),
3965 params: params.to_vec(),
3966 reprs: reprs.to_vec(),
3967 refs: RefMap::of(reprs),
3968 returns,
3969 captures: Vec::<Capture>::new(),
3970 code,
3971 spans,
3972 locals: Vec::new(),
3973 inlined: Vec::new(),
3974 span: nowhere,
3975 is_async: false,
3976 // A function a test builds by hand is a function with a
3977 // body, whatever the body is: nothing here stands in for a
3978 // declaration the lowering left out.
3979 stub: false,
3980 });
3981 let id = FunctionId(self.program.functions.len() as u32 - 1);
3982 self.program
3983 .by_name
3984 .insert((Arc::from("t"), Arc::from(name)), id);
3985 id
3986 }
3987
3988 /// A function that reads captures: what a lowered lambda is.
3989 ///
3990 /// The slot each capture lands in is filled in here rather than
3991 /// written out per fixture, because it is not a choice a fixture gets
3992 /// to make: captures follow the parameters, so the first one begins
3993 /// where the parameters' words end and each one after it follows at
3994 /// its own width, and a fixture free to say otherwise could agree
3995 /// with a machine that had the rule wrong.
3996 pub(crate) fn lambda(
3997 &mut self,
3998 name: &str,
3999 params: &[LayoutId],
4000 reprs: &[Repr],
4001 returns: LayoutId,
4002 captures: &[LayoutId],
4003 code: Vec<Inst>,
4004 ) -> FunctionId {
4005 let mut slot: Slot = params
4006 .iter()
4007 .map(|id| self.program.layout(*id).width())
4008 .sum();
4009 let held: Vec<Capture> = captures
4010 .iter()
4011 .enumerate()
4012 .map(|(at, layout)| {
4013 let capture = Capture {
4014 name: Arc::from(format!("c{at}")),
4015 slot,
4016 layout: *layout,
4017 };
4018 slot += self.program.layout(*layout).width();
4019 capture
4020 })
4021 .collect();
4022 let id = self.function(name, params, reprs, returns, code);
4023 self.program.functions[id.index()].captures = held;
4024 id
4025 }
4026
4027 /// Checks the program the way the lowering must, so a malformed test
4028 /// fixture fails as a fixture rather than as a machine bug.
4029 pub(crate) fn done(self) -> Program {
4030 cove_ir::verify(&self.program).expect("a hand-written test program is well formed");
4031 self.program
4032 }
4033
4034 /// A program of layouts and strings and no functions.
4035 ///
4036 /// What a boundary or a builtin test needs: both of them convert or
4037 /// read values rather than running code, and a function written only
4038 /// so that a program has one would be a fixture nothing reads.
4039 pub(crate) fn bare(mut self) -> Program {
4040 let str_layout = self.layout("String", Shape::Str);
4041 self.program.str_layout = str_layout;
4042 self.done()
4043 }
4044
4045 /// `String`'s layout, declared and recorded as `Program::str_layout`
4046 /// the way `cove_ir::lower` records it — `Inst::Str` and
4047 /// [ADR 0051](../../../docs/adr/0051-a-string-is-built-as-a-byte-run.md)'s
4048 /// `Inst::FinishString` read the field rather than being told the
4049 /// layout at each call site, so a fixture that only declared the
4050 /// shape without recording it here would allocate strings the
4051 /// dispatch loop could not finish into.
4052 pub(crate) fn string_layout(&mut self) -> LayoutId {
4053 let id = self.layout("String", Shape::Str);
4054 self.program.str_layout = id;
4055 id
4056 }
4057
4058 /// [ADR 0051](../../../docs/adr/0051-a-string-is-built-as-a-byte-run.md)'s
4059 /// byte-run layout, declared and recorded as `Program::bytes_layout`
4060 /// for `string_layout`'s reason: `Inst::AllocBytes` always allocates
4061 /// this field's layout rather than one named in the instruction.
4062 pub(crate) fn bytes_layout(&mut self) -> LayoutId {
4063 let id = self.layout("Bytes", Shape::Bytes);
4064 self.program.bytes_layout = id;
4065 id
4066 }
4067
4068 /// [ADR 0052](../../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)'s
4069 /// byte-buffer owner layout, declared and recorded as
4070 /// `Program::buffer_layout` for `bytes_layout`'s reason:
4071 /// `Inst::AllocBuffer` allocates both of the field's layouts rather
4072 /// than any named in the instruction, so a fixture that declared only
4073 /// the shape would allocate owners the dispatch loop could not read.
4074 pub(crate) fn buffer_layout(&mut self) -> LayoutId {
4075 let id = self.layout("ByteBuffer", Shape::ByteBuffer);
4076 self.program.buffer_layout = id;
4077 id
4078 }
4079
4080 /// The one-word layout of `repr`, declared once per fixture.
4081 pub(crate) fn scalar(&mut self, repr: Repr) -> LayoutId {
4082 if let Some(at) = self
4083 .program
4084 .layouts
4085 .iter()
4086 .position(|layout| layout.shape == Shape::Word(repr))
4087 {
4088 return LayoutId(at as u32);
4089 }
4090 self.word(repr.name(), repr)
4091 }
4092 }
4093
4094 pub(crate) fn budget() -> Meter {
4095 crate::budget::Budget::new(crate::budget::Limits::default()).meter()
4096 }
4097
4098 /// The words a run of `entry` answers.
4099 ///
4100 /// A function answers a *value location*, which is a run of words, so
4101 /// this is the general shape of a result and [`run`] is the common case
4102 /// of it. A fixture that answers a `Point` reads two words here rather
4103 /// than one address naming two words somewhere else.
4104 pub(crate) fn run_words(
4105 program: &Program,
4106 entry: FunctionId,
4107 args: &[u64],
4108 ) -> Result<Vec<u64>, RuntimeError> {
4109 Machine::new(program, 1 << 16).run(entry, args, &budget())
4110 }
4111
4112 /// The one word a run of `entry` answers.
4113 ///
4114 /// Most of what is under test here is one word wide — an `Int`, a `Bool`,
4115 /// a reference — and writing `[0]` at every one of those call sites would
4116 /// put the same unchecked index in fifty places. The assertion is what
4117 /// keeps it honest: a fixture whose answer stopped being one word fails
4118 /// here rather than quietly reporting its first word.
4119 fn run(program: &Program, entry: FunctionId, args: &[u64]) -> Result<u64, RuntimeError> {
4120 let words = run_words(program, entry, args)?;
4121 assert_eq!(words.len(), 1, "this fixture answers one word");
4122 Ok(words[0])
4123 }
4124
4125 #[test]
4126 fn a_constant_comes_back() {
4127 let mut build = Build::default();
4128 let int = build.scalar(Repr::Int);
4129 let f = build.function(
4130 "answer",
4131 &[],
4132 &[Repr::Int],
4133 int,
4134 vec![Inst::Int { dst: 0, value: 42 }, Inst::Return { src: 0 }],
4135 );
4136 let program = build.done();
4137 assert_eq!(run(&program, f, &[]).unwrap() as i64, 42);
4138 }
4139
4140 #[test]
4141 fn arithmetic_reads_and_writes_slots() {
4142 let mut build = Build::default();
4143 let int = build.scalar(Repr::Int);
4144 let f = build.function(
4145 "add",
4146 &[int, int],
4147 &[Repr::Int, Repr::Int, Repr::Int],
4148 int,
4149 vec![
4150 Inst::Arith {
4151 num: Num::Int,
4152 op: ArithOp::Add,
4153 dst: 2,
4154 a: 0,
4155 b: 1,
4156 },
4157 Inst::Return { src: 2 },
4158 ],
4159 );
4160 let program = build.done();
4161 assert_eq!(run(&program, f, &[3, 4]).unwrap() as i64, 7);
4162 }
4163
4164 /// The messages are the language's, not the backend's; the differential
4165 /// corpus compares them against the tree-walking oracle word for word.
4166 #[test]
4167 fn arithmetic_faults_say_what_the_oracle_says() {
4168 let cases: Vec<(ArithOp, i64, i64, &str)> = vec![
4169 (ArithOp::Div, 1, 0, "`Int` division by zero"),
4170 (ArithOp::Rem, 1, 0, "`Int` remainder by zero"),
4171 (ArithOp::Add, i64::MAX, 1, "`Int` addition overflowed"),
4172 (ArithOp::Mul, i64::MAX, 2, "`Int` multiplication overflowed"),
4173 ];
4174 for (op, a, b, message) in cases {
4175 let mut build = Build::default();
4176 let int = build.scalar(Repr::Int);
4177 let f = build.function(
4178 "fault",
4179 &[int, int],
4180 &[Repr::Int, Repr::Int, Repr::Int],
4181 int,
4182 vec![
4183 Inst::Arith {
4184 num: Num::Int,
4185 op,
4186 dst: 2,
4187 a: 0,
4188 b: 1,
4189 },
4190 Inst::Return { src: 2 },
4191 ],
4192 );
4193 let program = build.done();
4194 let error = run(&program, f, &[a as u64, b as u64]).unwrap_err();
4195 assert_eq!(error.message, message);
4196 }
4197 }
4198
4199 /// A `Duration` is nanoseconds and its arithmetic is an integer's, so the
4200 /// only thing that changes is what an overflow is called.
4201 #[test]
4202 fn a_duration_overflow_is_named_a_duration_overflow() {
4203 let mut build = Build::default();
4204 let duration = build.scalar(Repr::Duration);
4205 let f = build.function(
4206 "late",
4207 &[duration, duration],
4208 &[Repr::Duration, Repr::Duration, Repr::Duration],
4209 duration,
4210 vec![
4211 Inst::Arith {
4212 num: Num::Int,
4213 op: ArithOp::Add,
4214 dst: 2,
4215 a: 0,
4216 b: 1,
4217 },
4218 Inst::Return { src: 2 },
4219 ],
4220 );
4221 let program = build.done();
4222 let error = run(&program, f, &[i64::MAX as u64, 1]).unwrap_err();
4223 assert_eq!(error.message, "`Int` duration arithmetic overflowed");
4224 }
4225
4226 #[test]
4227 fn a_branch_takes_one_side() {
4228 let mut build = Build::default();
4229 let int = build.scalar(Repr::Int);
4230 // fn abs(n) { if n < 0 { -n } else { n } }
4231 let f = build.function(
4232 "abs",
4233 &[int],
4234 &[Repr::Int, Repr::Int, Repr::Bool],
4235 int,
4236 vec![
4237 Inst::Int { dst: 1, value: 0 },
4238 Inst::Cmp {
4239 on: Compare::Int,
4240 op: CmpOp::Lt,
4241 dst: 2,
4242 a: 0,
4243 b: 1,
4244 },
4245 Inst::BranchFalse { cond: 2, to: 4 },
4246 Inst::Neg {
4247 num: Num::Int,
4248 dst: 0,
4249 a: 0,
4250 },
4251 Inst::Return { src: 0 },
4252 ],
4253 );
4254 let program = build.done();
4255 assert_eq!(run(&program, f, &[(-5i64) as u64]).unwrap() as i64, 5);
4256 assert_eq!(run(&program, f, &[5]).unwrap() as i64, 5);
4257 }
4258
4259 #[test]
4260 fn a_loop_runs_to_its_bound() {
4261 let mut build = Build::default();
4262 let int = build.scalar(Repr::Int);
4263 // fn sum(n) { var t = 0; var i = 0; while i < n { t = t + i; i = i + 1 }; t }
4264 let f = build.function(
4265 "sum",
4266 &[int],
4267 &[Repr::Int, Repr::Int, Repr::Int, Repr::Bool, Repr::Int],
4268 int,
4269 vec![
4270 Inst::Int { dst: 1, value: 0 },
4271 Inst::Int { dst: 2, value: 0 },
4272 Inst::Cmp {
4273 on: Compare::Int,
4274 op: CmpOp::Lt,
4275 dst: 3,
4276 a: 2,
4277 b: 0,
4278 },
4279 Inst::BranchFalse { cond: 3, to: 8 },
4280 Inst::Arith {
4281 num: Num::Int,
4282 op: ArithOp::Add,
4283 dst: 1,
4284 a: 1,
4285 b: 2,
4286 },
4287 Inst::Int { dst: 4, value: 1 },
4288 Inst::Arith {
4289 num: Num::Int,
4290 op: ArithOp::Add,
4291 dst: 2,
4292 a: 2,
4293 b: 4,
4294 },
4295 Inst::Jump { to: 2 },
4296 Inst::Return { src: 1 },
4297 ],
4298 );
4299 let program = build.done();
4300 assert_eq!(run(&program, f, &[10]).unwrap() as i64, 45);
4301 }
4302
4303 /// A call writes its arguments straight into the callee's slots and its
4304 /// answer straight into the caller's destination. There is no buffer
4305 /// between the two frames, and this is what says so.
4306 #[test]
4307 fn recursion_nests_frames_and_unwinds_them() {
4308 let mut build = Build::default();
4309 let int = build.scalar(Repr::Int);
4310 let args = build.args(&[(3, int)]);
4311 // fn fact(n) { if n <= 1 { 1 } else { n * fact(n - 1) } }
4312 let f = build.function(
4313 "fact",
4314 &[int],
4315 &[Repr::Int, Repr::Int, Repr::Bool, Repr::Int, Repr::Int],
4316 int,
4317 vec![
4318 Inst::Int { dst: 1, value: 1 },
4319 Inst::Cmp {
4320 on: Compare::Int,
4321 op: CmpOp::Le,
4322 dst: 2,
4323 a: 0,
4324 b: 1,
4325 },
4326 Inst::BranchFalse { cond: 2, to: 4 },
4327 Inst::Return { src: 1 },
4328 Inst::Arith {
4329 num: Num::Int,
4330 op: ArithOp::Sub,
4331 dst: 3,
4332 a: 0,
4333 b: 1,
4334 },
4335 Inst::Call {
4336 dst: 4,
4337 callee: FunctionId(0),
4338 args,
4339 },
4340 Inst::Arith {
4341 num: Num::Int,
4342 op: ArithOp::Mul,
4343 dst: 4,
4344 a: 0,
4345 b: 4,
4346 },
4347 Inst::Return { src: 4 },
4348 ],
4349 );
4350 let program = build.done();
4351 assert_eq!(run(&program, f, &[10]).unwrap() as i64, 3_628_800);
4352 }
4353
4354 /// Depth is bounded by the reserved stack region, not by the Rust stack:
4355 /// a call does not recurse in the dispatch loop, so this returns an error
4356 /// rather than ending the process.
4357 #[test]
4358 fn an_unbounded_recursion_is_stopped() {
4359 let mut build = Build::default();
4360 let int = build.scalar(Repr::Int);
4361 let args = build.args(&[(0, int)]);
4362 let f = build.function(
4363 "forever",
4364 &[int],
4365 &[Repr::Int, Repr::Int],
4366 int,
4367 vec![
4368 Inst::Call {
4369 dst: 1,
4370 callee: FunctionId(0),
4371 args,
4372 },
4373 Inst::Return { src: 1 },
4374 ],
4375 );
4376 let program = build.done();
4377 let error = run(&program, f, &[0]).unwrap_err();
4378 assert_eq!(error.message, "this call nests too deeply");
4379 }
4380
4381 // ---- values that are more than one word ------------------------------
4382
4383 /// `docs/LINEAR_VM.md` §1, in the IR it writes out.
4384 ///
4385 /// ~~~cove
4386 /// struct Point { x: Int, y: Int }
4387 /// var a = Point(x: 1, y: 2)
4388 /// var b = a
4389 /// b.x = 7
4390 /// ~~~
4391 ///
4392 /// `a` is at slots 0–1 and `b` at 2–3, and `b = a` is one `Copy` of two
4393 /// words. `a.x` is slot 0 and nothing touched it — not because a bit said
4394 /// the copy was unshared, but because the copy put `b`'s words where `b`
4395 /// is. There is no sharing bit, no copy-on-write and no write path to
4396 /// unshare; `b.x = 7` writes slot 2 and that is all of it.
4397 ///
4398 /// The answer is the four slots read as one `Pair`, which is the same
4399 /// claim from the other side: a value location is a base slot and a
4400 /// layout, so two adjacent `Point`s *are* a four-word value.
4401 #[test]
4402 fn a_copy_is_the_words_of_the_value() {
4403 let mut build = Build::default();
4404 let int = build.scalar(Repr::Int);
4405 let point = build.structure("Point", &[("x", int), ("y", int)]);
4406 let pair = build.structure("Pair", &[("a", point), ("b", point)]);
4407 let f = build.function(
4408 "copy",
4409 &[],
4410 &[Repr::Int, Repr::Int, Repr::Int, Repr::Int, Repr::Int],
4411 pair,
4412 vec![
4413 Inst::Int { dst: 0, value: 1 },
4414 Inst::Int { dst: 1, value: 2 },
4415 Inst::Copy {
4416 dst: 2,
4417 src: 0,
4418 layout: point,
4419 },
4420 Inst::Int { dst: 4, value: 7 },
4421 // `b.x` is slot 2 + 0: a field of an inline struct is
4422 // arithmetic the lowering did, not an instruction.
4423 Inst::Copy {
4424 dst: 2,
4425 src: 4,
4426 layout: int,
4427 },
4428 Inst::Return { src: 0 },
4429 ],
4430 );
4431 let program = build.done();
4432 assert_eq!(program.layout(point).words, vec![Repr::Int, Repr::Int]);
4433 assert_eq!(run_words(&program, f, &[]).unwrap(), vec![1, 2, 7, 2]);
4434 }
4435
4436 /// `docs/LINEAR_VM.md` §3: `struct Wrapper { p: Point, v: Vector<Int> }`
4437 /// is `[p.x: Int, p.y: Int, v: Ref]`, and a copy copies all three words.
4438 ///
4439 /// Two answers fall out of that one copy and neither needed a policy. The
4440 /// `Point` words become independent, so writing `b.p.x` leaves `a.p.x`
4441 /// alone. The `Vector` address is duplicated, so both wrappers name one
4442 /// vector — which is ADR 0001 verbatim, because a `Vector`'s storage is
4443 /// shared and mutable by the language's own rule rather than by anything
4444 /// the representation decided.
4445 #[test]
4446 fn a_copied_wrapper_separates_its_point_and_shares_its_vector() {
4447 let mut build = Build::default();
4448 let int = build.scalar(Repr::Int);
4449 let point = build.structure("Point", &[("x", int), ("y", int)]);
4450 let vector = build.layout("Vector", Shape::Vector { elem: int });
4451 let wrapper = build.structure("Wrapper", &[("p", point), ("v", vector)]);
4452 let both = build.structure("Both", &[("a", wrapper), ("b", wrapper)]);
4453 let f = build.function(
4454 "wrap",
4455 &[],
4456 &[
4457 Repr::Int,
4458 Repr::Int,
4459 Repr::Ref,
4460 Repr::Int,
4461 Repr::Int,
4462 Repr::Ref,
4463 Repr::Int,
4464 ],
4465 both,
4466 vec![
4467 Inst::Int { dst: 0, value: 1 },
4468 Inst::Int { dst: 1, value: 2 },
4469 Inst::Alloc {
4470 dst: 2,
4471 layout: vector,
4472 len: Len::Fixed,
4473 },
4474 Inst::Copy {
4475 dst: 3,
4476 src: 0,
4477 layout: wrapper,
4478 },
4479 Inst::Int { dst: 6, value: 7 },
4480 Inst::Copy {
4481 dst: 3,
4482 src: 6,
4483 layout: int,
4484 },
4485 Inst::Return { src: 0 },
4486 ],
4487 );
4488 let program = build.done();
4489 assert_eq!(
4490 program.layout(wrapper).words,
4491 vec![Repr::Int, Repr::Int, Repr::Ref]
4492 );
4493 let words = run_words(&program, f, &[]).unwrap();
4494 assert_eq!(words.len(), 6);
4495 assert_eq!(&words[..2], &[1, 2], "`a`'s point is where `a` is");
4496 assert_eq!(&words[3..5], &[7, 2], "`b`'s point is where `b` is");
4497 assert_ne!(words[2], 0, "the vector was allocated");
4498 assert_eq!(words[2], words[5], "and both wrappers name that one vector");
4499 }
4500
4501 /// `docs/LINEAR_VM.md` §5: a parameter takes the words its layout says,
4502 /// from slot 0 onward in declaration order, so a `(Int, Point, Int)` list
4503 /// occupies slots 0, 1–2 and 3. Nothing is permuted into type groups,
4504 /// because there are no type groups.
4505 ///
4506 /// The answer is a `Point` too, and `Return` copies the two words its
4507 /// `Function::returns` describes into the caller's destination location.
4508 /// Neither direction allocates: a struct crosses a call as its words.
4509 #[test]
4510 fn a_struct_is_passed_and_returned_as_its_words() {
4511 let mut build = Build::default();
4512 let int = build.scalar(Repr::Int);
4513 let point = build.structure("Point", &[("x", int), ("y", int)]);
4514 // fn shift(n: Int, p: Point, m: Int) -> Point
4515 let shift = build.function(
4516 "shift",
4517 &[int, point, int],
4518 &[
4519 Repr::Int,
4520 Repr::Int,
4521 Repr::Int,
4522 Repr::Int,
4523 Repr::Int,
4524 Repr::Int,
4525 ],
4526 point,
4527 vec![
4528 Inst::Arith {
4529 num: Num::Int,
4530 op: ArithOp::Add,
4531 dst: 4,
4532 a: 1,
4533 b: 0,
4534 },
4535 Inst::Arith {
4536 num: Num::Int,
4537 op: ArithOp::Add,
4538 dst: 5,
4539 a: 2,
4540 b: 3,
4541 },
4542 Inst::Return { src: 4 },
4543 ],
4544 );
4545 let args = build.args(&[(0, int), (1, point), (3, int)]);
4546 let main = build.function(
4547 "main",
4548 &[],
4549 &[
4550 Repr::Int,
4551 Repr::Int,
4552 Repr::Int,
4553 Repr::Int,
4554 Repr::Int,
4555 Repr::Int,
4556 ],
4557 point,
4558 vec![
4559 Inst::Int { dst: 0, value: 10 },
4560 Inst::Int { dst: 1, value: 1 },
4561 Inst::Int { dst: 2, value: 2 },
4562 Inst::Int { dst: 3, value: 20 },
4563 Inst::Call {
4564 dst: 4,
4565 callee: shift,
4566 args,
4567 },
4568 Inst::Return { src: 4 },
4569 ],
4570 );
4571 let program = build.done();
4572 let target = program.function(shift);
4573 assert_eq!(target.param_slot(0, &program.layouts), 0);
4574 assert_eq!(target.param_slot(1, &program.layouts), 1);
4575 assert_eq!(target.param_slot(2, &program.layouts), 3);
4576 assert_eq!(target.param_words(&program.layouts), 4);
4577 assert_eq!(run_words(&program, main, &[]).unwrap(), vec![11, 22]);
4578 }
4579
4580 /// An `Array<Point>` is a run of two-word elements rather than a run of
4581 /// addresses, and the stride an element instruction uses is the element
4582 /// layout's width.
4583 ///
4584 /// The header's `len` counts *elements*, so an index is checked against
4585 /// three and then multiplied — which is why writing element 1 through an
4586 /// `AddrOfElem` leaves element 2 alone rather than smearing across it,
4587 /// and why index 3 is refused although the object holds six words.
4588 #[test]
4589 fn an_array_of_points_is_walked_at_a_two_word_stride() {
4590 let mut build = Build::default();
4591 let int = build.scalar(Repr::Int);
4592 let point = build.structure("Point", &[("x", int), ("y", int)]);
4593 let points = build.layout(
4594 "Array",
4595 Shape::Elements {
4596 elem: point,
4597 growable: false,
4598 },
4599 );
4600 let two = build.structure("Two", &[("a", point), ("b", point)]);
4601 let reprs = &[
4602 Repr::Ref,
4603 Repr::Int,
4604 Repr::Int,
4605 Repr::Int,
4606 Repr::Int,
4607 Repr::Int,
4608 Repr::Int,
4609 Repr::Int,
4610 Repr::Addr,
4611 ];
4612 let walk = build.function(
4613 "walk",
4614 &[],
4615 reprs,
4616 two,
4617 vec![
4618 Inst::Alloc {
4619 dst: 0,
4620 layout: points,
4621 len: Len::Count(3),
4622 },
4623 // xs[0] = Point(1, 2)
4624 Inst::Int { dst: 1, value: 0 },
4625 Inst::Int { dst: 2, value: 1 },
4626 Inst::Int { dst: 3, value: 2 },
4627 Inst::StoreElem {
4628 obj: 0,
4629 index: 1,
4630 src: 2,
4631 layout: point,
4632 },
4633 // xs[1] = Point(3, 4)
4634 Inst::Int { dst: 1, value: 1 },
4635 Inst::Int { dst: 2, value: 3 },
4636 Inst::Int { dst: 3, value: 4 },
4637 Inst::StoreElem {
4638 obj: 0,
4639 index: 1,
4640 src: 2,
4641 layout: point,
4642 },
4643 // xs[2] = Point(5, 6)
4644 Inst::Int { dst: 1, value: 2 },
4645 Inst::Int { dst: 2, value: 5 },
4646 Inst::Int { dst: 3, value: 6 },
4647 Inst::StoreElem {
4648 obj: 0,
4649 index: 1,
4650 src: 2,
4651 layout: point,
4652 },
4653 // A place naming element 1, written through: two words at
4654 // one address, with nothing between the address and them.
4655 Inst::Int { dst: 1, value: 1 },
4656 Inst::AddrOfElem {
4657 dst: 8,
4658 obj: 0,
4659 index: 1,
4660 layout: point,
4661 },
4662 Inst::Int { dst: 2, value: 30 },
4663 Inst::Int { dst: 3, value: 40 },
4664 Inst::Store {
4665 addr: 8,
4666 src: 2,
4667 layout: point,
4668 },
4669 Inst::LoadElem {
4670 dst: 4,
4671 obj: 0,
4672 index: 1,
4673 layout: point,
4674 },
4675 Inst::Int { dst: 1, value: 2 },
4676 Inst::LoadElem {
4677 dst: 6,
4678 obj: 0,
4679 index: 1,
4680 layout: point,
4681 },
4682 Inst::Return { src: 4 },
4683 ],
4684 );
4685 let past = build.function(
4686 "past",
4687 &[],
4688 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Int],
4689 int,
4690 vec![
4691 Inst::Alloc {
4692 dst: 0,
4693 layout: points,
4694 len: Len::Count(3),
4695 },
4696 Inst::Int { dst: 1, value: 3 },
4697 Inst::LoadElem {
4698 dst: 2,
4699 obj: 0,
4700 index: 1,
4701 layout: point,
4702 },
4703 Inst::Return { src: 2 },
4704 ],
4705 );
4706 let program = build.done();
4707 assert_eq!(
4708 run_words(&program, walk, &[]).unwrap(),
4709 vec![30, 40, 5, 6],
4710 "the write through element 1 left element 2 where it was"
4711 );
4712 let error = run(&program, past, &[]).unwrap_err();
4713 assert_eq!(error.message, "index 3 is outside a collection of 3");
4714 }
4715
4716 /// An enum is a discriminant word and a payload region wide enough for
4717 /// every case, and the offsets are assigned so that **every case using a
4718 /// payload word agrees on its `Repr`**.
4719 ///
4720 /// `enum Msg { Text(Cell), Count(Int) }` therefore lays out as
4721 /// `[disc: Int, Ref, Int]`: `Count`'s `Int` cannot share `Text`'s
4722 /// reference word, so it takes a third. Two things follow, and this is
4723 /// both of them. Constructing a case zeroes the region it does not fill,
4724 /// so `Count`'s reference word reads null rather than whatever `Text`
4725 /// left there. And the collector never reads the discriminant to decide
4726 /// what to trace — the region's map is static, which is one fewer thing
4727 /// that can be wrong.
4728 ///
4729 /// The heap holds one cell and not two, so the second allocation is the
4730 /// question: it succeeds when the value was rebuilt as `Count`, because
4731 /// the word naming the first cell was zeroed and nothing reaches it, and
4732 /// it fails when the value is still `Text`, because that same word is
4733 /// traced and the cell is live.
4734 #[test]
4735 fn an_enums_payload_is_retained_by_its_static_map() {
4736 let mut build = Build::default();
4737 let int = build.scalar(Repr::Int);
4738 let cell = build.layout(
4739 "Cell",
4740 Shape::Elements {
4741 elem: int,
4742 growable: false,
4743 },
4744 );
4745 let msg = build.enumeration("Msg", &[("Text", vec![cell]), ("Count", vec![int])]);
4746 let boolean = build.scalar(Repr::Bool);
4747 let f = build.function(
4748 "held",
4749 &[boolean],
4750 &[
4751 Repr::Bool,
4752 // The `Msg` is at slots 1–3: a discriminant, `Text`'s
4753 // reference and `Count`'s integer.
4754 Repr::Int,
4755 Repr::Ref,
4756 Repr::Int,
4757 Repr::Ref,
4758 Repr::Int,
4759 ],
4760 int,
4761 vec![
4762 Inst::Alloc {
4763 dst: 4,
4764 layout: cell,
4765 len: Len::Count(1200),
4766 },
4767 Inst::Int { dst: 1, value: 0 },
4768 Inst::Copy {
4769 dst: 2,
4770 src: 4,
4771 layout: cell,
4772 },
4773 // The enum's payload word is now the only name for the cell.
4774 Inst::Clear {
4775 slot: 4,
4776 layout: cell,
4777 },
4778 Inst::BranchFalse { cond: 0, to: 8 },
4779 // Constructing `Count` zeroes the region it does not fill,
4780 // which is what leaves `Text`'s reference word null.
4781 Inst::Clear {
4782 slot: 1,
4783 layout: msg,
4784 },
4785 Inst::Int { dst: 1, value: 1 },
4786 Inst::Int { dst: 3, value: 5 },
4787 Inst::Alloc {
4788 dst: 4,
4789 layout: cell,
4790 len: Len::Count(1200),
4791 },
4792 Inst::Int { dst: 5, value: 7 },
4793 Inst::Return { src: 5 },
4794 ],
4795 );
4796 let program = build.done();
4797 assert_eq!(
4798 program.layout(msg).words,
4799 vec![Repr::Int, Repr::Ref, Repr::Int]
4800 );
4801
4802 let mut kept = Machine::new(&program, 2048);
4803 let error = kept.run(f, &[0], &budget()).unwrap_err();
4804 assert_eq!(error.message, "this run has no memory left");
4805
4806 let mut dropped = Machine::new(&program, 2048);
4807 assert_eq!(dropped.run(f, &[1], &budget()).unwrap(), vec![7]);
4808 assert!(
4809 dropped.collected().collections > 0,
4810 "the second cell only fits after the first is reclaimed"
4811 );
4812 }
4813
4814 /// A one-parameter function that allocates `layout` with `Len::Slot(0)`
4815 /// and returns it: the shape issue #269 is about, where the count is a
4816 /// value the *running* program computed rather than one this compiler
4817 /// chose, so the parameter is a slot the caller controls entirely, and
4818 /// `Machine::allocate` is the one place left to check it.
4819 ///
4820 /// `build` and `int` are the caller's: a `LayoutId` only means anything
4821 /// against the layout table it came from, so `layout` and the scalar the
4822 /// parameter is declared with have to be pushed into the same `Build`
4823 /// this finishes.
4824 fn alloc_of_slot_length(
4825 mut build: Build,
4826 int: LayoutId,
4827 layout: LayoutId,
4828 ) -> (Program, FunctionId) {
4829 let f = build.function(
4830 "alloc_len",
4831 &[int],
4832 &[Repr::Int, Repr::Ref],
4833 layout,
4834 vec![
4835 Inst::Alloc {
4836 dst: 1,
4837 layout,
4838 len: Len::Slot(0),
4839 },
4840 Inst::Return { src: 1 },
4841 ],
4842 );
4843 (build.done(), f)
4844 }
4845
4846 /// A negative count is not a large `u32`: `Machine::allocate` reads the
4847 /// slot's bits as the `i64` they are before anything narrows them, so a
4848 /// negative one is caught rather than turned into an allocation of
4849 /// billions of elements.
4850 #[test]
4851 fn a_negative_length_is_rejected_before_it_narrows() {
4852 let mut build = Build::default();
4853 let int = build.scalar(Repr::Int);
4854 let cell = build.layout(
4855 "Cell",
4856 Shape::Elements {
4857 elem: int,
4858 growable: false,
4859 },
4860 );
4861 let (program, f) = alloc_of_slot_length(build, int, cell);
4862 let mut machine = Machine::new(&program, 1 << 16);
4863 let error = machine.run(f, &[(-1i64) as u64], &budget()).unwrap_err();
4864 assert_eq!(error.message, "this run has no memory left");
4865 }
4866
4867 /// A count past `u32::MAX` is not representable in the header's own
4868 /// length field, whatever the element width is.
4869 #[test]
4870 fn a_length_past_u32_max_is_rejected() {
4871 let mut build = Build::default();
4872 let int = build.scalar(Repr::Int);
4873 let cell = build.layout(
4874 "Cell",
4875 Shape::Elements {
4876 elem: int,
4877 growable: false,
4878 },
4879 );
4880 let (program, f) = alloc_of_slot_length(build, int, cell);
4881 let mut machine = Machine::new(&program, 1 << 16);
4882 let error = machine
4883 .run(f, &[u64::from(u32::MAX) + 1], &budget())
4884 .unwrap_err();
4885 assert_eq!(error.message, "this run has no memory left");
4886 }
4887
4888 /// A count that fits `u32` on its own can still make `count * stride`
4889 /// overflow it: `u32::MAX` elements of a two-word `Point` is the case
4890 /// [`cove_ir::Layout::try_payload_words`] exists for, checked in `u64`
4891 /// rather than wrapped in `u32`.
4892 #[test]
4893 fn a_count_times_stride_overflow_is_rejected() {
4894 let mut build = Build::default();
4895 let int = build.scalar(Repr::Int);
4896 let point = build.structure("Point", &[("x", int), ("y", int)]);
4897 let array = build.layout(
4898 "Array",
4899 Shape::Elements {
4900 elem: point,
4901 growable: false,
4902 },
4903 );
4904 let (program, f) = alloc_of_slot_length(build, int, array);
4905 let mut machine = Machine::new(&program, 1 << 16);
4906 let error = machine
4907 .run(f, &[u64::from(u32::MAX)], &budget())
4908 .unwrap_err();
4909 assert_eq!(error.message, "this run has no memory left");
4910 }
4911
4912 /// A count that is entirely in range, and whose payload size does not
4913 /// overflow anything, is still refused once it is larger than this run's
4914 /// own heap budget — the same "this run has no memory left" a
4915 /// `Len::Count` allocation raises, reached this time through a
4916 /// `Len::Slot` the running program computed.
4917 #[test]
4918 fn a_length_beyond_the_heap_budget_is_rejected() {
4919 let mut build = Build::default();
4920 let int = build.scalar(Repr::Int);
4921 let cell = build.layout(
4922 "Cell",
4923 Shape::Elements {
4924 elem: int,
4925 growable: false,
4926 },
4927 );
4928 let (program, f) = alloc_of_slot_length(build, int, cell);
4929 let mut machine = Machine::new(&program, 64);
4930 let error = machine.run(f, &[1_000_000], &budget()).unwrap_err();
4931 assert_eq!(error.message, "this run has no memory left");
4932 }
4933
4934 /// A frame's map is a function of its `Repr`s, and a multiword value
4935 /// contributes its flattened per-word ones.
4936 ///
4937 /// `docs/LINEAR_VM.md` §6: a `Wrapper { p: Point, v: Vector }` at slot 5
4938 /// contributes `Int, Int, Ref`, so slot 7 is a root and 5 and 6 are not.
4939 /// Nothing about the value's *width* reaches the collector — it reads one
4940 /// bit per slot, as it did when every value was one word, and a wide
4941 /// value is simply several slots' worth of bits.
4942 ///
4943 /// The other half is that a slot the map does not name cannot hold a
4944 /// reference at all: the verifier holds every instruction to the `Repr`
4945 /// of the slot it names, so a program that put an address in slot 6 is
4946 /// not a program. That is what makes one static bit per slot sound, and
4947 /// it is why the dynamic half of this test reaches for `Clear` instead —
4948 /// a reference slot the map *does* name, emptied at its last use.
4949 #[test]
4950 fn a_frames_map_covers_a_multiword_value_word_by_word() {
4951 let mut build = Build::default();
4952 let int = build.scalar(Repr::Int);
4953 let cell = build.layout(
4954 "Cell",
4955 Shape::Elements {
4956 elem: int,
4957 growable: false,
4958 },
4959 );
4960 let point = build.structure("Point", &[("x", int), ("y", int)]);
4961 let wrapper = build.structure("Wrapper", &[("p", point), ("v", cell)]);
4962 assert_eq!(
4963 build.program.layout(wrapper).words,
4964 vec![Repr::Int, Repr::Int, Repr::Ref],
4965 "three words: the `Point` inline, and the cell's address"
4966 );
4967
4968 // A `Wrapper` at slots 5-7, a scratch reference at slot 8, and the
4969 // answer at slot 9.
4970 let reprs = vec![
4971 Repr::Int,
4972 Repr::Int,
4973 Repr::Int,
4974 Repr::Int,
4975 Repr::Int,
4976 Repr::Int,
4977 Repr::Int,
4978 Repr::Ref,
4979 Repr::Ref,
4980 Repr::Int,
4981 ];
4982 let f = build.function(
4983 "wrapper",
4984 &[],
4985 &reprs,
4986 int,
4987 vec![
4988 // The wrapper's `v`, which its own slot keeps alive.
4989 Inst::Alloc {
4990 dst: 7,
4991 layout: cell,
4992 len: Len::Count(600),
4993 },
4994 // A second cell, named by a reference slot that is then
4995 // cleared — so the map still reads slot 8, and reads null.
4996 Inst::Alloc {
4997 dst: 8,
4998 layout: cell,
4999 len: Len::Count(600),
5000 },
5001 Inst::Clear {
5002 slot: 8,
5003 layout: cell,
5004 },
5005 Inst::Int { dst: 5, value: 1 },
5006 Inst::Int { dst: 6, value: 2 },
5007 // A third cell fits only if the second was reclaimed, and the
5008 // first must survive to be written through afterwards.
5009 Inst::Alloc {
5010 dst: 8,
5011 layout: cell,
5012 len: Len::Count(600),
5013 },
5014 Inst::Int { dst: 9, value: 0 },
5015 Inst::Int {
5016 dst: 4,
5017 value: 4242,
5018 },
5019 Inst::StoreElem {
5020 obj: 7,
5021 index: 9,
5022 src: 4,
5023 layout: int,
5024 },
5025 Inst::LoadElem {
5026 dst: 9,
5027 obj: 7,
5028 index: 9,
5029 layout: int,
5030 },
5031 Inst::Return { src: 9 },
5032 ],
5033 );
5034 let program = build.done();
5035
5036 // The static half, which is the claim `docs/LINEAR_VM.md` makes.
5037 let refs = &program.function(f).refs;
5038 assert!(!refs.is_ref(5), "the `Point`'s x is not a root");
5039 assert!(!refs.is_ref(6), "the `Point`'s y is not a root");
5040 assert!(refs.is_ref(7), "the vector's address is");
5041 assert_eq!(refs.iter().collect::<Vec<_>>(), vec![7, 8]);
5042
5043 // The dynamic half: two cells fit at a time and three do not, so the
5044 // run only finishes because the cleared slot stopped being a root —
5045 // and it finishes with the wrapper's own cell still there to write.
5046 let mut machine = Machine::new(&program, 1600);
5047 assert_eq!(machine.run(f, &[], &budget()).unwrap(), vec![4242]);
5048 assert!(
5049 machine.collected().collections > 0,
5050 "the third cell only fits after the cleared one is reclaimed"
5051 );
5052 }
5053
5054 // ---- closures ------------------------------------------------------
5055
5056 /// The layout of a lambda that reads `captures`.
5057 fn closure_layout(build: &mut Build, function: FunctionId, captures: &[LayoutId]) -> LayoutId {
5058 build.layout(
5059 "closure",
5060 Shape::Closure {
5061 function,
5062 captures: captures.to_vec(),
5063 },
5064 )
5065 }
5066
5067 /// A closure's frame is a callee's frame with two writes rather than one:
5068 /// the arguments into the words the parameters occupy, and then the
5069 /// captures into the slots `Function::captures` names, which are the ones
5070 /// straight after.
5071 #[test]
5072 fn a_closure_call_copies_the_arguments_then_the_captures() {
5073 let mut build = Build::default();
5074 let int = build.scalar(Repr::Int);
5075 // { it -> it + captured }
5076 let add = build.lambda(
5077 "lambda",
5078 &[int],
5079 &[Repr::Int, Repr::Int, Repr::Int],
5080 int,
5081 &[int],
5082 vec![
5083 Inst::Arith {
5084 num: Num::Int,
5085 op: ArithOp::Add,
5086 dst: 2,
5087 a: 0,
5088 b: 1,
5089 },
5090 Inst::Return { src: 2 },
5091 ],
5092 );
5093 let layout = closure_layout(&mut build, add, &[int]);
5094 let args = build.args(&[(3, int)]);
5095 let main = build.function(
5096 "main",
5097 &[],
5098 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Int, Repr::Int],
5099 int,
5100 vec![
5101 Inst::Alloc {
5102 dst: 0,
5103 layout,
5104 len: Len::Fixed,
5105 },
5106 Inst::Int {
5107 dst: 1,
5108 value: add.0 as i64,
5109 },
5110 Inst::StoreField {
5111 obj: 0,
5112 at: 0,
5113 src: 1,
5114 layout: int,
5115 },
5116 Inst::Int { dst: 2, value: 10 },
5117 Inst::StoreField {
5118 obj: 0,
5119 at: 1,
5120 src: 2,
5121 layout: int,
5122 },
5123 Inst::Int { dst: 3, value: 5 },
5124 Inst::CallClosure {
5125 dst: 4,
5126 closure: 0,
5127 args,
5128 result: int,
5129 },
5130 Inst::Return { src: 4 },
5131 ],
5132 );
5133 let program = build.done();
5134 assert_eq!(run(&program, main, &[]).unwrap() as i64, 15);
5135 }
5136
5137 /// A capture is copied into a `Repr::Ref` slot of the callee's frame, so
5138 /// it is a root of that frame like any other — which is what makes a
5139 /// closure need no second story for the collector.
5140 ///
5141 /// The captured object is reachable from nowhere else by the time the call
5142 /// happens: the caller cleared its own slot, and it is not a string, so the
5143 /// interned table is not quietly holding it either. The body then allocates
5144 /// until the heap has to be swept several times over before reading the
5145 /// capture back.
5146 #[test]
5147 fn a_capture_survives_a_collection_in_the_callee() {
5148 let mut build = Build::default();
5149 let int = build.scalar(Repr::Int);
5150 let cell = build.layout(
5151 "Cell",
5152 Shape::Elements {
5153 elem: int,
5154 growable: false,
5155 },
5156 );
5157 let body = build.lambda(
5158 "lambda",
5159 &[],
5160 &[
5161 Repr::Ref,
5162 Repr::Int,
5163 Repr::Int,
5164 Repr::Bool,
5165 Repr::Ref,
5166 Repr::Int,
5167 Repr::Int,
5168 Repr::Int,
5169 ],
5170 int,
5171 &[cell],
5172 vec![
5173 Inst::Int { dst: 2, value: 300 },
5174 Inst::Int { dst: 1, value: 0 },
5175 Inst::Cmp {
5176 on: Compare::Int,
5177 op: CmpOp::Lt,
5178 dst: 3,
5179 a: 1,
5180 b: 2,
5181 },
5182 Inst::BranchFalse { cond: 3, to: 9 },
5183 Inst::Alloc {
5184 dst: 4,
5185 layout: cell,
5186 len: Len::Count(64),
5187 },
5188 Inst::Clear {
5189 slot: 4,
5190 layout: cell,
5191 },
5192 Inst::Int { dst: 5, value: 1 },
5193 Inst::Arith {
5194 num: Num::Int,
5195 op: ArithOp::Add,
5196 dst: 1,
5197 a: 1,
5198 b: 5,
5199 },
5200 Inst::Jump { to: 2 },
5201 Inst::Int { dst: 6, value: 0 },
5202 Inst::LoadElem {
5203 dst: 7,
5204 obj: 0,
5205 index: 6,
5206 layout: int,
5207 },
5208 Inst::Return { src: 7 },
5209 ],
5210 );
5211 let layout = closure_layout(&mut build, body, &[cell]);
5212 let none = build.args(&[]);
5213 let main = build.function(
5214 "main",
5215 &[],
5216 &[
5217 Repr::Ref,
5218 Repr::Ref,
5219 Repr::Int,
5220 Repr::Int,
5221 Repr::Int,
5222 Repr::Int,
5223 ],
5224 int,
5225 vec![
5226 Inst::Alloc {
5227 dst: 1,
5228 layout: cell,
5229 len: Len::Count(1),
5230 },
5231 Inst::Int { dst: 3, value: 0 },
5232 Inst::Int {
5233 dst: 4,
5234 value: 4242,
5235 },
5236 Inst::StoreElem {
5237 obj: 1,
5238 index: 3,
5239 src: 4,
5240 layout: int,
5241 },
5242 Inst::Alloc {
5243 dst: 0,
5244 layout,
5245 len: Len::Fixed,
5246 },
5247 Inst::Int {
5248 dst: 2,
5249 value: body.0 as i64,
5250 },
5251 Inst::StoreField {
5252 obj: 0,
5253 at: 0,
5254 src: 2,
5255 layout: int,
5256 },
5257 Inst::StoreField {
5258 obj: 0,
5259 at: 1,
5260 src: 1,
5261 layout: cell,
5262 },
5263 Inst::Clear {
5264 slot: 1,
5265 layout: cell,
5266 },
5267 Inst::CallClosure {
5268 dst: 5,
5269 closure: 0,
5270 args: none,
5271 result: int,
5272 },
5273 Inst::Return { src: 5 },
5274 ],
5275 );
5276 let program = build.done();
5277 let mut machine = Machine::new(&program, 4096);
5278 assert_eq!(machine.run(main, &[], &budget()).unwrap(), vec![4242]);
5279 assert!(
5280 machine.collected().collections > 0,
5281 "the body is meant to allocate more than the heap holds"
5282 );
5283 }
5284
5285 /// A closure that calls itself through its own capture nests until the
5286 /// reserved stack region is full, and stops there — with the message any
5287 /// other unbounded recursion gets, because it is the same event. No Rust
5288 /// frame is added per turn, so how deep this goes is `STACK_WORDS` and
5289 /// nothing else.
5290 #[test]
5291 fn a_closure_chain_is_bounded_by_the_stack_region() {
5292 let mut build = Build::default();
5293 let int = build.scalar(Repr::Int);
5294 // What the closure captures is the closure, so the capture's layout
5295 // is one reference word rather than the callee's own `Int`.
5296 let held = build.word("captured", Repr::Ref);
5297 let none = build.args(&[]);
5298 // What it answers is never reached, because it never returns.
5299 let body = build.lambda(
5300 "lambda",
5301 &[],
5302 &[Repr::Ref, Repr::Int],
5303 int,
5304 &[held],
5305 vec![
5306 Inst::CallClosure {
5307 dst: 1,
5308 closure: 0,
5309 args: none,
5310 result: int,
5311 },
5312 Inst::Return { src: 1 },
5313 ],
5314 );
5315 let layout = closure_layout(&mut build, body, &[held]);
5316 let main = build.function(
5317 "main",
5318 &[],
5319 &[Repr::Ref, Repr::Int, Repr::Int],
5320 int,
5321 vec![
5322 Inst::Alloc {
5323 dst: 0,
5324 layout,
5325 len: Len::Fixed,
5326 },
5327 Inst::Int {
5328 dst: 1,
5329 value: body.0 as i64,
5330 },
5331 Inst::StoreField {
5332 obj: 0,
5333 at: 0,
5334 src: 1,
5335 layout: int,
5336 },
5337 // The closure captures itself, which is the shortest way to
5338 // write a call chain with no bound on it.
5339 Inst::StoreField {
5340 obj: 0,
5341 at: 1,
5342 src: 0,
5343 layout: held,
5344 },
5345 Inst::CallClosure {
5346 dst: 2,
5347 closure: 0,
5348 args: none,
5349 result: int,
5350 },
5351 Inst::Return { src: 2 },
5352 ],
5353 );
5354 let program = build.done();
5355 let error = run(&program, main, &[]).unwrap_err();
5356 assert_eq!(error.message, "this call nests too deeply");
5357 }
5358
5359 /// `fn main() { spin() }`, where `spin` is a closure whose body never
5360 /// leaves its loop.
5361 ///
5362 /// The caller is four instructions and the fifth enters the closure, so
5363 /// every safepoint after the first handful is one the closure's own frame
5364 /// is executing at.
5365 fn spinning_closure(build: &mut Build) -> FunctionId {
5366 let int = build.scalar(Repr::Int);
5367 let body = build.lambda(
5368 "lambda",
5369 &[],
5370 &[Repr::Int],
5371 int,
5372 &[],
5373 vec![Inst::Int { dst: 0, value: 0 }, Inst::Jump { to: 0 }],
5374 );
5375 let layout = closure_layout(build, body, &[]);
5376 let none = build.args(&[]);
5377 build.function(
5378 "main",
5379 &[],
5380 &[Repr::Ref, Repr::Int, Repr::Int],
5381 int,
5382 vec![
5383 Inst::Alloc {
5384 dst: 0,
5385 layout,
5386 len: Len::Fixed,
5387 },
5388 Inst::Int {
5389 dst: 1,
5390 value: body.0 as i64,
5391 },
5392 Inst::StoreField {
5393 obj: 0,
5394 at: 0,
5395 src: 1,
5396 layout: int,
5397 },
5398 Inst::CallClosure {
5399 dst: 2,
5400 closure: 0,
5401 args: none,
5402 result: int,
5403 },
5404 Inst::Return { src: 2 },
5405 ],
5406 )
5407 }
5408
5409 /// The safepoint is a fact about the loop, not about which frame the loop
5410 /// is in: a run spinning inside a closure is cancelled within one stride
5411 /// exactly as one spinning in its entry is.
5412 #[test]
5413 fn a_cancelled_run_stops_at_a_safepoint_inside_a_closure() {
5414 let mut build = Build::default();
5415 let main = spinning_closure(&mut build);
5416 let program = build.done();
5417 let cancellation = Cancellation::new();
5418 let budget = crate::budget::Budget::with_cancellation(
5419 crate::budget::Limits::default(),
5420 cancellation.clone(),
5421 );
5422 cancellation.cancel();
5423 let mut machine = Machine::new(&program, 1 << 12);
5424 let error = machine.run(main, &[], &budget.meter()).unwrap_err();
5425 assert_eq!(error.message, "execution stopped: the run was cancelled");
5426 assert!(machine.instructions() <= SAFEPOINT_STRIDE + 1);
5427 }
5428
5429 /// And fuel is charged at the same points, so a closure cannot spend a
5430 /// run's budget without the run noticing.
5431 #[test]
5432 fn fuel_runs_out_at_a_safepoint_inside_a_closure() {
5433 let mut build = Build::default();
5434 let main = spinning_closure(&mut build);
5435 let program = build.done();
5436 let budget = crate::budget::Budget::new(crate::budget::Limits {
5437 fuel: Some(2 * SAFEPOINT_STRIDE),
5438 ..Default::default()
5439 });
5440 let mut machine = Machine::new(&program, 1 << 12);
5441 let error = machine.run(main, &[], &budget.meter()).unwrap_err();
5442 assert_eq!(
5443 error.message,
5444 format!(
5445 "execution stopped: fuel budget of {} exhausted",
5446 2 * SAFEPOINT_STRIDE
5447 )
5448 );
5449 assert_eq!(machine.instructions(), 2 * SAFEPOINT_STRIDE);
5450 }
5451
5452 /// The callee comes out of a heap object, so the machine checks that the
5453 /// object is one a call can be made through rather than reading its first
5454 /// word as a function id. Nothing a program can write reaches this; a
5455 /// lowering that did would otherwise push a frame for whichever function
5456 /// the object's first word happened to name.
5457 #[test]
5458 fn a_call_through_something_that_is_not_a_closure_is_refused() {
5459 let mut build = Build::default();
5460 let int = build.scalar(Repr::Int);
5461 let point = build.structure("Point", &[("x", int)]);
5462 let none = build.args(&[]);
5463 let main = build.function(
5464 "main",
5465 &[],
5466 &[Repr::Ref, Repr::Int],
5467 int,
5468 vec![
5469 Inst::Alloc {
5470 dst: 0,
5471 layout: point,
5472 len: Len::Fixed,
5473 },
5474 Inst::CallClosure {
5475 dst: 1,
5476 closure: 0,
5477 args: none,
5478 result: int,
5479 },
5480 Inst::Return { src: 1 },
5481 ],
5482 );
5483 let program = build.done();
5484 let error = run(&program, main, &[]).unwrap_err();
5485 assert_eq!(error.message, "`Point` is not callable");
5486 }
5487
5488 /// A closure object whose captures are not the ones its callee reads is a
5489 /// lowering bug, and copying what it holds would fill the callee's capture
5490 /// slots from whatever follows the object in the heap.
5491 #[test]
5492 fn a_closure_whose_captures_do_not_match_its_callee_is_refused() {
5493 let mut build = Build::default();
5494 let int = build.scalar(Repr::Int);
5495 let none = build.args(&[]);
5496 let body = build.lambda(
5497 "lambda",
5498 &[],
5499 &[Repr::Int, Repr::Int],
5500 int,
5501 &[int, int],
5502 vec![Inst::Return { src: 0 }],
5503 );
5504 // One capture, against a callee that reads two.
5505 let layout = closure_layout(&mut build, body, &[int]);
5506 let main = build.function(
5507 "main",
5508 &[],
5509 &[Repr::Ref, Repr::Int, Repr::Int],
5510 int,
5511 vec![
5512 Inst::Alloc {
5513 dst: 0,
5514 layout,
5515 len: Len::Fixed,
5516 },
5517 Inst::Int {
5518 dst: 1,
5519 value: body.0 as i64,
5520 },
5521 Inst::StoreField {
5522 obj: 0,
5523 at: 0,
5524 src: 1,
5525 layout: int,
5526 },
5527 Inst::CallClosure {
5528 dst: 2,
5529 closure: 0,
5530 args: none,
5531 result: int,
5532 },
5533 Inst::Return { src: 2 },
5534 ],
5535 );
5536 let program = build.done();
5537 let error = run(&program, main, &[]).unwrap_err();
5538 assert_eq!(
5539 error.message,
5540 "this closure and `t.lambda` disagree about its captures: 1 held, 2 read"
5541 );
5542 }
5543
5544 /// `bump(var total)` adds to the caller's own binding rather than to a
5545 /// copy of it. A place is one word holding the address of that binding,
5546 /// and this is the whole of the mechanism.
5547 #[test]
5548 fn a_place_writes_the_callers_own_slot() {
5549 let mut build = Build::default();
5550 let int = build.scalar(Repr::Int);
5551 let unit = build.scalar(Repr::Unit);
5552 let place = build.scalar(Repr::Addr);
5553 let args = build.args(&[(1, place)]);
5554 let bump = build.function(
5555 "bump",
5556 &[place],
5557 &[Repr::Addr, Repr::Int, Repr::Int, Repr::Unit],
5558 unit,
5559 vec![
5560 Inst::Load {
5561 dst: 1,
5562 addr: 0,
5563 layout: int,
5564 },
5565 Inst::Int { dst: 2, value: 1 },
5566 Inst::Arith {
5567 num: Num::Int,
5568 op: ArithOp::Add,
5569 dst: 1,
5570 a: 1,
5571 b: 2,
5572 },
5573 Inst::Store {
5574 addr: 0,
5575 src: 1,
5576 layout: int,
5577 },
5578 Inst::Unit { dst: 3 },
5579 Inst::Return { src: 3 },
5580 ],
5581 );
5582 let caller = build.function(
5583 "main",
5584 &[],
5585 &[Repr::Int, Repr::Addr, Repr::Unit],
5586 int,
5587 vec![
5588 Inst::Int { dst: 0, value: 10 },
5589 Inst::AddrOfSlot { dst: 1, slot: 0 },
5590 Inst::Call {
5591 dst: 2,
5592 callee: bump,
5593 args,
5594 },
5595 Inst::Return { src: 0 },
5596 ],
5597 );
5598 let program = build.done();
5599 assert_eq!(run(&program, caller, &[]).unwrap() as i64, 11);
5600 }
5601
5602 /// A field of a *heap object* is a load and a store; a field of an inline
5603 /// struct is not an instruction at all. This is the first kind, which is
5604 /// what a struct reaches by being the payload of an object.
5605 #[test]
5606 fn an_object_round_trips_through_its_fields() {
5607 let mut build = Build::default();
5608 let int = build.scalar(Repr::Int);
5609 let point = build.structure("Point", &[("x", int), ("y", int)]);
5610 let f = build.function(
5611 "make",
5612 &[],
5613 &[Repr::Ref, Repr::Int, Repr::Int],
5614 int,
5615 vec![
5616 Inst::Alloc {
5617 dst: 0,
5618 layout: point,
5619 len: Len::Fixed,
5620 },
5621 Inst::Int { dst: 1, value: 3 },
5622 Inst::StoreField {
5623 obj: 0,
5624 at: 0,
5625 src: 1,
5626 layout: int,
5627 },
5628 Inst::Int { dst: 1, value: 4 },
5629 Inst::StoreField {
5630 obj: 0,
5631 at: 1,
5632 src: 1,
5633 layout: int,
5634 },
5635 Inst::LoadField {
5636 dst: 1,
5637 obj: 0,
5638 at: 0,
5639 layout: int,
5640 },
5641 Inst::LoadField {
5642 dst: 2,
5643 obj: 0,
5644 at: 1,
5645 layout: int,
5646 },
5647 Inst::Arith {
5648 num: Num::Int,
5649 op: ArithOp::Mul,
5650 dst: 1,
5651 a: 1,
5652 b: 2,
5653 },
5654 Inst::Return { src: 1 },
5655 ],
5656 );
5657 let program = build.done();
5658 assert_eq!(run(&program, f, &[]).unwrap() as i64, 12);
5659 }
5660
5661 /// Reading past an object is a lowering bug, and the machine reports it
5662 /// rather than reading whatever follows the object in the heap.
5663 ///
5664 /// The object reaches the slot it is read out of through a `Copy`, which
5665 /// is what leaves the machine to answer: `cove_ir::verify` refuses this
5666 /// statically wherever it can prove which layout a reference slot holds,
5667 /// and a slot written by a copy holds whatever the source held. Both
5668 /// checks are wanted — the static one catches the bug at lowering time,
5669 /// and this one catches it where the layout is not a static fact.
5670 #[test]
5671 fn a_field_past_the_object_is_refused() {
5672 let mut build = Build::default();
5673 let int = build.scalar(Repr::Int);
5674 let one = build.structure("One", &[("x", int)]);
5675 let held = build.layout("Held", Shape::Word(Repr::Ref));
5676 let f = build.function(
5677 "past",
5678 &[],
5679 &[Repr::Ref, Repr::Int, Repr::Ref],
5680 int,
5681 vec![
5682 Inst::Alloc {
5683 dst: 0,
5684 layout: one,
5685 len: Len::Fixed,
5686 },
5687 Inst::Copy {
5688 dst: 2,
5689 src: 0,
5690 layout: held,
5691 },
5692 Inst::LoadField {
5693 dst: 1,
5694 obj: 2,
5695 at: 3,
5696 layout: int,
5697 },
5698 Inst::Return { src: 1 },
5699 ],
5700 );
5701 let program = build.done();
5702 let error = run(&program, f, &[]).unwrap_err();
5703 assert!(
5704 error.message.contains("word 3 of a `One`"),
5705 "{}",
5706 error.message
5707 );
5708 }
5709
5710 /// The loop allocates in a loop, clearing the slot each turn. Without
5711 /// `Clear` the frame would hold every object it ever made; with it the
5712 /// heap stays flat, and this is the test that says so.
5713 #[test]
5714 fn clearing_a_slot_lets_the_collector_reclaim() {
5715 let mut build = Build::default();
5716 let int = build.scalar(Repr::Int);
5717 let cell = build.layout(
5718 "Cell",
5719 Shape::Elements {
5720 elem: int,
5721 growable: false,
5722 },
5723 );
5724 let f = build.function(
5725 "churn",
5726 &[int],
5727 &[Repr::Int, Repr::Int, Repr::Bool, Repr::Ref, Repr::Int],
5728 int,
5729 vec![
5730 Inst::Int { dst: 1, value: 0 },
5731 Inst::Cmp {
5732 on: Compare::Int,
5733 op: CmpOp::Lt,
5734 dst: 2,
5735 a: 1,
5736 b: 0,
5737 },
5738 Inst::BranchFalse { cond: 2, to: 8 },
5739 Inst::Alloc {
5740 dst: 3,
5741 layout: cell,
5742 len: Len::Count(64),
5743 },
5744 Inst::Clear {
5745 slot: 3,
5746 layout: cell,
5747 },
5748 Inst::Int { dst: 4, value: 1 },
5749 Inst::Arith {
5750 num: Num::Int,
5751 op: ArithOp::Add,
5752 dst: 1,
5753 a: 1,
5754 b: 4,
5755 },
5756 Inst::Jump { to: 1 },
5757 Inst::Return { src: 1 },
5758 ],
5759 );
5760 let program = build.done();
5761 // A heap far smaller than 4000 objects of 65 words: the run only
5762 // finishes because each turn's object is unreachable by the next.
5763 let mut machine = Machine::new(&program, 4096);
5764 let answer = machine.run(f, &[4000], &budget()).unwrap();
5765 assert_eq!(answer, vec![4000]);
5766 assert!(
5767 machine.collected().collections > 0,
5768 "the run should have had to collect"
5769 );
5770 }
5771
5772 #[test]
5773 fn a_string_literal_is_allocated_once() {
5774 let mut build = Build::default().strings(&["hello"]);
5775 let bool_layout = build.scalar(Repr::Bool);
5776 let str_layout = build.layout("String", Shape::Str);
5777 build.program.str_layout = str_layout;
5778 let f = build.function(
5779 "twice",
5780 &[],
5781 &[Repr::Ref, Repr::Ref, Repr::Bool],
5782 bool_layout,
5783 vec![
5784 Inst::Str {
5785 dst: 0,
5786 text: StrId(0),
5787 },
5788 Inst::Str {
5789 dst: 1,
5790 text: StrId(0),
5791 },
5792 Inst::Cmp {
5793 on: Compare::Identity,
5794 op: CmpOp::Eq,
5795 dst: 2,
5796 a: 0,
5797 b: 1,
5798 },
5799 Inst::Return { src: 2 },
5800 ],
5801 );
5802 let program = build.done();
5803 assert_eq!(run(&program, f, &[]).unwrap(), 1);
5804 }
5805
5806 #[test]
5807 fn strings_compare_by_their_bytes() {
5808 let mut build = Build::default().strings(&["apple", "banana"]);
5809 let bool_layout = build.scalar(Repr::Bool);
5810 let str_layout = build.layout("String", Shape::Str);
5811 build.program.str_layout = str_layout;
5812 let f = build.function(
5813 "order",
5814 &[],
5815 &[Repr::Ref, Repr::Ref, Repr::Bool],
5816 bool_layout,
5817 vec![
5818 Inst::Str {
5819 dst: 0,
5820 text: StrId(0),
5821 },
5822 Inst::Str {
5823 dst: 1,
5824 text: StrId(1),
5825 },
5826 Inst::Cmp {
5827 on: Compare::Str,
5828 op: CmpOp::Lt,
5829 dst: 2,
5830 a: 0,
5831 b: 1,
5832 },
5833 Inst::Return { src: 2 },
5834 ],
5835 );
5836 let program = build.done();
5837 assert_eq!(run(&program, f, &[]).unwrap(), 1);
5838 }
5839
5840 // --- ADR 0045: a literal is there before the program runs -------------
5841
5842 /// A literal in a hot loop allocates nothing: `Inst::Str` is a load of a
5843 /// precomputed address, and `Machine::allocated_words` — the figure that
5844 /// used to grow the first time a loop reached the literal — does not
5845 /// move at all once the run has started.
5846 #[test]
5847 fn a_literal_in_a_loop_allocates_nothing() {
5848 let mut build = Build::default().strings(&["hot"]);
5849 let str_layout = build.layout("String", Shape::Str);
5850 build.program.str_layout = str_layout;
5851 let int = build.scalar(Repr::Int);
5852 // fn turns() -> Int { var i = 0; while i < 1000 { let _s = "hot"; i += 1 }; i }
5853 let f = build.function(
5854 "turns",
5855 &[],
5856 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Bool, Repr::Int],
5857 int,
5858 vec![
5859 Inst::Int {
5860 dst: 2,
5861 value: 1000,
5862 },
5863 Inst::Int { dst: 1, value: 0 },
5864 Inst::Cmp {
5865 on: Compare::Int,
5866 op: CmpOp::Lt,
5867 dst: 3,
5868 a: 1,
5869 b: 2,
5870 },
5871 Inst::BranchFalse { cond: 3, to: 8 },
5872 Inst::Str {
5873 dst: 0,
5874 text: StrId(0),
5875 },
5876 Inst::Int { dst: 4, value: 1 },
5877 Inst::Arith {
5878 num: Num::Int,
5879 op: ArithOp::Add,
5880 dst: 1,
5881 a: 1,
5882 b: 4,
5883 },
5884 Inst::Jump { to: 2 },
5885 Inst::Return { src: 1 },
5886 ],
5887 );
5888 let program = build.done();
5889 let mut machine = Machine::new(&program, 1 << 16);
5890 let before = machine.allocated_words();
5891 assert_eq!(machine.run(f, &[], &budget()).unwrap(), vec![1000]);
5892 assert_eq!(
5893 machine.allocated_words(),
5894 before,
5895 "a thousand turns through `str` allocated nothing beyond the literal's own placement"
5896 );
5897 assert_eq!(
5898 machine.collected().collections,
5899 0,
5900 "nothing here ever had reason to collect"
5901 );
5902 }
5903
5904 /// An unused literal is still built: `Machine::place_literals` places
5905 /// every entry of `Program::strings`, not only the ones an entry
5906 /// happens to load. Today an unused literal costs nothing; under ADR
5907 /// 0045 it costs its bytes, which is the regression the ADR names
5908 /// rather than hides.
5909 #[test]
5910 fn an_unused_literal_is_still_built() {
5911 let mut build = Build::default().strings(&["never loaded"]);
5912 let str_layout = build.layout("String", Shape::Str);
5913 build.program.str_layout = str_layout;
5914 let int = build.scalar(Repr::Int);
5915 // `f`'s body never mentions `StrId(0)` — there is no `Inst::Str` in
5916 // it at all — and the literal is placed before `f` can run anyway.
5917 let f = build.function(
5918 "f",
5919 &[],
5920 &[Repr::Int],
5921 int,
5922 vec![Inst::Int { dst: 0, value: 0 }, Inst::Return { src: 0 }],
5923 );
5924 let program = build.done();
5925 let mut machine = Machine::new(&program, 1 << 12);
5926 let addr = machine.literal_addr(StrId(0));
5927 assert_ne!(
5928 addr, 0,
5929 "the literal was placed even though nothing loads it"
5930 );
5931 assert_eq!(machine.string_bytes(addr), b"never loaded");
5932 // And `f` runs exactly as it would if the literal did not exist:
5933 // placing it ahead of time changes nothing this body can observe.
5934 assert_eq!(machine.run(f, &[], &budget()).unwrap(), vec![0]);
5935 }
5936
5937 /// Literals are never collected, rooted or not — the floor's whole job.
5938 /// A sweep begins at `Space::static_end` and never walks below it, so
5939 /// nothing there can be found unmarked and freed, whether or not any
5940 /// frame, slot or temp names it. This machine has none of those at all.
5941 #[test]
5942 fn a_literal_survives_a_collection_that_roots_nothing() {
5943 let mut build = Build::default().strings(&["kept forever"]);
5944 let str_layout = build.layout("String", Shape::Str);
5945 build.program.str_layout = str_layout;
5946 let program = build.done();
5947 let mut machine = Machine::new(&program, 1 << 12);
5948 let addr = machine.literal_addr(StrId(0));
5949 machine.collect();
5950 assert_eq!(
5951 machine.string_bytes(addr),
5952 b"kept forever",
5953 "a collection that roots nothing still leaves the literal untouched"
5954 );
5955 // Surviving and being counted as surviving are two claims, and the
5956 // second is the one a floor can quietly lose: the sweep starts above
5957 // the literal, so nothing marks it and nothing adds it up unless the
5958 // static region is counted whole. `freed + live` is what says how
5959 // much was occupied when the collection began.
5960 let words = 1 + machine
5961 .program
5962 .layout(machine.program.str_layout)
5963 .try_payload_words(b"kept forever".len() as u32, &machine.program.layouts)
5964 .expect("a twelve-byte string has a payload");
5965 assert_eq!(
5966 machine.collected().live_words,
5967 u64::from(words),
5968 "the literal's header and payload are live words, unwalked or not"
5969 );
5970 assert_eq!(machine.collected().freed_words, 0);
5971 }
5972
5973 /// A heap object holding a reference to a literal survives a collection
5974 /// that reclaims everything else, and reads the reference back
5975 /// correctly: tracing *through* a reference to a placed literal already
5976 /// worked before this ADR — `reachable` accepts anything below `bump` —
5977 /// and this is what confirms it still does.
5978 #[test]
5979 fn a_struct_field_holding_a_literal_survives_a_collection() {
5980 let mut build = Build::default().strings(&["held by a field"]);
5981 let str_layout = build.layout("String", Shape::Str);
5982 build.program.str_layout = str_layout;
5983 let holder = build.structure("Holder", &[("text", str_layout)]);
5984 let program = build.done();
5985 let mut machine = Machine::new(&program, 1 << 12);
5986 let literal = machine.literal_addr(StrId(0));
5987
5988 let object = machine.new_object(holder, 0).expect("the heap has room");
5989 machine.set_payload(object, 0, literal);
5990 // The struct's own address is the only root; the literal is
5991 // reachable only by tracing through it.
5992 machine.push_temp(object);
5993
5994 machine.collect();
5995
5996 assert_eq!(machine.mem.payload(object, 0), literal);
5997 assert_eq!(machine.string_bytes(literal), b"held by a field");
5998 }
5999
6000 /// Two tasks of one run see the same address for one literal: the
6001 /// entry places it once, and `Machine::for_task` receives the table
6002 /// rather than building its own — which is the whole of the per-task
6003 /// duplication this ADR removes.
6004 #[test]
6005 fn two_tasks_of_one_run_see_the_same_address_for_a_literal() {
6006 let mut build = Build::default().strings(&["shared"]);
6007 let str_layout = build.layout("String", Shape::Str);
6008 build.program.str_layout = str_layout;
6009 let program = build.done();
6010
6011 let entry = Machine::new(&program, 1 << 12);
6012 let entry_addr = entry.literal_addr(StrId(0));
6013
6014 let segment = entry
6015 .mem
6016 .for_task()
6017 .expect("a fresh space has a segment free");
6018 let task = Machine::for_task(
6019 &program,
6020 None,
6021 None,
6022 Arc::clone(&entry.resources),
6023 segment,
6024 Cancellation::new(),
6025 1,
6026 entry.code().expect("this fixture encodes"),
6027 entry.literals().expect("the literals placed"),
6028 Arc::clone(&entry.widths),
6029 );
6030
6031 assert_eq!(
6032 task.literal_addr(StrId(0)),
6033 entry_addr,
6034 "no second placement, no second address"
6035 );
6036 assert_eq!(task.string_bytes(entry_addr), b"shared");
6037 }
6038
6039 /// The eager-placement failure, end to end: a heap too small for the
6040 /// program's literals fails before the entry runs, at the entry's own
6041 /// span, with the message an exhausted heap already gives, and no fuel
6042 /// charged — this program executed nothing.
6043 #[test]
6044 fn a_heap_too_small_for_its_literals_fails_before_the_entry_runs() {
6045 let mut build = Build::default().strings(&["far too long for the heap this run was given"]);
6046 let str_layout = build.layout("String", Shape::Str);
6047 build.program.str_layout = str_layout;
6048 let int = build.scalar(Repr::Int);
6049 let f = build.function(
6050 "f",
6051 &[],
6052 &[Repr::Int],
6053 int,
6054 vec![Inst::Int { dst: 0, value: 0 }, Inst::Return { src: 0 }],
6055 );
6056 let program = build.done();
6057 // Far fewer words than the literal's own header and payload, so
6058 // placing it is what fails — nothing in `f`'s body ever runs.
6059 let mut machine = Machine::new(&program, 4);
6060 let meter = budget();
6061 let error = machine
6062 .run(f, &[], &meter)
6063 .expect_err("a heap this small cannot hold the literal");
6064 assert_eq!(error.message, "this run has no memory left");
6065 assert_eq!(
6066 error.span,
6067 Some(program.function(f).span),
6068 "the entry's own span, not the literal's"
6069 );
6070 assert_eq!(
6071 meter.fuel_spent(),
6072 0,
6073 "a run that executed nothing is charged nothing"
6074 );
6075 }
6076
6077 /// A box carries the *layout* of what it holds in payload word 0, not a
6078 /// per-word `Repr`: erasure is where a value stops having a static width,
6079 /// so what the box has to record is the thing that says the width.
6080 #[test]
6081 fn a_box_answers_the_layout_it_holds_and_refuses_another() {
6082 let mut build = Build::default();
6083 let int = build.scalar(Repr::Int);
6084 let boolean = build.scalar(Repr::Bool);
6085 build.boxed();
6086 let good = build.function(
6087 "round-trip",
6088 &[int],
6089 &[Repr::Int, Repr::Ref, Repr::Int],
6090 int,
6091 vec![
6092 Inst::Box {
6093 dst: 1,
6094 src: 0,
6095 layout: int,
6096 },
6097 Inst::Unbox {
6098 dst: 2,
6099 src: 1,
6100 layout: int,
6101 },
6102 Inst::Return { src: 2 },
6103 ],
6104 );
6105 let wrong = build.function(
6106 "wrong-type",
6107 &[int],
6108 &[Repr::Int, Repr::Ref, Repr::Bool],
6109 boolean,
6110 vec![
6111 Inst::Box {
6112 dst: 1,
6113 src: 0,
6114 layout: int,
6115 },
6116 Inst::Unbox {
6117 dst: 2,
6118 src: 1,
6119 layout: boolean,
6120 },
6121 Inst::Return { src: 2 },
6122 ],
6123 );
6124 let program = build.done();
6125 assert_eq!(run(&program, good, &[7]).unwrap() as i64, 7);
6126 assert_eq!(
6127 run(&program, wrong, &[7]).unwrap_err().message,
6128 "this value is not of the type it is being read as"
6129 );
6130 }
6131
6132 /// A boxed `Point` is a two-word payload rather than a reference to
6133 /// somewhere else again: the object holds the `LayoutId` in payload word
6134 /// 0 and the value's words after it, and the header's `len` is that
6135 /// value's width, because a `Boxed` layout cannot know it.
6136 ///
6137 /// So an `Unbox` at the wrong layout is refused for the same reason it is
6138 /// on a scalar — the word the box carries is a layout and the layouts do
6139 /// not match — and nothing about the width had to be guessed.
6140 #[test]
6141 fn a_box_holds_a_multiword_value_inline() {
6142 let mut build = Build::default();
6143 let int = build.scalar(Repr::Int);
6144 let point = build.structure("Point", &[("x", int), ("y", int)]);
6145 build.boxed();
6146 let round_trip = build.function(
6147 "round-trip",
6148 &[],
6149 &[Repr::Int, Repr::Int, Repr::Ref, Repr::Int, Repr::Int],
6150 point,
6151 vec![
6152 Inst::Int { dst: 0, value: 3 },
6153 Inst::Int { dst: 1, value: 4 },
6154 Inst::Box {
6155 dst: 2,
6156 src: 0,
6157 layout: point,
6158 },
6159 Inst::Unbox {
6160 dst: 3,
6161 src: 2,
6162 layout: point,
6163 },
6164 Inst::Return { src: 3 },
6165 ],
6166 );
6167 let width = build.function(
6168 "width",
6169 &[],
6170 &[Repr::Int, Repr::Int, Repr::Ref, Repr::Int],
6171 int,
6172 vec![
6173 Inst::Int { dst: 0, value: 3 },
6174 Inst::Int { dst: 1, value: 4 },
6175 Inst::Box {
6176 dst: 2,
6177 src: 0,
6178 layout: point,
6179 },
6180 Inst::Len { dst: 3, obj: 2 },
6181 Inst::Return { src: 3 },
6182 ],
6183 );
6184 let wrong = build.function(
6185 "wrong-layout",
6186 &[],
6187 &[Repr::Int, Repr::Int, Repr::Ref, Repr::Int],
6188 int,
6189 vec![
6190 Inst::Int { dst: 0, value: 3 },
6191 Inst::Int { dst: 1, value: 4 },
6192 Inst::Box {
6193 dst: 2,
6194 src: 0,
6195 layout: point,
6196 },
6197 Inst::Unbox {
6198 dst: 3,
6199 src: 2,
6200 layout: int,
6201 },
6202 Inst::Return { src: 3 },
6203 ],
6204 );
6205 let program = build.done();
6206 assert_eq!(run_words(&program, round_trip, &[]).unwrap(), vec![3, 4]);
6207 assert_eq!(run(&program, width, &[]).unwrap(), 2);
6208 assert_eq!(
6209 run(&program, wrong, &[]).unwrap_err().message,
6210 "this value is not of the type it is being read as"
6211 );
6212 }
6213
6214 #[test]
6215 fn a_switch_picks_a_case_and_falls_to_its_default() {
6216 let mut build = Build::default();
6217 let int = build.scalar(Repr::Int);
6218 let table = build.table(&[3, 5], 7);
6219 let f = build.function(
6220 "pick",
6221 &[int],
6222 &[Repr::Int, Repr::Int],
6223 int,
6224 vec![
6225 Inst::Switch { on: 0, table },
6226 Inst::Int { dst: 1, value: 0 },
6227 Inst::Return { src: 1 },
6228 Inst::Int { dst: 1, value: 10 },
6229 Inst::Return { src: 1 },
6230 Inst::Int { dst: 1, value: 20 },
6231 Inst::Return { src: 1 },
6232 Inst::Int { dst: 1, value: 30 },
6233 Inst::Return { src: 1 },
6234 ],
6235 );
6236 let program = build.done();
6237 assert_eq!(run(&program, f, &[0]).unwrap() as i64, 10);
6238 assert_eq!(run(&program, f, &[1]).unwrap() as i64, 20);
6239 assert_eq!(run(&program, f, &[9]).unwrap() as i64, 30);
6240 }
6241
6242 // ---- the host boundary -------------------------------------------
6243
6244 /// A host with one operation of each kind of argument the boundary has
6245 /// to move: a scalar in and out, and a string in and out.
6246 ///
6247 /// Written here rather than reused from a shipped module because what is
6248 /// under test is the *instruction*: `console.println` would drag in a
6249 /// grant table, an output stream and a schema written for a different
6250 /// purpose, and a failure would take a paragraph to attribute.
6251 struct Probe;
6252
6253 static PROBE_OPS: &[cove_schema::OperationSchema] = &[
6254 cove_schema::OperationSchema {
6255 name: "double",
6256 params: &[cove_schema::HostType::Int],
6257 variadic: false,
6258 result: cove_schema::HostType::Int,
6259 capability: "probe",
6260 effect: cove_schema::Effect::Read,
6261 cancellable: false,
6262 recordable: true,
6263 result_is_task_safe: true,
6264 },
6265 cove_schema::OperationSchema {
6266 name: "shout",
6267 params: &[cove_schema::HostType::String],
6268 variadic: false,
6269 result: cove_schema::HostType::String,
6270 capability: "probe",
6271 effect: cove_schema::Effect::Read,
6272 cancellable: false,
6273 recordable: true,
6274 result_is_task_safe: true,
6275 },
6276 ];
6277
6278 impl crate::host::HostApi for Probe {
6279 fn module_schema(&self) -> cove_schema::ModuleSchema {
6280 cove_schema::ModuleSchema {
6281 name: "probe",
6282 capability: "probe",
6283 operations: PROBE_OPS,
6284 types: &[],
6285 resources: &[],
6286 }
6287 }
6288
6289 fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
6290 match op {
6291 "double" => Ok(Value::int(
6292 args[0].as_int().expect("the schema holds it") * 2,
6293 )),
6294 "shout" => Ok(Value::string(format!(
6295 "{}!",
6296 args[0].as_str().expect("the schema holds it")
6297 ))),
6298 other => Err(RuntimeError::new(format!("no `{other}` here"))),
6299 }
6300 }
6301 }
6302
6303 fn probing(granted: bool) -> crate::host::HostRegistry {
6304 let grants = if granted {
6305 crate::host::Grants::new(["probe"])
6306 } else {
6307 crate::host::Grants::new(Vec::<String>::new())
6308 };
6309 let mut hosts = crate::host::HostRegistry::new(grants);
6310 hosts.register(Box::new(Probe));
6311 hosts
6312 }
6313
6314 /// `fn f(n) { probe.double(n) }`, in the IR.
6315 fn calls_double(build: &mut Build) -> FunctionId {
6316 let int = build.scalar(Repr::Int);
6317 build.program.host_ops.push(cove_ir::HostOp {
6318 resource: None,
6319 module: Arc::from("probe"),
6320 operation: Arc::from("double"),
6321 result: int,
6322 });
6323 let op = cove_ir::HostOpId(build.program.host_ops.len() as u32 - 1);
6324 let args = build.args(&[(0, int)]);
6325 build.function(
6326 "f",
6327 &[int],
6328 &[Repr::Int, Repr::Int],
6329 int,
6330 vec![Inst::CallHost { dst: 1, op, args }, Inst::Return { src: 1 }],
6331 )
6332 }
6333
6334 #[test]
6335 fn a_host_call_moves_a_word_out_and_the_answer_back() {
6336 let mut build = Build::default();
6337 let f = calls_double(&mut build);
6338 let program = build.done();
6339 let hosts = probing(true);
6340 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6341 assert_eq!(machine.run(f, &[21], &budget()).unwrap(), vec![42]);
6342 }
6343
6344 /// A string argument and a string answer, which is the case that
6345 /// allocates on both sides of the boundary.
6346 #[test]
6347 fn a_host_call_carries_strings_in_and_out() {
6348 let mut build = Build::default().strings(&["hey"]);
6349 let str_layout = build.layout("String", Shape::Str);
6350 build.program.str_layout = str_layout;
6351 build.program.host_ops.push(cove_ir::HostOp {
6352 resource: None,
6353 module: Arc::from("probe"),
6354 operation: Arc::from("shout"),
6355 result: str_layout,
6356 });
6357 let op = cove_ir::HostOpId(0);
6358 let args = build.args(&[(0, str_layout)]);
6359 let f = build.function(
6360 "f",
6361 &[],
6362 &[Repr::Ref, Repr::Ref],
6363 str_layout,
6364 vec![
6365 Inst::Str {
6366 dst: 0,
6367 text: StrId(0),
6368 },
6369 Inst::CallHost { dst: 1, op, args },
6370 Inst::Return { src: 1 },
6371 ],
6372 );
6373 let program = build.done();
6374 let hosts = probing(true);
6375 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6376 let words = machine.run(f, &[], &budget()).unwrap();
6377 assert_eq!(
6378 String::from_utf8(machine.string_bytes(words[0])).unwrap(),
6379 "hey!"
6380 );
6381 }
6382
6383 /// The boundary refuses an ungranted capability, and it is the boundary
6384 /// that does it: the machine passes the call on and reports what came
6385 /// back, classification included.
6386 #[test]
6387 fn an_ungranted_call_is_refused_at_the_boundary() {
6388 let mut build = Build::default();
6389 let f = calls_double(&mut build);
6390 let program = build.done();
6391 let hosts = probing(false);
6392 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6393 let error = machine.run(f, &[1], &budget()).unwrap_err();
6394 assert!(
6395 error.message.contains("probe"),
6396 "the refusal names the capability: {}",
6397 error.message
6398 );
6399 assert_eq!(error.denied_capability.as_deref(), Some("probe"));
6400 assert_eq!(error.outcome, crate::trace::RunOutcome::HostBoundary);
6401 }
6402
6403 /// The host-call limit is charged inside the boundary, which is where the
6404 /// oracle charges it too — `Budget::charge_host_call`, once per call,
6405 /// before the host is reached.
6406 #[test]
6407 fn a_host_call_is_charged_the_way_the_oracle_charges_it() {
6408 let mut build = Build::default();
6409 let int = build.scalar(Repr::Int);
6410 build.program.host_ops.push(cove_ir::HostOp {
6411 resource: None,
6412 module: Arc::from("probe"),
6413 operation: Arc::from("double"),
6414 result: int,
6415 });
6416 let op = cove_ir::HostOpId(0);
6417 let args = build.args(&[(0, int)]);
6418 let f = build.function(
6419 "f",
6420 &[int],
6421 &[Repr::Int, Repr::Int],
6422 int,
6423 vec![
6424 Inst::CallHost { dst: 1, op, args },
6425 Inst::CallHost { dst: 1, op, args },
6426 Inst::Return { src: 1 },
6427 ],
6428 );
6429 let program = build.done();
6430 let mut hosts = probing(true);
6431 let limits = crate::budget::Limits {
6432 max_host_calls: Some(1),
6433 ..Default::default()
6434 };
6435 let budget = crate::budget::Budget::new(limits);
6436 let meter = budget.meter();
6437 hosts.set_budget(budget);
6438 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6439 let error = machine.run(f, &[2], &meter).unwrap_err();
6440 assert_eq!(
6441 error.message,
6442 "execution stopped: host-call limit of 1 exceeded"
6443 );
6444 // Two, not one: the boundary counts the call it is about to make
6445 // and then refuses it for being past the limit. That is the shared
6446 // counter doing what it does for every backend, which is the point —
6447 // nothing here keeps a count of its own.
6448 assert_eq!(hosts.with_budget(|budget| budget.host_calls()), Some(2));
6449 }
6450
6451 /// A machine with no host behind it says what is missing rather than
6452 /// answering as if the call had happened.
6453 #[test]
6454 fn a_host_call_with_no_boundary_says_what_is_missing() {
6455 let mut build = Build::default();
6456 let f = calls_double(&mut build);
6457 let program = build.done();
6458 let mut machine = Machine::new(&program, 1 << 12);
6459 let error = machine.run(f, &[1], &budget()).unwrap_err();
6460 assert_eq!(
6461 error.message,
6462 "`probe.double` cannot be called, because this run has no host boundary"
6463 );
6464 }
6465
6466 // ---- host resources ------------------------------------------------
6467
6468 /// A host that issues a resource and takes one back.
6469 ///
6470 /// The two directions a `Repr::Host` word has to move, and nothing else:
6471 /// `open` answers a handle the way `files.open(path)` answers a
6472 /// `files.Reader`, and `read` is handed one back the way
6473 /// `files.read(reader)` is. It counts what it has opened, so two readers
6474 /// are two resources and `read` answering the id says which one arrived.
6475 #[derive(Default)]
6476 struct Vault {
6477 opened: std::sync::atomic::AtomicU64,
6478 }
6479
6480 static VAULT_RESOURCES: &[cove_schema::ResourceSchema] = &[cove_schema::ResourceSchema {
6481 name: "Reader",
6482 task_safe: true,
6483 operations: &[],
6484 }];
6485
6486 static VAULT_OPS: &[cove_schema::OperationSchema] = &[
6487 cove_schema::OperationSchema {
6488 name: "open",
6489 params: &[],
6490 variadic: false,
6491 result: cove_schema::HostType::Named("vault.Reader"),
6492 capability: "vault",
6493 effect: cove_schema::Effect::Read,
6494 cancellable: false,
6495 recordable: true,
6496 result_is_task_safe: true,
6497 },
6498 cove_schema::OperationSchema {
6499 name: "read",
6500 params: &[cove_schema::HostType::Named("vault.Reader")],
6501 variadic: false,
6502 result: cove_schema::HostType::Int,
6503 capability: "vault",
6504 effect: cove_schema::Effect::Read,
6505 cancellable: false,
6506 recordable: true,
6507 result_is_task_safe: true,
6508 },
6509 ];
6510
6511 impl crate::host::HostApi for Vault {
6512 fn module_schema(&self) -> cove_schema::ModuleSchema {
6513 cove_schema::ModuleSchema {
6514 name: "vault",
6515 capability: "vault",
6516 operations: VAULT_OPS,
6517 types: &[],
6518 resources: VAULT_RESOURCES,
6519 }
6520 }
6521
6522 fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
6523 match op {
6524 // Counting upward and never reusing, which is the rule
6525 // ADR 0013 puts on an identity.
6526 "open" => {
6527 let id = self
6528 .opened
6529 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
6530 + 1;
6531 Ok(Value::from_resource(ResourceHandle::new(
6532 "vault",
6533 &VAULT_RESOURCES[0],
6534 id,
6535 )))
6536 }
6537 // The host recognises its own resource, which is the whole
6538 // point of the name crossing back: nothing about the word
6539 // reached here.
6540 "read" => Ok(Value::int(
6541 args[0].resource().expect("the schema holds it").id as i64,
6542 )),
6543 other => Err(RuntimeError::new(format!("no `{other}` here"))),
6544 }
6545 }
6546 }
6547
6548 fn vault() -> crate::host::HostRegistry {
6549 let mut hosts = crate::host::HostRegistry::new(crate::host::Grants::new(["vault"]));
6550 hosts.register(Box::new(Vault::default()));
6551 hosts
6552 }
6553
6554 /// The two host operations, and the one-word family a handle occupies.
6555 fn vault_ops(build: &mut Build) -> (LayoutId, cove_ir::HostOpId, cove_ir::HostOpId) {
6556 let int = build.scalar(Repr::Int);
6557 let reader = build.word("vault.Reader", Repr::Host);
6558 build.program.host_ops.push(cove_ir::HostOp {
6559 resource: None,
6560 module: Arc::from("vault"),
6561 operation: Arc::from("open"),
6562 result: reader,
6563 });
6564 build.program.host_ops.push(cove_ir::HostOp {
6565 resource: None,
6566 module: Arc::from("vault"),
6567 operation: Arc::from("read"),
6568 result: int,
6569 });
6570 let ops = build.program.host_ops.len() as u32;
6571 (
6572 reader,
6573 cove_ir::HostOpId(ops - 2),
6574 cove_ir::HostOpId(ops - 1),
6575 )
6576 }
6577
6578 /// A host operation whose result is a resource writes the word, not a
6579 /// boxed value: the answer is a value location of one `Repr::Host` slot,
6580 /// and `Inst::CallHost` copies its words into the frame exactly as it
6581 /// does for an `Int`. Nothing about the instruction knows a resource is
6582 /// different.
6583 #[test]
6584 fn a_host_call_answering_a_resource_writes_the_word() {
6585 let mut build = Build::default();
6586 let (reader, open, _) = vault_ops(&mut build);
6587 let args = build.args(&[]);
6588 let f = build.function(
6589 "f",
6590 &[],
6591 &[Repr::Host],
6592 reader,
6593 vec![
6594 Inst::CallHost {
6595 dst: 0,
6596 op: open,
6597 args,
6598 },
6599 Inst::Return { src: 0 },
6600 ],
6601 );
6602 let program = build.done();
6603 let hosts = vault();
6604 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6605
6606 let answer = machine.run(f, &[], &budget()).unwrap();
6607 assert_eq!(
6608 answer.len(),
6609 1,
6610 "a handle is a name, and a name is one word"
6611 );
6612 assert_eq!(
6613 machine
6614 .resource(answer[0])
6615 .map(|handle| handle.to_string())
6616 .as_deref(),
6617 Some("vault.Reader#1"),
6618 "the word indexes the run's table, and the table holds the name"
6619 );
6620 assert_eq!(
6621 machine.allocated_words(),
6622 0,
6623 "a resource is not an object, so nothing was allocated to hold one"
6624 );
6625 }
6626
6627 /// A resource goes back to the host that issued it, by the name it was
6628 /// issued under. The host is what recognises it; the word never left.
6629 #[test]
6630 fn a_resource_goes_back_to_the_host_that_issued_it() {
6631 let mut build = Build::default();
6632 let (reader, open, read) = vault_ops(&mut build);
6633 let int = build.scalar(Repr::Int);
6634 let none = build.args(&[]);
6635 let one = build.args(&[(0, reader)]);
6636 let f = build.function(
6637 "f",
6638 &[],
6639 &[Repr::Host, Repr::Host, Repr::Int],
6640 int,
6641 vec![
6642 Inst::CallHost {
6643 dst: 0,
6644 op: open,
6645 args: none,
6646 },
6647 // A second resource, so that an answer of `1` is the first
6648 // reader rather than whatever the table happened to hold.
6649 Inst::CallHost {
6650 dst: 1,
6651 op: open,
6652 args: none,
6653 },
6654 Inst::CallHost {
6655 dst: 2,
6656 op: read,
6657 args: one,
6658 },
6659 Inst::Return { src: 2 },
6660 ],
6661 );
6662 let program = build.done();
6663 let hosts = vault();
6664 let mut machine = Machine::with_hosts(&program, 1 << 12, Some(&hosts));
6665 assert_eq!(machine.run(f, &[], &budget()).unwrap(), vec![1]);
6666 }
6667
6668 /// A frame holding a resource across a collection keeps it, and the
6669 /// collector never sees it.
6670 ///
6671 /// Both halves are the claim. The static one is that `Function::refs` —
6672 /// which is `RefMap::of` the frame's `Repr`s — does not name the `Host`
6673 /// slot, so the one pass the collector makes over a frame does not read
6674 /// it. The dynamic one is that the run still gets the right resource back
6675 /// afterwards: the word is untouched, the table is not swept, and the
6676 /// handle it indexes is the one the host issued.
6677 #[test]
6678 fn a_resource_in_a_frame_survives_a_collection_and_is_not_a_root() {
6679 let mut build = Build::default();
6680 let (reader, open, read) = vault_ops(&mut build);
6681 let int = build.scalar(Repr::Int);
6682 let cell = build.layout(
6683 "Cell",
6684 Shape::Elements {
6685 elem: int,
6686 growable: false,
6687 },
6688 );
6689 let none = build.args(&[]);
6690 let one = build.args(&[(0, reader)]);
6691 let f = build.function(
6692 "f",
6693 &[],
6694 &[Repr::Host, Repr::Ref, Repr::Ref, Repr::Int],
6695 int,
6696 vec![
6697 Inst::CallHost {
6698 dst: 0,
6699 op: open,
6700 args: none,
6701 },
6702 Inst::Alloc {
6703 dst: 1,
6704 layout: cell,
6705 len: Len::Count(600),
6706 },
6707 // The cell's last use. A second one fits only if this one is
6708 // reclaimed, which is what makes the collection happen with
6709 // the resource live in slot 0.
6710 Inst::Clear {
6711 slot: 1,
6712 layout: cell,
6713 },
6714 Inst::Alloc {
6715 dst: 2,
6716 layout: cell,
6717 len: Len::Count(600),
6718 },
6719 Inst::CallHost {
6720 dst: 3,
6721 op: read,
6722 args: one,
6723 },
6724 Inst::Return { src: 3 },
6725 ],
6726 );
6727 let program = build.done();
6728
6729 // The static half: the collector's one question about a slot, asked
6730 // of the map it actually reads.
6731 let refs = &program.function(f).refs;
6732 assert!(!refs.is_ref(0), "a host word is not a root");
6733 assert_eq!(refs.iter().collect::<Vec<_>>(), vec![1, 2]);
6734
6735 let hosts = vault();
6736 let mut machine = Machine::with_hosts(&program, 1000, Some(&hosts));
6737 assert_eq!(machine.run(f, &[], &budget()).unwrap(), vec![1]);
6738 assert!(
6739 machine.collected().collections > 0,
6740 "the second cell only fits after the first is reclaimed"
6741 );
6742 }
6743
6744 /// A run that will not stop on its own is stopped by its budget, and the
6745 /// stride is what bounds how long that takes.
6746 #[test]
6747 fn a_cancelled_run_stops_at_a_safepoint() {
6748 let mut build = Build::default();
6749 let int = build.scalar(Repr::Int);
6750 let f = build.function(
6751 "spin",
6752 &[],
6753 &[Repr::Int],
6754 int,
6755 vec![Inst::Int { dst: 0, value: 0 }, Inst::Jump { to: 0 }],
6756 );
6757 let program = build.done();
6758 let cancellation = Cancellation::new();
6759 let budget = crate::budget::Budget::with_cancellation(
6760 crate::budget::Limits::default(),
6761 cancellation.clone(),
6762 );
6763 cancellation.cancel();
6764 let mut machine = Machine::new(&program, 1 << 12);
6765 assert!(machine.run(f, &[], &budget.meter()).is_err());
6766 assert!(machine.instructions() <= SAFEPOINT_STRIDE + 1);
6767 }
6768 // ---- tasks ---------------------------------------------------------------
6769
6770 /// A `Repr::Scope` slot, a `Repr::Task` slot, and the three scratch words
6771 /// every fixture below wants.
6772 ///
6773 /// Written once because what is under test is the scheduler and not the
6774 /// arithmetic around it: every one of these programs opens a scope,
6775 /// builds a closure environment, spawns it, and leaves the scope, and the
6776 /// only thing that differs is what the body does.
6777 fn counter(build: &mut Build) -> LayoutId {
6778 let int = build.scalar(Repr::Int);
6779 build.structure("Counter", &[("n", int)])
6780 }
6781
6782 /// The instructions that fill a closure environment naming `body` and
6783 /// capturing the object in `held`, into slot `dst`, using `scratch`.
6784 fn close_over(
6785 build: &mut Build,
6786 layout: LayoutId,
6787 body: FunctionId,
6788 dst: Slot,
6789 scratch: Slot,
6790 held: Option<Slot>,
6791 ) -> Vec<Inst> {
6792 let int = build.scalar(Repr::Int);
6793 let word = build.scalar(Repr::Ref);
6794 let mut code = vec![
6795 Inst::Alloc {
6796 dst,
6797 layout,
6798 len: Len::Fixed,
6799 },
6800 Inst::Int {
6801 dst: scratch,
6802 value: body.0 as i64,
6803 },
6804 Inst::StoreField {
6805 obj: dst,
6806 at: 0,
6807 src: scratch,
6808 layout: int,
6809 },
6810 ];
6811 if let Some(src) = held {
6812 code.push(Inst::StoreField {
6813 obj: dst,
6814 at: 1,
6815 src,
6816 layout: word,
6817 });
6818 }
6819 code
6820 }
6821
6822 /// Leaving a scope waits for a task the body never awaited.
6823 ///
6824 /// The Language Card's sentence, measured the only way a machine can
6825 /// measure it: the child spends fifty thousand turns before it writes,
6826 /// and the parent reads the write. A `ScopeLeave` that did not join would
6827 /// read the zero the allocation left.
6828 #[test]
6829 fn leaving_a_scope_waits_for_a_task_the_body_never_awaited() {
6830 let mut build = Build::default().strings(&["tasks"]);
6831 let str_layout = build.layout("String", Shape::Str);
6832 build.program.str_layout = str_layout;
6833 let int = build.scalar(Repr::Int);
6834 let word = build.scalar(Repr::Ref);
6835 let held = counter(&mut build);
6836 let body = build.lambda(
6837 "body",
6838 &[],
6839 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Bool, Repr::Int],
6840 int,
6841 &[word],
6842 vec![
6843 Inst::Int { dst: 1, value: 0 },
6844 Inst::Int {
6845 dst: 2,
6846 value: 50_000,
6847 },
6848 Inst::Int { dst: 4, value: 1 },
6849 Inst::Cmp {
6850 on: Compare::Int,
6851 op: CmpOp::Lt,
6852 dst: 3,
6853 a: 1,
6854 b: 2,
6855 },
6856 Inst::BranchFalse { cond: 3, to: 7 },
6857 Inst::Arith {
6858 num: Num::Int,
6859 op: ArithOp::Add,
6860 dst: 1,
6861 a: 1,
6862 b: 4,
6863 },
6864 Inst::Jump { to: 3 },
6865 Inst::Int { dst: 1, value: 7 },
6866 Inst::StoreField {
6867 obj: 0,
6868 at: 0,
6869 src: 1,
6870 layout: int,
6871 },
6872 Inst::Return { src: 1 },
6873 ],
6874 );
6875 let environment = closure_layout(&mut build, body, &[word]);
6876 let mut code = vec![
6877 Inst::Alloc {
6878 dst: 0,
6879 layout: held,
6880 len: Len::Fixed,
6881 },
6882 Inst::Int { dst: 6, value: 0 },
6883 Inst::StoreField {
6884 obj: 0,
6885 at: 0,
6886 src: 6,
6887 layout: int,
6888 },
6889 Inst::ScopeEnter {
6890 dst: 1,
6891 name: StrId(0),
6892 },
6893 ];
6894 code.extend(close_over(&mut build, environment, body, 2, 6, Some(0)));
6895 code.extend([
6896 Inst::Spawn {
6897 dst: 3,
6898 scope: 1,
6899 closure: 2,
6900 answer: int,
6901 },
6902 Inst::ScopeLeave {
6903 scope: 1,
6904 failed: 4,
6905 error: 5,
6906 layout: int,
6907 },
6908 Inst::LoadField {
6909 dst: 6,
6910 obj: 0,
6911 at: 0,
6912 layout: int,
6913 },
6914 Inst::Return { src: 6 },
6915 ]);
6916 let main = build.function(
6917 "main",
6918 &[],
6919 &[
6920 Repr::Ref,
6921 Repr::Scope,
6922 Repr::Ref,
6923 Repr::Task,
6924 Repr::Bool,
6925 Repr::Int,
6926 Repr::Int,
6927 ],
6928 int,
6929 code,
6930 );
6931 let program = build.done();
6932 assert_eq!(run(&program, main, &[]).unwrap() as i64, 7);
6933 }
6934
6935 /// A body runs at most once and is waited for at most once, so awaiting
6936 /// the same handle twice answers the same value and repeats no effect.
6937 ///
6938 /// The counter is what says "no effect twice": it is incremented by the
6939 /// body and read after both awaits, and the answer is the product, so a
6940 /// second run would double it.
6941 #[test]
6942 fn awaiting_the_same_handle_twice_runs_the_body_once() {
6943 let mut build = Build::default().strings(&["tasks"]);
6944 let str_layout = build.layout("String", Shape::Str);
6945 build.program.str_layout = str_layout;
6946 let int = build.scalar(Repr::Int);
6947 let word = build.scalar(Repr::Ref);
6948 let held = counter(&mut build);
6949 let body = build.lambda(
6950 "body",
6951 &[],
6952 &[Repr::Ref, Repr::Int, Repr::Int],
6953 int,
6954 &[word],
6955 vec![
6956 Inst::LoadField {
6957 dst: 1,
6958 obj: 0,
6959 at: 0,
6960 layout: int,
6961 },
6962 Inst::Int { dst: 2, value: 1 },
6963 Inst::Arith {
6964 num: Num::Int,
6965 op: ArithOp::Add,
6966 dst: 1,
6967 a: 1,
6968 b: 2,
6969 },
6970 Inst::StoreField {
6971 obj: 0,
6972 at: 0,
6973 src: 1,
6974 layout: int,
6975 },
6976 Inst::Int { dst: 1, value: 7 },
6977 Inst::Return { src: 1 },
6978 ],
6979 );
6980 let environment = closure_layout(&mut build, body, &[word]);
6981 let mut code = vec![
6982 Inst::Alloc {
6983 dst: 0,
6984 layout: held,
6985 len: Len::Fixed,
6986 },
6987 Inst::Int { dst: 8, value: 0 },
6988 Inst::StoreField {
6989 obj: 0,
6990 at: 0,
6991 src: 8,
6992 layout: int,
6993 },
6994 Inst::ScopeEnter {
6995 dst: 1,
6996 name: StrId(0),
6997 },
6998 ];
6999 code.extend(close_over(&mut build, environment, body, 2, 8, Some(0)));
7000 code.extend([
7001 Inst::Spawn {
7002 dst: 3,
7003 scope: 1,
7004 closure: 2,
7005 answer: int,
7006 },
7007 Inst::Await {
7008 dst: 4,
7009 task: 3,
7010 answer: int,
7011 },
7012 Inst::Await {
7013 dst: 5,
7014 task: 3,
7015 answer: int,
7016 },
7017 Inst::ScopeLeave {
7018 scope: 1,
7019 failed: 6,
7020 error: 7,
7021 layout: int,
7022 },
7023 Inst::LoadField {
7024 dst: 8,
7025 obj: 0,
7026 at: 0,
7027 layout: int,
7028 },
7029 Inst::Arith {
7030 num: Num::Int,
7031 op: ArithOp::Add,
7032 dst: 4,
7033 a: 4,
7034 b: 5,
7035 },
7036 Inst::Arith {
7037 num: Num::Int,
7038 op: ArithOp::Mul,
7039 dst: 8,
7040 a: 8,
7041 b: 4,
7042 },
7043 Inst::Return { src: 8 },
7044 ]);
7045 let main = build.function(
7046 "main",
7047 &[],
7048 &[
7049 Repr::Ref,
7050 Repr::Scope,
7051 Repr::Ref,
7052 Repr::Task,
7053 Repr::Int,
7054 Repr::Int,
7055 Repr::Bool,
7056 Repr::Int,
7057 Repr::Int,
7058 ],
7059 int,
7060 code,
7061 );
7062 let program = build.done();
7063 // One run of the body, and 7 from each of the two awaits.
7064 assert_eq!(run(&program, main, &[]).unwrap() as i64, 14);
7065 }
7066
7067 /// A task the program cancelled has no value to await, in the words the
7068 /// oracle uses.
7069 ///
7070 /// The body cannot end any other way — it is an unbounded loop — so what
7071 /// is being measured is that the flag reached a safepoint and that the
7072 /// join told a stop from a finish.
7073 #[test]
7074 fn awaiting_a_cancelled_task_is_refused() {
7075 let mut build = Build::default().strings(&["tasks"]);
7076 let str_layout = build.layout("String", Shape::Str);
7077 build.program.str_layout = str_layout;
7078 let int = build.scalar(Repr::Int);
7079 let body = build.lambda(
7080 "body",
7081 &[],
7082 &[Repr::Int],
7083 int,
7084 &[],
7085 vec![Inst::Int { dst: 0, value: 0 }, Inst::Jump { to: 0 }],
7086 );
7087 let environment = closure_layout(&mut build, body, &[]);
7088 let mut code = vec![Inst::ScopeEnter {
7089 dst: 0,
7090 name: StrId(0),
7091 }];
7092 code.extend(close_over(&mut build, environment, body, 1, 6, None));
7093 code.extend([
7094 Inst::Spawn {
7095 dst: 2,
7096 scope: 0,
7097 closure: 1,
7098 answer: int,
7099 },
7100 Inst::Cancel { task: 2 },
7101 Inst::Await {
7102 dst: 3,
7103 task: 2,
7104 answer: int,
7105 },
7106 Inst::ScopeLeave {
7107 scope: 0,
7108 failed: 4,
7109 error: 5,
7110 layout: int,
7111 },
7112 Inst::Return { src: 3 },
7113 ]);
7114 let main = build.function(
7115 "main",
7116 &[],
7117 &[
7118 Repr::Scope,
7119 Repr::Ref,
7120 Repr::Task,
7121 Repr::Int,
7122 Repr::Bool,
7123 Repr::Int,
7124 Repr::Int,
7125 ],
7126 int,
7127 code,
7128 );
7129 let program = build.done();
7130 let error = run(&program, main, &[]).unwrap_err();
7131 assert_eq!(
7132 error.message,
7133 "task 1 of scope `tasks` was cancelled, so it has no value to await"
7134 );
7135 }
7136
7137 /// A call to an `async fn` is a call and a handle, and the handle can be
7138 /// awaited twice.
7139 ///
7140 /// No `ScopeEnter`, no `Spawn` and no thread: an `async fn` runs at its
7141 /// call site and `Inst::Settled` is the handle around what it produced.
7142 /// Awaiting twice answers the same words, which falls out of the state
7143 /// rather than being arranged — the same way it does for a spawned task,
7144 /// and the same way `crate::task::settle` gets it.
7145 #[test]
7146 fn a_settled_task_answers_the_call_s_words_however_often_it_is_awaited() {
7147 let mut build = Build::default();
7148 let int = build.scalar(Repr::Int);
7149 let none = build.args(&[]);
7150 let body = build.function(
7151 "body",
7152 &[],
7153 &[Repr::Int],
7154 int,
7155 vec![Inst::Int { dst: 0, value: 7 }, Inst::Return { src: 0 }],
7156 );
7157 let main = build.function(
7158 "main",
7159 &[],
7160 &[Repr::Int, Repr::Task, Repr::Int, Repr::Int],
7161 int,
7162 vec![
7163 Inst::Call {
7164 dst: 0,
7165 callee: body,
7166 args: none,
7167 },
7168 Inst::Settled {
7169 dst: 1,
7170 src: 0,
7171 answer: int,
7172 },
7173 Inst::Await {
7174 dst: 2,
7175 task: 1,
7176 answer: int,
7177 },
7178 Inst::Await {
7179 dst: 3,
7180 task: 1,
7181 answer: int,
7182 },
7183 Inst::Arith {
7184 num: Num::Int,
7185 op: ArithOp::Add,
7186 dst: 2,
7187 a: 2,
7188 b: 3,
7189 },
7190 Inst::Return { src: 2 },
7191 ],
7192 );
7193 let program = build.done();
7194 assert_eq!(run(&program, main, &[]).unwrap() as i64, 14);
7195 }
7196
7197 /// Cancelling a settled task does nothing, so awaiting it still answers.
7198 ///
7199 /// `crate::task::Task::cancel` asks a task that is *running* to stop, and
7200 /// a task whose body already ran is not one: cancellation stops work that
7201 /// has not happened, it does not undo work that has. An `async fn`'s
7202 /// handle is the extreme case of that, because its work was over before
7203 /// the handle existed.
7204 #[test]
7205 fn cancelling_a_settled_task_does_not_take_its_value_away() {
7206 let mut build = Build::default();
7207 let int = build.scalar(Repr::Int);
7208 let none = build.args(&[]);
7209 let body = build.function(
7210 "body",
7211 &[],
7212 &[Repr::Int],
7213 int,
7214 vec![Inst::Int { dst: 0, value: 7 }, Inst::Return { src: 0 }],
7215 );
7216 let main = build.function(
7217 "main",
7218 &[],
7219 &[Repr::Int, Repr::Task, Repr::Int],
7220 int,
7221 vec![
7222 Inst::Call {
7223 dst: 0,
7224 callee: body,
7225 args: none,
7226 },
7227 Inst::Settled {
7228 dst: 1,
7229 src: 0,
7230 answer: int,
7231 },
7232 Inst::Cancel { task: 1 },
7233 Inst::Await {
7234 dst: 2,
7235 task: 1,
7236 answer: int,
7237 },
7238 Inst::Return { src: 2 },
7239 ],
7240 );
7241 let program = build.done();
7242 assert_eq!(run(&program, main, &[]).unwrap() as i64, 7);
7243 }
7244
7245 /// A settled task's answer is a root of the task that made it.
7246 ///
7247 /// The words go into a heap object the scheduler table names, and the
7248 /// slot the call left them in is cleared at once — which is what the
7249 /// lowering emits, because the `Inst::Settled` consumed the temporary.
7250 /// So from that instruction onwards the table is the only thing that
7251 /// names the object, and the loop below allocates far more than the heap
7252 /// holds to say so: without the table among the roots the sweep would
7253 /// take the answer and the `await` would read a reclaimed word.
7254 #[test]
7255 fn a_settled_task_s_answer_survives_a_collection() {
7256 let mut build = Build::default().strings(&["kept"]);
7257 let int = build.scalar(Repr::Int);
7258 let str_layout = build.layout("String", Shape::Str);
7259 build.program.str_layout = str_layout;
7260 let cell = build.layout(
7261 "Cell",
7262 Shape::Elements {
7263 elem: int,
7264 growable: false,
7265 },
7266 );
7267 let none = build.args(&[]);
7268 let body = build.function(
7269 "body",
7270 &[],
7271 &[Repr::Ref],
7272 str_layout,
7273 vec![
7274 Inst::Str {
7275 dst: 0,
7276 text: StrId(0),
7277 },
7278 Inst::Return { src: 0 },
7279 ],
7280 );
7281 // s0 the call's answer, s1 the handle, s2 the counter, s3 the bound,
7282 // s4 the test, s5 the churn, s6 the step, s7 what the await answers,
7283 // s8 the length that is returned.
7284 let main = build.function(
7285 "main",
7286 &[],
7287 &[
7288 Repr::Ref,
7289 Repr::Task,
7290 Repr::Int,
7291 Repr::Int,
7292 Repr::Bool,
7293 Repr::Ref,
7294 Repr::Int,
7295 Repr::Ref,
7296 Repr::Int,
7297 ],
7298 int,
7299 vec![
7300 Inst::Call {
7301 dst: 0,
7302 callee: body,
7303 args: none,
7304 },
7305 Inst::Settled {
7306 dst: 1,
7307 src: 0,
7308 answer: str_layout,
7309 },
7310 Inst::Clear {
7311 slot: 0,
7312 layout: str_layout,
7313 },
7314 Inst::Int { dst: 2, value: 0 },
7315 Inst::Int {
7316 dst: 3,
7317 value: 4000,
7318 },
7319 Inst::Int { dst: 6, value: 1 },
7320 Inst::Cmp {
7321 on: Compare::Int,
7322 op: CmpOp::Lt,
7323 dst: 4,
7324 a: 2,
7325 b: 3,
7326 },
7327 Inst::BranchFalse { cond: 4, to: 12 },
7328 Inst::Alloc {
7329 dst: 5,
7330 layout: cell,
7331 len: Len::Count(64),
7332 },
7333 Inst::Clear {
7334 slot: 5,
7335 layout: cell,
7336 },
7337 Inst::Arith {
7338 num: Num::Int,
7339 op: ArithOp::Add,
7340 dst: 2,
7341 a: 2,
7342 b: 6,
7343 },
7344 Inst::Jump { to: 6 },
7345 Inst::Await {
7346 dst: 7,
7347 task: 1,
7348 answer: str_layout,
7349 },
7350 Inst::Len { dst: 8, obj: 7 },
7351 Inst::Return { src: 8 },
7352 ],
7353 );
7354 let program = build.done();
7355 // A heap far smaller than 4000 objects of 65 words, so the run only
7356 // reaches the `await` by collecting several times on the way.
7357 let mut machine = Machine::new(&program, 4096);
7358 // `kept` is four bytes, and it is still four bytes.
7359 assert_eq!(machine.run(main, &[], &budget()).unwrap(), vec![4]);
7360 assert!(
7361 machine.collected().collections > 0,
7362 "the run should have had to collect"
7363 );
7364 }
7365
7366 /// A child that raised propagates as itself out of the scope it was in.
7367 ///
7368 /// Not as a value: a runtime error is not something a Cove expression can
7369 /// hold, so `ScopeLeave` fails with it rather than answering it. That is
7370 /// the difference `crate::task::ChildFailure` draws, kept.
7371 #[test]
7372 fn a_child_that_raises_leaves_the_scope_with_its_own_error() {
7373 let mut build = Build::default().strings(&["tasks", "the child said so"]);
7374 let str_layout = build.layout("String", Shape::Str);
7375 build.program.str_layout = str_layout;
7376 let int = build.scalar(Repr::Int);
7377 let body = build.lambda(
7378 "body",
7379 &[],
7380 &[Repr::Int],
7381 int,
7382 &[],
7383 vec![Inst::Trap { message: StrId(1) }],
7384 );
7385 let environment = closure_layout(&mut build, body, &[]);
7386 let mut code = vec![Inst::ScopeEnter {
7387 dst: 0,
7388 name: StrId(0),
7389 }];
7390 code.extend(close_over(&mut build, environment, body, 1, 5, None));
7391 code.extend([
7392 Inst::Spawn {
7393 dst: 2,
7394 scope: 0,
7395 closure: 1,
7396 answer: int,
7397 },
7398 Inst::ScopeLeave {
7399 scope: 0,
7400 failed: 3,
7401 error: 4,
7402 layout: int,
7403 },
7404 Inst::Return { src: 4 },
7405 ]);
7406 let main = build.function(
7407 "main",
7408 &[],
7409 &[
7410 Repr::Scope,
7411 Repr::Ref,
7412 Repr::Task,
7413 Repr::Bool,
7414 Repr::Int,
7415 Repr::Int,
7416 ],
7417 int,
7418 code,
7419 );
7420 let program = build.done();
7421 let error = run(&program, main, &[]).unwrap_err();
7422 assert_eq!(error.message, "the child said so");
7423 }
7424
7425 /// Two tasks allocating at once over one heap, and an object only the
7426 /// parent's frame names.
7427 ///
7428 /// This is the whole of issue #240's Q1 as a test. The two children churn
7429 /// far past the heap's budget, so the collections are theirs; the parent
7430 /// is parked in a join for all of them, holding one object no child can
7431 /// reach. A collection that read a stale snapshot of the parent's frame,
7432 /// or that did not wait for a task at all, frees it.
7433 #[test]
7434 fn a_collection_a_sibling_ran_keeps_what_the_parent_holds() {
7435 let mut build = Build::default().strings(&["tasks"]);
7436 let str_layout = build.layout("String", Shape::Str);
7437 build.program.str_layout = str_layout;
7438 let int = build.scalar(Repr::Int);
7439 let held = counter(&mut build);
7440 let body = build.lambda(
7441 "body",
7442 &[],
7443 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Bool, Repr::Int],
7444 int,
7445 &[],
7446 vec![
7447 Inst::Int { dst: 1, value: 0 },
7448 Inst::Int {
7449 dst: 2,
7450 value: 20_000,
7451 },
7452 Inst::Int { dst: 4, value: 1 },
7453 Inst::Cmp {
7454 on: Compare::Int,
7455 op: CmpOp::Lt,
7456 dst: 3,
7457 a: 1,
7458 b: 2,
7459 },
7460 Inst::BranchFalse { cond: 3, to: 8 },
7461 Inst::Alloc {
7462 dst: 0,
7463 layout: held,
7464 len: Len::Fixed,
7465 },
7466 Inst::Arith {
7467 num: Num::Int,
7468 op: ArithOp::Add,
7469 dst: 1,
7470 a: 1,
7471 b: 4,
7472 },
7473 Inst::Jump { to: 3 },
7474 Inst::Return { src: 1 },
7475 ],
7476 );
7477 let environment = closure_layout(&mut build, body, &[]);
7478 let mut code = vec![
7479 Inst::Alloc {
7480 dst: 0,
7481 layout: held,
7482 len: Len::Fixed,
7483 },
7484 Inst::Int { dst: 9, value: 42 },
7485 Inst::StoreField {
7486 obj: 0,
7487 at: 0,
7488 src: 9,
7489 layout: int,
7490 },
7491 Inst::ScopeEnter {
7492 dst: 1,
7493 name: StrId(0),
7494 },
7495 ];
7496 code.extend(close_over(&mut build, environment, body, 2, 9, None));
7497 code.extend([
7498 Inst::Spawn {
7499 dst: 3,
7500 scope: 1,
7501 closure: 2,
7502 answer: int,
7503 },
7504 Inst::Spawn {
7505 dst: 4,
7506 scope: 1,
7507 closure: 2,
7508 answer: int,
7509 },
7510 Inst::Await {
7511 dst: 5,
7512 task: 3,
7513 answer: int,
7514 },
7515 Inst::Await {
7516 dst: 6,
7517 task: 4,
7518 answer: int,
7519 },
7520 Inst::ScopeLeave {
7521 scope: 1,
7522 failed: 7,
7523 error: 8,
7524 layout: int,
7525 },
7526 // The two children's turns, plus the word the parent held from
7527 // before the first collection to after the last.
7528 Inst::Arith {
7529 num: Num::Int,
7530 op: ArithOp::Add,
7531 dst: 5,
7532 a: 5,
7533 b: 6,
7534 },
7535 Inst::LoadField {
7536 dst: 9,
7537 obj: 0,
7538 at: 0,
7539 layout: int,
7540 },
7541 Inst::Arith {
7542 num: Num::Int,
7543 op: ArithOp::Add,
7544 dst: 9,
7545 a: 9,
7546 b: 5,
7547 },
7548 Inst::Return { src: 9 },
7549 ]);
7550 let main = build.function(
7551 "main",
7552 &[],
7553 &[
7554 Repr::Ref,
7555 Repr::Scope,
7556 Repr::Ref,
7557 Repr::Task,
7558 Repr::Task,
7559 Repr::Int,
7560 Repr::Int,
7561 Repr::Bool,
7562 Repr::Int,
7563 Repr::Int,
7564 ],
7565 int,
7566 code,
7567 );
7568 let program = build.done();
7569 assert_eq!(run(&program, main, &[]).unwrap() as i64, 42 + 40_000);
7570 }
7571 /// A child whose *value* was `Err(...)` leaves that error where the
7572 /// enclosing function can return it.
7573 ///
7574 /// The other half of what a failing child can be, and the half the corpus
7575 /// does not reach: `scope s { s.spawn { f()? } }` means the failure
7576 /// reaches the caller rather than sitting unread in a handle nobody
7577 /// awaited, and `crate::task::ChildFailure::Returned` is where the oracle
7578 /// says so. Here the answer is a run of words in the object the parent
7579 /// allocated, and what `ScopeLeave` copies out of it is the `Err` case's
7580 /// payload — at the layout the instruction names, which is the
7581 /// *enclosing* function's failure and not the child's answer.
7582 #[test]
7583 fn a_child_whose_value_failed_leaves_the_scope_with_its_payload() {
7584 let mut build = Build::default().strings(&["tasks"]);
7585 let str_layout = build.layout("String", Shape::Str);
7586 build.program.str_layout = str_layout;
7587 let int = build.scalar(Repr::Int);
7588 let answer = build.enumeration("Result", &[("Ok", vec![int]), ("Err", vec![int])]);
7589 let body = build.lambda(
7590 "body",
7591 &[],
7592 &[Repr::Int, Repr::Int],
7593 answer,
7594 &[],
7595 vec![
7596 // `Err(9)`: the case index, then the payload word the case
7597 // was placed at.
7598 Inst::Int { dst: 0, value: 1 },
7599 Inst::Int { dst: 1, value: 9 },
7600 Inst::Return { src: 0 },
7601 ],
7602 );
7603 let environment = closure_layout(&mut build, body, &[]);
7604 let mut code = vec![Inst::ScopeEnter {
7605 dst: 0,
7606 name: StrId(0),
7607 }];
7608 code.extend(close_over(&mut build, environment, body, 1, 5, None));
7609 code.extend([
7610 Inst::Spawn {
7611 dst: 2,
7612 scope: 0,
7613 closure: 1,
7614 answer,
7615 },
7616 Inst::ScopeLeave {
7617 scope: 0,
7618 failed: 3,
7619 error: 4,
7620 layout: int,
7621 },
7622 Inst::Return { src: 4 },
7623 ]);
7624 let main = build.function(
7625 "main",
7626 &[],
7627 &[
7628 Repr::Scope,
7629 Repr::Ref,
7630 Repr::Task,
7631 Repr::Bool,
7632 Repr::Int,
7633 Repr::Int,
7634 ],
7635 int,
7636 code,
7637 );
7638 let program = build.done();
7639 // Zero would be the location as the frame was zeroed, which is what
7640 // a `ScopeLeave` that had not noticed would leave there.
7641 assert_eq!(run(&program, main, &[]).unwrap() as i64, 9);
7642 }
7643
7644 // ---- a host runs a Cove callback ------------------------------------
7645
7646 /// A host that runs the callback it was handed.
7647 ///
7648 /// Three shapes, which are the three the shipped hosts have: `apply`
7649 /// calls once, as `http.Server.handle` does; `twice` calls more than
7650 /// once, as `clock.every` does; and `bounded` bounds the body with a flag
7651 /// it raises first, as `clock.timeout` does, and turns the stop into its
7652 /// own answer rather than passing the error on.
7653 struct Runner;
7654
7655 static RUNNER_OPS: &[cove_schema::OperationSchema] = &[
7656 cove_schema::OperationSchema {
7657 name: "apply",
7658 params: &[cove_schema::HostType::Any],
7659 variadic: false,
7660 result: cove_schema::HostType::Int,
7661 capability: "runner",
7662 effect: cove_schema::Effect::Read,
7663 cancellable: false,
7664 recordable: false,
7665 result_is_task_safe: true,
7666 },
7667 cove_schema::OperationSchema {
7668 name: "twice",
7669 params: &[cove_schema::HostType::Any],
7670 variadic: false,
7671 result: cove_schema::HostType::Int,
7672 capability: "runner",
7673 effect: cove_schema::Effect::Read,
7674 cancellable: false,
7675 recordable: false,
7676 result_is_task_safe: true,
7677 },
7678 cove_schema::OperationSchema {
7679 name: "bounded",
7680 params: &[cove_schema::HostType::Any],
7681 variadic: false,
7682 result: cove_schema::HostType::Int,
7683 capability: "runner",
7684 effect: cove_schema::Effect::Read,
7685 cancellable: true,
7686 recordable: false,
7687 result_is_task_safe: true,
7688 },
7689 cove_schema::OperationSchema {
7690 name: "caught",
7691 params: &[cove_schema::HostType::Any],
7692 variadic: false,
7693 result: cove_schema::HostType::Int,
7694 capability: "runner",
7695 effect: cove_schema::Effect::Read,
7696 cancellable: false,
7697 recordable: false,
7698 result_is_task_safe: true,
7699 },
7700 ];
7701
7702 impl crate::host::HostApi for Runner {
7703 fn module_schema(&self) -> cove_schema::ModuleSchema {
7704 cove_schema::ModuleSchema {
7705 name: "runner",
7706 capability: "runner",
7707 operations: RUNNER_OPS,
7708 types: &[],
7709 resources: &[],
7710 }
7711 }
7712
7713 fn call_with(
7714 &self,
7715 op: &str,
7716 args: Vec<Value>,
7717 back: &mut dyn Reentry,
7718 ) -> Result<Value, RuntimeError> {
7719 match op {
7720 "apply" => back.call(&args[0], Vec::new()),
7721 "twice" => {
7722 let one = back.call(&args[0], Vec::new())?.as_int().unwrap_or(0);
7723 let other = back.call(&args[0], Vec::new())?.as_int().unwrap_or(0);
7724 Ok(Value::int(one + other))
7725 }
7726 // The `clock.timeout` shape: the flag is raised before the
7727 // body runs, so the body stops at its first safepoint and the
7728 // host answers its own bound rather than passing the error on.
7729 "bounded" => {
7730 let stop = Cancellation::new();
7731 stop.cancel();
7732 match back.call_until(&args[0], Vec::new(), &stop) {
7733 Ok(_) => Ok(Value::int(0)),
7734 Err(_) if stop.is_cancelled() => Ok(Value::int(-1)),
7735 Err(error) => Err(error),
7736 }
7737 }
7738 // A host that catches what a callback failed with and carries
7739 // on, which is what makes restoring the frames this call
7740 // grew a requirement rather than a tidiness.
7741 "caught" => match back.call(&args[0], Vec::new()) {
7742 Ok(value) => Ok(value),
7743 Err(_) => Ok(Value::int(-2)),
7744 },
7745 other => Err(RuntimeError::new(format!("no `{other}` here"))),
7746 }
7747 }
7748
7749 fn call(&self, op: &str, _args: Vec<Value>) -> Result<Value, RuntimeError> {
7750 Err(RuntimeError::new(format!("`{op}` needs a way back")))
7751 }
7752 }
7753
7754 fn running() -> crate::host::HostRegistry {
7755 let mut hosts = crate::host::HostRegistry::new(crate::host::Grants::new(["runner"]));
7756 hosts.register(Box::new(Runner));
7757 hosts
7758 }
7759
7760 /// A program whose entry hands `runner.<op>` a closure over `depth`.
7761 ///
7762 /// The lambda is the recursion the reentry bound is about:
7763 ///
7764 /// ~~~text
7765 /// fn step() -> Int { // captures d
7766 /// if d == 0 { return 0 }
7767 /// runner.apply(fn() { ... d - 1 ... }) + 1
7768 /// }
7769 /// ~~~
7770 ///
7771 /// So `step` at `d` makes one host call, which runs `step` at `d - 1`,
7772 /// and the answer counts the levels — which is what makes a wrong bound
7773 /// visible as a wrong number rather than only as a missing error.
7774 fn a_reentering_program(op: &str, depth: i64) -> Program {
7775 let mut build = Build::default();
7776 let int = build.scalar(Repr::Int);
7777 let reference = build.word("Fn", Repr::Ref);
7778 build.program.host_ops.push(cove_ir::HostOp {
7779 resource: None,
7780 module: Arc::from("runner"),
7781 operation: Arc::from(op),
7782 result: int,
7783 });
7784 let call = cove_ir::HostOpId(build.program.host_ops.len() as u32 - 1);
7785 let args = build.args(&[(0, reference)]);
7786 let inner = build.args(&[(3, reference)]);
7787
7788 // The lambda is at the index it will be pushed to, which is what lets
7789 // its own body name it: a closure over `step` is what `step` builds.
7790 let step_id = FunctionId(build.program.functions.len() as u32);
7791 let closure = build.layout(
7792 "closure",
7793 Shape::Closure {
7794 function: step_id,
7795 captures: vec![int],
7796 },
7797 );
7798 let step = build.lambda(
7799 "step",
7800 &[],
7801 &[
7802 Repr::Int, // 0: the capture, `d`
7803 Repr::Int, // 1: zero
7804 Repr::Bool, // 2: d != 0
7805 Repr::Ref, // 3: the closure over d - 1
7806 Repr::Int, // 4: the callee's id
7807 Repr::Int, // 5: d - 1
7808 Repr::Int, // 6: the answer
7809 Repr::Int, // 7: one
7810 ],
7811 int,
7812 &[int],
7813 vec![
7814 Inst::Int { dst: 1, value: 0 },
7815 Inst::Cmp {
7816 on: Compare::Int,
7817 op: CmpOp::Ne,
7818 dst: 2,
7819 a: 0,
7820 b: 1,
7821 },
7822 Inst::BranchFalse { cond: 2, to: 11 },
7823 Inst::Alloc {
7824 dst: 3,
7825 layout: closure,
7826 len: Len::Fixed,
7827 },
7828 Inst::FuncRef {
7829 dst: 4,
7830 callee: step_id,
7831 },
7832 Inst::StoreField {
7833 obj: 3,
7834 at: 0,
7835 src: 4,
7836 layout: int,
7837 },
7838 Inst::Int { dst: 7, value: 1 },
7839 Inst::Arith {
7840 num: Num::Int,
7841 op: ArithOp::Sub,
7842 dst: 5,
7843 a: 0,
7844 b: 7,
7845 },
7846 Inst::StoreField {
7847 obj: 3,
7848 at: 1,
7849 src: 5,
7850 layout: int,
7851 },
7852 Inst::CallHost {
7853 dst: 6,
7854 op: call,
7855 args: inner,
7856 },
7857 Inst::Arith {
7858 num: Num::Int,
7859 op: ArithOp::Add,
7860 dst: 6,
7861 a: 6,
7862 b: 7,
7863 },
7864 // The frame is zeroed, so the `d == 0` arm answers the zero
7865 // that is already standing there.
7866 Inst::Return { src: 6 },
7867 ],
7868 );
7869 assert_eq!(step, step_id, "the lambda is where its own body says");
7870
7871 build.function(
7872 "main",
7873 &[],
7874 &[Repr::Ref, Repr::Int, Repr::Int, Repr::Int],
7875 int,
7876 vec![
7877 Inst::Alloc {
7878 dst: 0,
7879 layout: closure,
7880 len: Len::Fixed,
7881 },
7882 Inst::FuncRef {
7883 dst: 1,
7884 callee: step_id,
7885 },
7886 Inst::StoreField {
7887 obj: 0,
7888 at: 0,
7889 src: 1,
7890 layout: int,
7891 },
7892 Inst::Int {
7893 dst: 2,
7894 value: depth,
7895 },
7896 Inst::StoreField {
7897 obj: 0,
7898 at: 1,
7899 src: 2,
7900 layout: int,
7901 },
7902 Inst::CallHost {
7903 dst: 3,
7904 op: call,
7905 args,
7906 },
7907 Inst::Return { src: 3 },
7908 ],
7909 );
7910 build.done()
7911 }
7912
7913 /// The entry of [`a_reentering_program`], run.
7914 fn reentering(op: &str, depth: i64) -> Result<i64, RuntimeError> {
7915 let program = a_reentering_program(op, depth);
7916 let entry = program.functions.len() as u32 - 1;
7917 let hosts = running();
7918 let mut machine = Machine::with_hosts(&program, 1 << 14, Some(&hosts));
7919 machine
7920 .run(FunctionId(entry), &[], &budget())
7921 .map(|words| words[0] as i64)
7922 }
7923
7924 /// A closure reaches a host, and the host runs it.
7925 ///
7926 /// The whole of what was missing. The boundary refused to materialise one
7927 /// and `Back::call` refused on the other side, so a program that wrote
7928 /// `clock.timeout(500ms) { .. }` did not lower at all.
7929 #[test]
7930 fn a_host_runs_the_callback_it_was_handed() {
7931 assert_eq!(reentering("apply", 1).unwrap(), 1);
7932 }
7933
7934 /// A host may call the callback as many times as its operation means, and
7935 /// each one is a call the run pays for in full.
7936 #[test]
7937 fn a_host_may_call_its_callback_more_than_once() {
7938 // Each round answers 1, so a host that ran it twice answers 2 — and
7939 // one that reused a frame instead of opening a second would not.
7940 assert_eq!(reentering("twice", 1).unwrap(), 2);
7941 }
7942
7943 /// A callback that fails leaves the machine exactly as it found it.
7944 ///
7945 /// `clock.timeout` catches what the body failed with and answers its own
7946 /// bound, so the frames the callback grew, the words its frames occupied
7947 /// and the scopes it opened must all be back where they were — the outer
7948 /// run has no unwinding, and the reasoning that makes that sound (the run
7949 /// is ending) does not reach a host that carries on.
7950 #[test]
7951 fn a_failed_callback_leaves_the_frames_it_grew() {
7952 // The body divides by zero at the first level and the host catches
7953 // it; the entry then returns through frames that have to be intact.
7954 let mut build = Build::default();
7955 let int = build.scalar(Repr::Int);
7956 let reference = build.word("Fn", Repr::Ref);
7957 build.program.host_ops.push(cove_ir::HostOp {
7958 resource: None,
7959 module: Arc::from("runner"),
7960 operation: Arc::from("caught"),
7961 result: int,
7962 });
7963 let op = cove_ir::HostOpId(0);
7964 let args = build.args(&[(0, reference)]);
7965 let step_id = FunctionId(build.program.functions.len() as u32);
7966 let closure = build.layout(
7967 "closure",
7968 Shape::Closure {
7969 function: step_id,
7970 captures: vec![],
7971 },
7972 );
7973 let step = build.lambda(
7974 "step",
7975 &[],
7976 &[Repr::Int, Repr::Int, Repr::Int],
7977 int,
7978 &[],
7979 vec![
7980 Inst::Int { dst: 0, value: 1 },
7981 Inst::Int { dst: 1, value: 0 },
7982 Inst::Arith {
7983 num: Num::Int,
7984 op: ArithOp::Div,
7985 dst: 2,
7986 a: 0,
7987 b: 1,
7988 },
7989 Inst::Return { src: 2 },
7990 ],
7991 );
7992 assert_eq!(step, step_id);
7993 let main = build.function(
7994 "main",
7995 &[],
7996 &[Repr::Ref, Repr::Int, Repr::Int],
7997 int,
7998 vec![
7999 Inst::Alloc {
8000 dst: 0,
8001 layout: closure,
8002 len: Len::Fixed,
8003 },
8004 Inst::FuncRef {
8005 dst: 1,
8006 callee: step_id,
8007 },
8008 Inst::StoreField {
8009 obj: 0,
8010 at: 0,
8011 src: 1,
8012 layout: int,
8013 },
8014 Inst::CallHost { dst: 2, op, args },
8015 Inst::Return { src: 2 },
8016 ],
8017 );
8018 let program = build.done();
8019 let hosts = running();
8020 let mut machine = Machine::with_hosts(&program, 1 << 13, Some(&hosts));
8021 let before = machine.mem.stack_words();
8022 assert_eq!(
8023 machine.run(main, &[], &budget()).unwrap(),
8024 vec![-2i64 as u64]
8025 );
8026 assert_eq!(
8027 machine.mem.stack_words(),
8028 before,
8029 "the callback's frame went back where it came from"
8030 );
8031 assert!(machine.frames.is_empty());
8032 }
8033
8034 /// A bounded call's flag stops the body at its next safepoint.
8035 ///
8036 /// `Reentry::call_until` says `stop` *"bounds this call and everything
8037 /// inside it"*, and until a callback could run here at all there was
8038 /// nothing for it to bound: `Machine::stops` was `&[]` at every safepoint
8039 /// and at the boundary. The body here is a loop long enough to reach a
8040 /// safepoint, and what stops it is the oracle's own `stopped_here`.
8041 #[test]
8042 fn a_bounded_callback_stops_at_a_safepoint() {
8043 let mut build = Build::default();
8044 let int = build.scalar(Repr::Int);
8045 let reference = build.word("Fn", Repr::Ref);
8046 build.program.host_ops.push(cove_ir::HostOp {
8047 resource: None,
8048 module: Arc::from("runner"),
8049 operation: Arc::from("bounded"),
8050 result: int,
8051 });
8052 let op = cove_ir::HostOpId(0);
8053 let args = build.args(&[(0, reference)]);
8054 let step_id = FunctionId(build.program.functions.len() as u32);
8055 let closure = build.layout(
8056 "closure",
8057 Shape::Closure {
8058 function: step_id,
8059 captures: vec![],
8060 },
8061 );
8062 // `var i = 0; while i < 100000 { i += 1 }; i`
8063 let step = build.lambda(
8064 "step",
8065 &[],
8066 &[Repr::Int, Repr::Int, Repr::Bool, Repr::Int],
8067 int,
8068 &[],
8069 vec![
8070 Inst::Int { dst: 0, value: 0 },
8071 Inst::Int {
8072 dst: 1,
8073 value: 100_000,
8074 },
8075 Inst::Int { dst: 3, value: 1 },
8076 Inst::Cmp {
8077 on: Compare::Int,
8078 op: CmpOp::Lt,
8079 dst: 2,
8080 a: 0,
8081 b: 1,
8082 },
8083 Inst::BranchFalse { cond: 2, to: 6 },
8084 Inst::Arith {
8085 num: Num::Int,
8086 op: ArithOp::Add,
8087 dst: 0,
8088 a: 0,
8089 b: 3,
8090 },
8091 Inst::Jump { to: 3 },
8092 Inst::Return { src: 0 },
8093 ],
8094 );
8095 assert_eq!(step, step_id);
8096 let main = build.function(
8097 "main",
8098 &[],
8099 &[Repr::Ref, Repr::Int, Repr::Int],
8100 int,
8101 vec![
8102 Inst::Alloc {
8103 dst: 0,
8104 layout: closure,
8105 len: Len::Fixed,
8106 },
8107 Inst::FuncRef {
8108 dst: 1,
8109 callee: step_id,
8110 },
8111 Inst::StoreField {
8112 obj: 0,
8113 at: 0,
8114 src: 1,
8115 layout: int,
8116 },
8117 Inst::CallHost { dst: 2, op, args },
8118 Inst::Return { src: 2 },
8119 ],
8120 );
8121 let program = build.done();
8122 let hosts = running();
8123 let mut machine = Machine::with_hosts(&program, 1 << 13, Some(&hosts));
8124 // The host turned the stop into its own answer, which it could only
8125 // do because the body stopped.
8126 assert_eq!(
8127 machine.run(main, &[], &budget()).unwrap(),
8128 vec![-1i64 as u64]
8129 );
8130 // And the flag went with the call that raised it.
8131 assert!(machine.stops.is_empty());
8132 }
8133
8134 // ---- how deep a reentry may nest ------------------------------------
8135
8136 /// Host → Cove → Host → Cove, as deep as the bound allows.
8137 ///
8138 /// This is the case that decides what `docs/LINEAR_VM.md`'s *"a builtin
8139 /// never calls back into Cove"* means here. Cove calling Cove adds no
8140 /// native frame in this backend, so the reserved stack region is the
8141 /// whole of the depth question — but a *host* callback is not Cove
8142 /// calling Cove: the host is already a Rust frame, and running its
8143 /// callback puts `HostRegistry::dispatch`, the host's own frames, and
8144 /// another turn of `encoded::dispatch` under every Cove frame the
8145 /// callback makes. So the bound is the oracle's `MAX_REENTRY_DEPTH`,
8146 /// which exists for exactly that.
8147 #[test]
8148 fn a_reentry_may_nest_up_to_the_bound() {
8149 // One host call per level, `MAX_REENTRY_DEPTH` of them stacked at the
8150 // deepest point, and the answer counts them.
8151 let depth = crate::interp::MAX_REENTRY_DEPTH as i64 - 1;
8152 assert_eq!(reentering("apply", depth).unwrap(), depth);
8153 }
8154
8155 /// And no deeper: the run stops, rather than the process.
8156 #[test]
8157 fn a_reentry_past_the_bound_stops_the_run_rather_than_the_process() {
8158 let depth = crate::interp::MAX_REENTRY_DEPTH as i64;
8159 let error = reentering("apply", depth).unwrap_err();
8160 assert_eq!(
8161 error.message,
8162 format!(
8163 "reentry depth limit of {} reached while a host ran a Cove callback",
8164 crate::interp::MAX_REENTRY_DEPTH
8165 ),
8166 "the refusal is the oracle's own, word for word"
8167 );
8168 }
8169
8170 /// A host that runs its callback twice pays for one level twice over
8171 /// rather than for two levels at once.
8172 #[test]
8173 fn calling_a_callback_twice_is_one_level_twice() {
8174 // Deep enough that two levels at once would be past the bound, and
8175 // `twice` at every level, so a count that did not come back down
8176 // would refuse long before this answers.
8177 let depth = crate::interp::MAX_REENTRY_DEPTH as i64 - 1;
8178 assert!(reentering("twice", depth).is_ok());
8179 }
8180
8181 // ---- cells ---------------------------------------------------------
8182
8183 /// A cell taken and given back leaves nothing held.
8184 ///
8185 /// The pair the lowering emits, on its own, so that a failure here is the
8186 /// machine's arms and not a lowering that emitted the wrong pair.
8187 #[test]
8188 fn a_cell_is_taken_and_given_back_by_the_pair_of_instructions() {
8189 let mut build = Build::default();
8190 let int = build.word("Int", Repr::Int);
8191 let held = build.layout("Shared", Shape::Shared { value: int });
8192 let main = build.function(
8193 "main",
8194 &[],
8195 &[Repr::Int, Repr::Ref],
8196 int,
8197 vec![
8198 Inst::Alloc {
8199 dst: 1,
8200 layout: held,
8201 len: Len::Fixed,
8202 },
8203 Inst::SharedLock { cell: 1 },
8204 Inst::Int { dst: 0, value: 7 },
8205 Inst::StoreField {
8206 obj: 1,
8207 at: cove_ir::SHARED_VALUE,
8208 src: 0,
8209 layout: int,
8210 },
8211 Inst::SharedUnlock { cell: 1 },
8212 Inst::LoadField {
8213 dst: 0,
8214 obj: 1,
8215 at: cove_ir::SHARED_VALUE,
8216 layout: int,
8217 },
8218 Inst::Return { src: 0 },
8219 ],
8220 );
8221 let program = build.done();
8222 let mut machine = Machine::new(&program, 1 << 12);
8223 assert_eq!(machine.run(main, &[], &budget()).unwrap(), vec![7]);
8224 assert!(machine.held.is_empty());
8225 }
8226
8227 /// A run that fails inside a `lock` gives back every cell it was holding.
8228 ///
8229 /// The release is an obligation on every exit path, and a runtime error is
8230 /// the one path the lowering cannot write: it is not a jump, so no
8231 /// `Inst::SharedUnlock` stands between it and the end of the run. Without
8232 /// this a cell a failing task never gave back would be a cell no task
8233 /// could ever take — and *no task* is the point, because the heap and the
8234 /// cells in it belong to the run rather than to the task that failed.
8235 ///
8236 /// Two cells, because they nest and the refusal is per cell: giving back
8237 /// only the innermost would leave the other held.
8238 #[test]
8239 fn a_failing_run_gives_back_every_cell_it_held() {
8240 let mut build = Build::default().strings(&["stop"]);
8241 let str_layout = build.layout("String", Shape::Str);
8242 build.program.str_layout = str_layout;
8243 let int = build.word("Int", Repr::Int);
8244 let held = build.layout("Shared", Shape::Shared { value: int });
8245 let main = build.function(
8246 "main",
8247 &[],
8248 &[Repr::Int, Repr::Ref, Repr::Ref],
8249 int,
8250 vec![
8251 Inst::Alloc {
8252 dst: 1,
8253 layout: held,
8254 len: Len::Fixed,
8255 },
8256 Inst::Alloc {
8257 dst: 2,
8258 layout: held,
8259 len: Len::Fixed,
8260 },
8261 Inst::SharedLock { cell: 1 },
8262 Inst::SharedLock { cell: 2 },
8263 Inst::Trap {
8264 message: cove_ir::StrId(0),
8265 },
8266 Inst::Return { src: 0 },
8267 ],
8268 );
8269 let program = build.done();
8270 let mut machine = Machine::new(&program, 1 << 12);
8271 let error = machine.run(main, &[], &budget()).unwrap_err();
8272 assert_eq!(error.message, "stop");
8273
8274 assert!(machine.held.is_empty());
8275 // The frames are left standing by a failed run, so the two cells are
8276 // still where the fixture put them and can be asked.
8277 let base = machine.frames[0].base;
8278 for slot in [1, 2] {
8279 let addr = machine.mem.slot(base, slot);
8280 assert_eq!(
8281 cell::holder(&machine.mem, addr),
8282 0,
8283 "a cell a failing task took is free again"
8284 );
8285 }
8286 }
8287
8288 /// A task that already holds a cell is refused rather than made to wait,
8289 /// and the refusal does not take the cell.
8290 #[test]
8291 fn a_reentrant_lock_is_refused_and_leaves_the_cell_held_once() {
8292 let mut build = Build::default();
8293 let int = build.word("Int", Repr::Int);
8294 let held = build.layout("Shared", Shape::Shared { value: int });
8295 let main = build.function(
8296 "main",
8297 &[],
8298 &[Repr::Int, Repr::Ref],
8299 int,
8300 vec![
8301 Inst::Alloc {
8302 dst: 1,
8303 layout: held,
8304 len: Len::Fixed,
8305 },
8306 Inst::SharedLock { cell: 1 },
8307 Inst::SharedLock { cell: 1 },
8308 Inst::Return { src: 0 },
8309 ],
8310 );
8311 let program = build.done();
8312 let mut machine = Machine::new(&program, 1 << 12);
8313 let error = machine.run(main, &[], &budget()).unwrap_err();
8314 assert_eq!(
8315 error.message,
8316 "this task already holds this `Shared`, so `lock` would wait for itself"
8317 );
8318 // The refusal did not take the cell a second time, so the unwind gives
8319 // it back exactly once.
8320 assert!(machine.held.is_empty());
8321 let addr = machine.mem.slot(machine.frames[0].base, 1);
8322 assert_eq!(cell::holder(&machine.mem, addr), 0);
8323 }
8324
8325 // --- ADR 0052: the safepoint schedule is work, not a multiple ----------
8326
8327 /// **A stride past the last charge is the stride's next multiple, while
8328 /// every instruction costs one — and stays a stride of work when one does
8329 /// not.**
8330 ///
8331 /// The schedule is contract arithmetic:
8332 /// `docs/adr/0040-a-bound-outlives-its-backend.md` states every stop bound
8333 /// in multiples of [`SAFEPOINT_STRIDE`] and `tests/responsiveness.rs`
8334 /// measures each one, so neither the change from `instructions % S == 0`
8335 /// to a difference nor the move into the `work` coordinate may shift a
8336 /// single count while every instruction still costs one.
8337 /// `crate::vm::debug`'s `the_safepoint_fires_at_the_same_counts_as_it_did_before`
8338 /// proves that end to end through the fuel limit; this proves the
8339 /// arithmetic, including the bulk case that test cannot reach.
8340 #[test]
8341 fn the_next_question_is_a_stride_of_work_past_the_last_charge() {
8342 let program = Build::default().done();
8343 let mut machine = Machine::new(&program, 1 << 12);
8344
8345 // While every instruction costs one, `charged_work` lands on a
8346 // multiple at every safepoint, so the next question is the next
8347 // multiple — which is what the condition used to say in so many words.
8348 for turn in 0..4u64 {
8349 machine.charged_work = turn * SAFEPOINT_STRIDE;
8350 machine.instructions = machine.charged_work + 1;
8351 machine.bulk_work = 0;
8352 assert_eq!(
8353 machine.next_question(),
8354 (turn + 1) * SAFEPOINT_STRIDE,
8355 "with {} charged, the question is the next multiple",
8356 machine.charged_work
8357 );
8358 }
8359
8360 // And when a copy charges for the words it moved, the question is
8361 // still a stride of *work* away rather than a multiple the charge may
8362 // have stepped clean over. `2500` is past `2048` and is not a multiple
8363 // of `1024`: the old rule answered false here and skipped the
8364 // safepoint entirely.
8365 machine.charged_work = 0;
8366 machine.instructions = 500;
8367 machine.bulk_work = 2000;
8368 assert_eq!(machine.work(), 2500);
8369 // Already past a stride of work, so the question is due now.
8370 assert_eq!(machine.next_question(), 0);
8371 assert!(
8372 machine.work() - machine.charged_work >= SAFEPOINT_STRIDE,
8373 "2500 units of work since the last charge is a safepoint"
8374 );
8375 assert!(
8376 !machine.work().is_multiple_of(SAFEPOINT_STRIDE),
8377 "and 2500 is not a multiple of the stride, which is the bug"
8378 );
8379 }
8380}