cove_ir/program.rs
1//! A lowered program, and the tables an instruction indexes into.
2//!
3//! A [`Program`] is immutable once lowered. ADR 0008 runs a spawned task on
4//! a thread of its own and a task's body is a lowered function like any
5//! other, so every thread of one run reads this same program rather than a
6//! copy of it — which is why the strings in it are `Arc<str>` and why
7//! nothing here is behind a cell.
8
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12use cove_diag::Span;
13
14use crate::inst::{Inst, Pc, Slot};
15use crate::layout::{Layout, LayoutId};
16use crate::repr::{RefMap, Repr};
17
18macro_rules! id {
19 ($(#[$doc:meta])* $name:ident, $prefix:literal) => {
20 $(#[$doc])*
21 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22 pub struct $name(pub u32);
23
24 impl $name {
25 /// The index this id names.
26 pub fn index(self) -> usize {
27 self.0 as usize
28 }
29 }
30
31 impl std::fmt::Display for $name {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, concat!($prefix, "{}"), self.0)
34 }
35 }
36 };
37}
38
39id!(
40 /// Names a [`Function`] in [`Program::functions`].
41 ///
42 /// # Scope and stability
43 ///
44 /// A `FunctionId` is dense — it is a position in one [`Program`]'s
45 /// `functions`, not a name — and it means nothing outside that one
46 /// linked program. It is not stable across an edit to the package that
47 /// produced it: declarations are numbered first and lambdas and generic
48 /// instantiations are appended after them, so adding, removing or moving
49 /// an earlier one renumbers every later one. It is not a stable external
50 /// identity either, in the way a qualified name is — two builds of the
51 /// same source are not guaranteed to number a function the same way, and
52 /// nothing here promises they will.
53 ///
54 /// Nothing may persist a bare `FunctionId` across an artifact boundary,
55 /// because there is no third thing to check it against once it is on
56 /// the far side of one. Every boundary Cove has today avoids the
57 /// question instead of answering it: `cove build` embeds the checked
58 /// source in the generated crate and lowers a fresh [`Program`] when
59 /// that crate is compiled, rather than serialising this one's; the
60 /// trace and replay format keys an entry point by `module` and
61 /// `function` **strings** ([`Program::function_named`] resolves the
62 /// pair against whatever program is current, back into a `FunctionId`
63 /// of *that* run) and never writes the id itself; and the wasm
64 /// playground's boundary is rendered text — [`crate::print`]'s
65 /// listing — not the program that produced it. No id crosses a boundary
66 /// today because nothing here has ever needed one to.
67 FunctionId, "fn"
68);
69id!(
70 /// Names a string in [`Program::strings`].
71 StrId, "str"
72);
73id!(
74 /// Names an argument list in [`Program::args`].
75 ///
76 /// A call's arguments are a static list of [`Arg`]s, held once in the
77 /// program rather than inline in the instruction, so that [`Inst`] stays
78 /// small enough to be worth copying and a repeated call shape costs one
79 /// list rather than one per site.
80 ArgsId, "args"
81);
82id!(
83 /// Names a jump table in [`Program::tables`].
84 TableId, "table"
85);
86id!(
87 /// Names one case of an enum layout: its position in
88 /// [`crate::layout::Shape::Enum::cases`].
89 ///
90 /// It is the number an enum's discriminant word holds, and it is a type
91 /// of its own for the reason [`crate::Repr::Tag`] is: the word is an
92 /// integer and the value is not one. Where the number is written into a
93 /// slot — [`crate::Inst::Tag`] — the id says which case it names, and the
94 /// verifier bounds it against the layout rather than against nothing.
95 CaseId, "case"
96);
97id!(
98 /// Names a host operation in [`Program::host_ops`].
99 HostOpId, "host"
100);
101id!(
102 /// Names a builtin in [`Program::builtins`].
103 BuiltinId, "builtin"
104);
105
106/// One argument of a call: where the value is, and what it is.
107///
108/// A slot alone says where an operand *begins* and never how wide it is. A
109/// scalar is described by the `Repr` of the slot it sits in and a reference
110/// by the header of the object it names, but an inline struct or enum is a
111/// run of words with nothing attached to it at all — a `Point` in a frame is
112/// described by neither. So a callee that is polymorphic over the values it
113/// is handed had no way to read one: `"{Point(x: 1)}"` rendered the first
114/// word, `a == b` on two structs compared the first word, and the operations
115/// that put a whole value into a collection refused rather than store half of
116/// one.
117///
118/// Carrying the layout beside the slot answers all of them at once, and it is
119/// carried for *every* argument rather than for the calls that turned out to
120/// need it. A layout is what an argument is; which callee reads it is not the
121/// argument's business, and one rule the verifier checks everywhere is worth
122/// more than the word this costs at the sites that could have done without.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub struct Arg {
125 /// The first slot of the value location in the caller's frame.
126 pub slot: Slot,
127 /// The layout of that location, which is what says how wide it is.
128 pub layout: LayoutId,
129}
130
131/// One host operation a program calls: `console.log`, `files.read`,
132/// `files.Writer.writeLine`.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct HostOp {
135 pub module: Arc<str>,
136 pub operation: Arc<str>,
137 /// The resource kind the operation belongs to, for one addressed to a
138 /// handle rather than to the module: `Writer` in
139 /// `files.Writer.writeLine`.
140 ///
141 /// It is what [`Inst::CallResource`] names and what
142 /// [`Inst::CallHost`] does not, so one table holds both and the two
143 /// namings cannot collide: a module's `files.write` and a resource's
144 /// `files.Writer.write` are two entries rather than one.
145 ///
146 /// Nothing dispatches on it. Which resource an operation reaches is the
147 /// business of the handle the receiver names — ADR 0013 gives the host
148 /// the only record of what is open — and this is what the call site
149 /// settled, kept for the disassembly and for a diagnostic that has to
150 /// say what was being called.
151 pub resource: Option<Arc<str>>,
152 /// The layout of the value location the host's answer is written into.
153 ///
154 /// A schema that declared its result `Any` gives a boxed layout;
155 /// anything else gives the layout of the declared type.
156 pub result: LayoutId,
157}
158
159impl HostOp {
160 /// The operation as the source writes it: `console.println`, or
161 /// `files.Writer.writeLine` for one addressed to a resource.
162 pub fn qualified(&self) -> String {
163 match &self.resource {
164 Some(kind) => format!("{}.{kind}.{}", self.module, self.operation),
165 None => format!("{}.{}", self.module, self.operation),
166 }
167 }
168}
169
170/// One builtin a program calls: `Array.length`, `String.split`, `Int.abs`.
171///
172/// A builtin is named rather than numbered because the set of them is the
173/// language reference's, not the IR's: adding one is a runtime change, and
174/// the IR should not have to be renumbered for it.
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub struct Builtin {
177 /// The type the operation belongs to: `Array`, `String`, `Map`, `Int`.
178 pub receiver: Arc<str>,
179 pub operation: Arc<str>,
180 pub result: LayoutId,
181}
182
183/// Where a [`Inst::Switch`] goes.
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct Table {
186 /// One target per case index, in order.
187 pub targets: Vec<Pc>,
188 /// Where an index outside `targets` goes.
189 ///
190 /// A `match` the checker proved exhaustive still has one, because the
191 /// value being switched on came out of a heap object and the machine
192 /// does not take the lowering's word for what is in it.
193 pub default: Pc,
194}
195
196/// A capture a closure body reads.
197#[derive(Clone, Debug, PartialEq, Eq)]
198pub struct Capture {
199 pub name: Arc<str>,
200 /// The first slot of the closure frame's value location for it.
201 ///
202 /// Captures follow the parameters, each taking the words its layout
203 /// says. It is written down rather than derived because the machine
204 /// should not have to re-add a run of widths it can read.
205 pub slot: Slot,
206 pub layout: LayoutId,
207}
208
209/// One named binding, and the range of the function's code over which that
210/// name denotes that slot.
211///
212/// A side table, and read for the same reason [`Function::spans`] is: a name
213/// is wanted when a *human* asks what a frame holds — a debugger stopped at a
214/// breakpoint, issue #241 — and never in the dispatch loop, so it belongs
215/// beside the code rather than in it.
216///
217/// It exists because neither half of that question is answerable from the
218/// frame. [`Function::reprs`] says what a slot's *word* holds, for the whole
219/// function, and that is all it says: until this table the only name anywhere
220/// in a lowered program was [`Capture::name`], parameters were positional and
221/// locals were anonymous. So a debugger could say `s7:int = 3`, which is true
222/// of the machine and can be a lie about the program.
223///
224/// # Two locals may share a slot, and that is the point
225///
226/// [`Function::reprs`]' own note says a slot may be reused by a later value
227/// of the same `Repr`, because the lowering hands a dead run to the next
228/// value that asks for that shape. One slot is therefore several source
229/// variables over a function's life, and nothing but this table can tell them
230/// apart. Two locals of one slot have *disjoint* ranges and, usually,
231/// different names.
232///
233/// # Two locals may share a name
234///
235/// Shadowing is recorded, not resolved. `let x = 1; let x = "two"` is two
236/// bindings and both are kept, because the first is still what the frame
237/// holds at every pc before the second — and because resolving here would
238/// make the table disagree with the lowering, whose scope is searched
239/// backwards so that the latest declaration wins. Their ranges may overlap
240/// and their slots differ. A reader keeps the locals whose range contains the
241/// pc and **takes the last match**; [`Function::local_at`] is that rule
242/// written down.
243///
244/// A `break` or a `continue` is not an end of a range. `[from, to)` is an
245/// interval of program counters, every pc inside a scope's body is one the
246/// binding is live at, and the pc a `break` jumps to is outside the interval
247/// already.
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub struct Local {
250 pub name: Arc<str>,
251 pub slot: Slot,
252 pub layout: LayoutId,
253 /// The first pc at which the name is bound.
254 pub from: Pc,
255 /// One past the last. `[from, to)` is a half-open interval, like a
256 /// [`Span`].
257 pub to: Pc,
258}
259
260/// A body that was written elsewhere and expanded into this one.
261///
262/// `[from, to)` is the run of this function's program counters the expansion
263/// occupies — from where the call stood to where its answer landed —
264/// `callee` is whose instructions they are, and `site` is where the call was
265/// written.
266///
267/// `site` is the whole of what an error chain lost. `Machine::call_chain`
268/// walks the live frames and reads each one's call site; an expansion has no
269/// frame, so its call site was not there to read, and an error raised inside
270/// one named where it happened and not where it was called from. One span per
271/// expansion is what puts that back.
272#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct Inlined {
274 pub from: Pc,
275 pub to: Pc,
276 pub callee: FunctionId,
277 pub site: Span,
278 /// The names the expanded body bound, in this function's slots and this
279 /// function's counters.
280 ///
281 /// Here rather than in [`Function::locals`], and that is not tidiness. A
282 /// caller's binding and an expanded body's parameter can be bound at the
283 /// *same* program counter — the caller's `let raised = n + 1` and the
284 /// callee's `n`, when the argument needed no copy — and a reader sorting
285 /// one table by "which expansion contains this counter" cannot tell them
286 /// apart. Which body declared a name is not something a counter answers,
287 /// so it is recorded rather than derived.
288 pub locals: Vec<Local>,
289}
290
291/// One lowered function.
292#[derive(Clone, Debug)]
293pub struct Function {
294 /// The module and name the source declared, for diagnostics and for
295 /// [`Program::function_named`].
296 pub module: Arc<str>,
297 pub name: Arc<str>,
298 /// The layout of each parameter, in declaration order.
299 ///
300 /// Parameters occupy the frame from slot 0 onward, each taking the words
301 /// its layout says: a `(Int, Point, Int)` list occupies slots 0, 1–2 and
302 /// 3. Declaration order, not a permutation into type groups — ADR 0034's
303 /// *"a mixed list such as `(Int, String, Int)` is not permuted into type
304 /// regions"*. There are no type regions to permute into.
305 pub params: Vec<LayoutId>,
306 /// What each slot of the frame holds. `reprs.len()` is the frame size.
307 ///
308 /// A slot's `Repr` is fixed for the whole function; that is what makes
309 /// [`Function::refs`] correct at every program counter. A slot may be
310 /// reused by a later value of the same `Repr`, and a reference slot is
311 /// cleared to null at its last use, so the static map costs no retention
312 /// beyond a value's live range.
313 pub reprs: Vec<Repr>,
314 /// Which slots are references, derived from [`Function::reprs`].
315 pub refs: RefMap,
316 /// The layout of what the function answers.
317 ///
318 /// [`Inst::Return`] names the base slot of the answer in the callee's
319 /// frame and the caller's [`Inst::Call`] names the base slot of the
320 /// destination location in its own; the machine copies this many words
321 /// between them.
322 pub returns: LayoutId,
323 /// The values the enclosing body handed this function, if it is a
324 /// lambda. Empty for a declared function.
325 pub captures: Vec<Capture>,
326 pub code: Vec<Inst>,
327 /// The source span of each instruction, parallel to [`Function::code`].
328 ///
329 /// A parallel array rather than a field of [`Inst`]: a span is read when
330 /// a run fails or a trace is written, and never in the dispatch loop, so
331 /// it should not be in the cache line the loop is reading.
332 pub spans: Vec<Span>,
333 /// What the source called the values in the frame, and where each name
334 /// meant which slot.
335 ///
336 /// In declaration order, which is the order the shadowing rule reads
337 /// them in; see [`Local`]. Not parallel to anything — a function binds as
338 /// many names as it binds — and empty is a legal answer for a body that
339 /// binds none.
340 pub locals: Vec<Local>,
341 /// The bodies this function holds that were written somewhere else.
342 ///
343 /// `lower::inline` expands a call to a small leaf where it is made, and
344 /// the frame that call would have pushed then does not exist. Nothing
345 /// downstream can tell: a run of instructions in the middle of this
346 /// function *is* another function, and every reader that walks frames —
347 /// an error's chain, a backtrace, a profile — sees one frame where there
348 /// were two.
349 ///
350 /// So the expansion writes down what it removed. This is that record, and
351 /// it is [`Local`]'s shape for [`Local`]'s reason: a slot number is not an
352 /// answer to "what did the source call this", and a program counter is not
353 /// an answer to "whose instruction is this".
354 ///
355 /// In the order the expansions were made, which is program-counter order,
356 /// and ranges nest rather than overlap. A reader takes the *last* range
357 /// that contains the pc, which is the innermost body — the same rule
358 /// [`Function::local_at`] follows, for the same reason.
359 pub inlined: Vec<Inlined>,
360 /// Where the declaration itself is, for a diagnostic that is about the
361 /// function rather than about one of its instructions.
362 pub span: Span,
363 /// Whether the body is a task's: `async fn`, or the lambda a `spawn`
364 /// was handed.
365 pub is_async: bool,
366 /// Whether this is a stand-in the lowering left for a declaration it
367 /// did not lower a body for. See `lower::stub`, and `Function::is_stub`.
368 pub stub: bool,
369}
370
371impl Function {
372 /// How many words a call to this function occupies on the stack.
373 pub fn frame_size(&self) -> u32 {
374 self.reprs.len() as u32
375 }
376
377 /// How many parameters the function declares.
378 pub fn arity(&self) -> u32 {
379 self.params.len() as u32
380 }
381
382 /// The first slot of parameter `at`, which is the widths of the ones
383 /// before it.
384 pub fn param_slot(&self, at: usize, layouts: &[Layout]) -> Slot {
385 self.params[..at]
386 .iter()
387 .map(|id| layouts[id.index()].width())
388 .sum()
389 }
390
391 /// How many slots the parameters occupy in total.
392 pub fn param_words(&self, layouts: &[Layout]) -> u32 {
393 self.params
394 .iter()
395 .map(|id| layouts[id.index()].width())
396 .sum()
397 }
398
399 /// What slot `slot` holds.
400 pub fn repr(&self, slot: Slot) -> Option<Repr> {
401 self.reprs.get(slot as usize).copied()
402 }
403
404 /// The span of the instruction at `pc`, or the declaration's own.
405 pub fn span_at(&self, pc: usize) -> Span {
406 self.spans.get(pc).copied().unwrap_or(self.span)
407 }
408
409 /// Which slot `name` denotes at `pc`, if the source bound it there.
410 ///
411 /// The last match wins, because a shadowing declaration is recorded
412 /// beside the one it shadows rather than in place of it: see [`Local`].
413 pub fn local_at(&self, name: &str, pc: Pc) -> Option<&Local> {
414 self.locals
415 .iter()
416 .rev()
417 .find(|local| &*local.name == name && local.from <= pc && pc < local.to)
418 }
419
420 /// The expanded bodies `pc` is inside, innermost last.
421 ///
422 /// A reader that wants one frame's worth of context wants the last of
423 /// them; a reader rebuilding a chain wants all of them, innermost first,
424 /// which is this reversed. Ranges nest, so "contains the pc" and "in the
425 /// order they were made" is enough to order them: an inner expansion is
426 /// always written after the outer one it sits in.
427 pub fn inlined_at(&self, pc: Pc) -> impl Iterator<Item = &Inlined> + '_ {
428 self.inlined
429 .iter()
430 .filter(move |held| held.from <= pc && pc < held.to)
431 }
432
433 /// `module.name`, as a diagnostic writes it.
434 pub fn qualified(&self) -> String {
435 format!("{}.{}", self.module, self.name)
436 }
437
438 /// Whether this is a stand-in rather than a lowered body.
439 ///
440 /// A stub has no body to stop at: its one instruction is a `Return`
441 /// written at the declaration's own span, and it has no parameters and
442 /// no names, because there was no boundary and no scope to bind them
443 /// from. A tool that resolves a source location or a breakpoint against
444 /// a program — a debugger walking [`Function::locals`], a stack trace
445 /// reading [`Function::span_at`] — has to skip a stub rather than answer
446 /// out of it, or it answers a question about a function that was never
447 /// written.
448 ///
449 /// It answers `true` for all three kinds `lower::stub`'s doc comment
450 /// describes, because `stub` is the one place any of them is built and
451 /// this reads back exactly what it recorded. But a program that actually
452 /// runs — the output of [`lower_roots`](crate::lower_roots) or
453 /// [`lower_entry`](crate::lower_entry) once a lowering finishes without
454 /// error — can only hold two of the three: the declaration a slice left
455 /// out, and a generic declaration whose instantiations carry the real
456 /// code beside it. The third kind, a declaration this lowering reported
457 /// a gap about, belongs to a lowering that never got handed back — a gap
458 /// is an error, so the program it would have been part of does not exist
459 /// for a caller of this method to ask about.
460 ///
461 /// This is a stored fact rather than a test of the four fields above,
462 /// because a shape a stub happens to have is not a shape only a stub
463 /// has. `lower::stub`'s own construction is the only place that knows
464 /// *why* the instruction, span, and empty lists are what they are;
465 /// asking a shape test to recover that intent at a distance means the
466 /// day a real body of one instruction is ever written at its
467 /// declaration's own span, the test is wrong and nothing says so.
468 /// Recording the fact the lowering already has costs one field;
469 /// re-deriving it costs a convention two crates now have to keep in
470 /// sync by hand.
471 pub fn is_stub(&self) -> bool {
472 self.stub
473 }
474}
475
476/// A whole lowered package.
477#[derive(Clone, Debug, Default)]
478pub struct Program {
479 pub functions: Vec<Function>,
480 pub layouts: Vec<Layout>,
481 pub strings: Vec<Arc<str>>,
482 pub args: Vec<Vec<Arg>>,
483 pub tables: Vec<Table>,
484 pub host_ops: Vec<HostOp>,
485 pub builtins: Vec<Builtin>,
486 /// The layout every string object shares.
487 ///
488 /// One field rather than a layout in each [`Inst::Str`], because every
489 /// string in a program has the same shape and the machine should not
490 /// have to be told it once per literal. A program that mentions no
491 /// string still declares it: the machine allocates one for a host's
492 /// answer, and a table it has to check for emptiness first is a branch
493 /// on a path that always takes the same side.
494 pub str_layout: LayoutId,
495 /// The layout every byte run under construction shares.
496 ///
497 /// A program-wide constant for the reason [`Program::str_layout`] is one:
498 /// [`Inst::AllocBytes`] should not have to be told this layout per call
499 /// site, and [ADR 0051](../../docs/adr/0051-a-string-is-built-as-a-byte-run.md)
500 /// gives every run the same [`crate::layout::Shape::Bytes`] shape whatever
501 /// string it will become.
502 pub bytes_layout: LayoutId,
503 /// The layout every byte buffer's owner shares.
504 ///
505 /// A program-wide constant for [`Program::bytes_layout`]'s reason, and the
506 /// other half of the pair: an owner and its store are allocated together
507 /// by [`Inst::AllocBuffer`], so neither layout is named at a call site.
508 /// [ADR 0052](../../docs/adr/0052-a-growable-value-is-a-stable-owner-over-a-replaceable-run.md)
509 /// gives every buffer the same [`crate::layout::Shape::ByteBuffer`] shape
510 /// whatever bytes it will hold, because an owner's two words are a length
511 /// and a reference whatever the store's capacity.
512 pub buffer_layout: LayoutId,
513 /// The layout every [`Inst::Box`] allocates its object as.
514 ///
515 /// A program-wide constant for the same reason [`Program::str_layout`]
516 /// is one: every box has the same *object* shape, and what differs — the
517 /// layout of the value inside it — is in the box's first payload word.
518 /// The machine should not have to search a table for a shape that is
519 /// always the same, and a search that fails has to answer something.
520 pub boxed_layout: LayoutId,
521 /// `module.name` to id, for an entry point named on a command line.
522 pub by_name: BTreeMap<(Arc<str>, Arc<str>), FunctionId>,
523}
524
525impl Program {
526 pub fn function(&self, id: FunctionId) -> &Function {
527 &self.functions[id.index()]
528 }
529
530 pub fn layout(&self, id: LayoutId) -> &Layout {
531 &self.layouts[id.index()]
532 }
533
534 pub fn string(&self, id: StrId) -> &Arc<str> {
535 &self.strings[id.index()]
536 }
537
538 pub fn arg_list(&self, id: ArgsId) -> &[Arg] {
539 &self.args[id.index()]
540 }
541
542 pub fn table(&self, id: TableId) -> &Table {
543 &self.tables[id.index()]
544 }
545
546 pub fn host_op(&self, id: HostOpId) -> &HostOp {
547 &self.host_ops[id.index()]
548 }
549
550 pub fn builtin(&self, id: BuiltinId) -> &Builtin {
551 &self.builtins[id.index()]
552 }
553
554 /// The id of `module.name`, if the program has it.
555 pub fn function_named(&self, module: &str, name: &str) -> Option<FunctionId> {
556 self.by_name
557 .iter()
558 .find(|((m, n), _)| &**m == module && &**n == name)
559 .map(|(_, id)| *id)
560 }
561}