cove_ir/verify.rs
1//! A static check that a lowered program is well formed.
2//!
3//! The machine takes the lowering's word for a great deal: that a value
4//! location fits the frame it is in, that a jump lands on an instruction,
5//! that a call passes the layouts the callee declares, and — the one that
6//! matters most — that a slot's [`Repr`] is what [`Function::refs`] says it
7//! is. A collection walks frames using that map, so a lowering that wrote a
8//! reference into a slot the map calls an `Int` would produce a dangling
9//! reference at the next collection and a wrong answer some time after that.
10//!
11//! # A location agrees with its layout, word for word
12//!
13//! Every instruction that moves a value names the layout it is moving, and a
14//! layout is a run of [`Repr`]s. So the check is not "the destination is a
15//! reference" but "the destination's words *are* the layout's words, in
16//! order". That is what makes the one-value-many-slots rule checkable: a
17//! `Copy` of a three-word `Wrapper` into a location whose second word is a
18//! `Float` is a fault here rather than a `Float` traced as a pointer later.
19//!
20//! # A width is checked, not assumed
21//!
22//! Two of those checks are about how far a run of words reaches, and they are
23//! here because nothing downstream can make them. A value location has to fit
24//! the frame it is in — `slot + width <= frame_size` — or a `Copy` near the
25//! top of a frame reads or writes the frame above it, which was the shape of
26//! five separate failures while this backend was being built and which
27//! `Memory::copy_words` was left asserting about in a debug build. And a
28//! field access has to fit the object it names, which this can say wherever
29//! the object's layout is a static fact; where it is not, the machine's own
30//! bounds check is what answers, from the header.
31//!
32//! This is where those assumptions are checked, once, before anything runs.
33//! It is not a type checker: `cove-sema` already did that, and a failure here
34//! is a bug in the lowering rather than a fault in the program. It exists so
35//! that such a bug is a loud failure at lowering time instead of a quiet one
36//! at collection time.
37
38use crate::inst::{Compare, Inst, Len, Num, Slot};
39use crate::layout::{LayoutId, Shape};
40use crate::program::{Function, FunctionId, Program};
41use crate::repr::{RefMap, Repr};
42
43/// A way in which a lowered program is not well formed.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Invalid {
46 /// The function the fault is in, as `module.name`.
47 pub function: String,
48 /// The instruction it is at, or `None` when the fault is the function's
49 /// own — a frame whose reference map disagrees with its reprs, say.
50 pub pc: Option<usize>,
51 pub what: String,
52}
53
54impl std::fmt::Display for Invalid {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self.pc {
57 Some(pc) => write!(f, "{}+{pc}: {}", self.function, self.what),
58 None => write!(f, "{}: {}", self.function, self.what),
59 }
60 }
61}
62
63/// Checks every function of `program`, reporting every fault rather than the
64/// first: one lowering bug usually shows up in several places, and seeing
65/// all of them is what says which one is the cause.
66pub fn verify(program: &Program) -> Result<(), Vec<Invalid>> {
67 let mut faults = Vec::new();
68 for (index, function) in program.functions.iter().enumerate() {
69 Check {
70 program,
71 function,
72 id: FunctionId(index as u32),
73 objects: Vec::new(),
74 funcs: Vec::new(),
75 faults: &mut faults,
76 }
77 .run();
78 }
79 if faults.is_empty() {
80 Ok(())
81 } else {
82 Err(faults)
83 }
84}
85
86/// Marks `width` words starting at `slot` as written by something
87/// [`Check::slot_facts`] declines to guess about — the `Some(None)` case any
88/// writer other than the one this fact is about produces.
89fn poison<T>(seen: &mut [Option<Option<T>>], slot: Slot, width: u32) {
90 for at in slot..slot.saturating_add(width) {
91 if let Some(place) = seen.get_mut(at as usize) {
92 *place = Some(None);
93 }
94 }
95}
96
97/// Marks `slot` as written by `id` — the answer stays `id` if every writer
98/// [`Check::slot_facts`] has seen so far agrees, and becomes the poisoned
99/// [`Option::None`] the moment two disagree.
100fn identify<T: Copy + PartialEq>(seen: &mut [Option<Option<T>>], slot: Slot, id: T) {
101 if let Some(place) = seen.get_mut(slot as usize) {
102 *place = match *place {
103 None => Some(Some(id)),
104 Some(Some(held)) if held == id => Some(Some(id)),
105 _ => Some(None),
106 };
107 }
108}
109
110struct Check<'a> {
111 program: &'a Program,
112 function: &'a Function,
113 id: FunctionId,
114 /// The layout of the object each reference slot holds, where the whole
115 /// function agrees on one. See [`Check::slot_facts`].
116 objects: Vec<Option<LayoutId>>,
117 /// The callee each slot holds, where the whole function agrees on one and
118 /// the writer was [`Inst::FuncRef`]. See [`Check::slot_facts`].
119 funcs: Vec<Option<FunctionId>>,
120 faults: &'a mut Vec<Invalid>,
121}
122
123impl Check<'_> {
124 fn run(&mut self) {
125 let (objects, funcs) = self.slot_facts();
126 self.objects = objects;
127 self.funcs = funcs;
128 self.check_frame();
129 for pc in 0..self.function.code.len() {
130 self.check_inst(pc);
131 }
132 self.check_falls_off_the_end();
133 }
134
135 /// Which slots hold an object whose layout is a static fact, and which
136 /// hold a callee whose id is one — one walk of the code answering both,
137 /// because a slot is disqualified from the second the same way it is
138 /// from the first.
139 ///
140 /// A `Repr::Ref` slot carries no layout — that is the point of the header
141 /// — so in general only the machine can bound a field access. But a slot
142 /// that is written by allocations alone, all naming one layout, holds
143 /// either null or an object of that layout at every program counter: a
144 /// slot's `Repr` is fixed for the whole function and a run is only ever
145 /// reused by a location of the same words, so the *set* of layouts ever
146 /// written into a slot bounds what it can hold without a walk of the
147 /// control flow. One layout and no other writer is the case this can
148 /// answer, and it is the common one — a lowering allocates an object and
149 /// reads its fields in the same breath.
150 ///
151 /// The second answer is the same question about [`Inst::FuncRef`] instead
152 /// of [`Inst::Alloc`]: a slot written by one, and by nothing else, holds
153 /// that callee at every program counter. A temporary is given back to the
154 /// pool once its value is stored, [`crate::lower::closures`] among its
155 /// callers, so one slot number can hold two different closures' callees
156 /// in one function — which is a second writer with a different id, and
157 /// poisons the answer exactly as a second, different [`Inst::Alloc`]
158 /// would.
159 ///
160 /// Anything else is `None`, which means the check the answer feeds is
161 /// skipped rather than failed. A slot written by a call, a load or a copy
162 /// holds whatever the callee or the source held, and this declines to
163 /// guess.
164 fn slot_facts(&self) -> (Vec<Option<LayoutId>>, Vec<Option<FunctionId>>) {
165 // `Some(None)` is "written, by something that says no fact"; `None`
166 // is "not written yet". The parameters and the captures are written
167 // by the caller, so they start as the first.
168 let mut objects: Vec<Option<Option<LayoutId>>> = vec![None; self.function.reprs.len()];
169 let mut funcs: Vec<Option<Option<FunctionId>>> = vec![None; self.function.reprs.len()];
170 let words = |id: LayoutId| {
171 self.program
172 .layouts
173 .get(id.index())
174 .map_or(1, |layout| layout.width())
175 };
176 for at in 0..self.function.param_words(&self.program.layouts) {
177 poison(&mut objects, at, 1);
178 poison(&mut funcs, at, 1);
179 }
180 for capture in &self.function.captures {
181 poison(&mut objects, capture.slot, words(capture.layout));
182 poison(&mut funcs, capture.slot, words(capture.layout));
183 }
184 for inst in &self.function.code {
185 match *inst {
186 // The three that say what they allocate. A `Clear` is not
187 // among them and is not a writer either: it stores null, and
188 // null is refused by the machine before a layout is asked
189 // about. None of the three is `Inst::FuncRef`, so all three
190 // poison the second answer the way any other writer does.
191 Inst::Alloc { dst, layout, .. } => {
192 identify(&mut objects, dst, layout);
193 poison(&mut funcs, dst, 1);
194 }
195 Inst::Str { dst, .. } => {
196 identify(&mut objects, dst, self.program.str_layout);
197 poison(&mut funcs, dst, 1);
198 }
199 Inst::Box { dst, .. } => {
200 identify(&mut objects, dst, self.program.boxed_layout);
201 poison(&mut funcs, dst, 1);
202 }
203 // The one instruction that identifies a callee rather than a
204 // layout. It is not an allocation, so it poisons the first
205 // answer exactly as `Inst::Int` does.
206 Inst::FuncRef { dst, callee } => {
207 poison(&mut objects, dst, 1);
208 identify(&mut funcs, dst, callee);
209 }
210 Inst::Clear { .. } | Inst::Jump { .. } | Inst::BranchFalse { .. } => {}
211 Inst::Switch { .. } | Inst::Return { .. } | Inst::Trap { .. } => {}
212 // Scheduler state, not objects. A `Repr::Task` and a
213 // `Repr::Scope` word name a table entry, so there is no
214 // layout for one of these to claim.
215 Inst::ScopeEnter { dst, .. } => {
216 poison(&mut objects, dst, 1);
217 poison(&mut funcs, dst, 1);
218 }
219 Inst::Spawn { dst, .. } => {
220 poison(&mut objects, dst, 1);
221 poison(&mut funcs, dst, 1);
222 }
223 Inst::Settled { dst, .. } => {
224 poison(&mut objects, dst, 1);
225 poison(&mut funcs, dst, 1);
226 }
227 Inst::ScopeCancel { .. } | Inst::Cancel { .. } => {}
228 // Neither writes a slot: what they change is the cell's own
229 // lock word, which is not a location this frame numbers.
230 Inst::SharedLock { .. } | Inst::SharedUnlock { .. } => {}
231 Inst::ScopeLeave {
232 failed,
233 error,
234 layout,
235 ..
236 } => {
237 poison(&mut objects, failed, 1);
238 poison(&mut funcs, failed, 1);
239 poison(&mut objects, error, words(layout));
240 poison(&mut funcs, error, words(layout));
241 }
242 Inst::Await { dst, answer, .. } => {
243 poison(&mut objects, dst, words(answer));
244 poison(&mut funcs, dst, words(answer));
245 }
246 // Writes nothing a program can read: what it writes is the
247 // run's report of where an assertion failed.
248 Inst::AssertFailed { .. } => {}
249 Inst::Store { .. } | Inst::StoreField { .. } | Inst::StoreElem { .. } => {}
250 Inst::Unit { dst } | Inst::Bool { dst, .. } => {
251 poison(&mut objects, dst, 1);
252 poison(&mut funcs, dst, 1);
253 }
254 Inst::Int { dst, .. } | Inst::Tag { dst, .. } | Inst::Float { dst, .. } => {
255 poison(&mut objects, dst, 1);
256 poison(&mut funcs, dst, 1);
257 }
258 Inst::Neg { dst, .. } | Inst::Not { dst, .. } => {
259 poison(&mut objects, dst, 1);
260 poison(&mut funcs, dst, 1);
261 }
262 Inst::Arith { dst, .. } | Inst::Cmp { dst, .. } => {
263 poison(&mut objects, dst, 1);
264 poison(&mut funcs, dst, 1);
265 }
266 Inst::ArithImm { dst, .. } | Inst::CmpImm { dst, .. } => {
267 poison(&mut objects, dst, 1);
268 poison(&mut funcs, dst, 1);
269 }
270 Inst::Convert { dst, .. } => {
271 poison(&mut objects, dst, 1);
272 poison(&mut funcs, dst, 1);
273 }
274 Inst::ByteAt { dst, .. } | Inst::Len { dst, .. } | Inst::LayoutOf { dst, .. } => {
275 poison(&mut objects, dst, 1);
276 poison(&mut funcs, dst, 1);
277 }
278 // `AllocBytes` always allocates `Program::bytes_layout`, but
279 // this poisons `dst` exactly as `ByteAt` does rather than
280 // `identify`ing it the way `Inst::Alloc` and `Inst::Str` do:
281 // a byte run under construction is not a value this pass
282 // needs to reason about by layout, only by `Repr`, and
283 // `FinishString` immediately relabels the same slot to a
284 // `Str` object anyway.
285 Inst::AllocBytes { dst, .. } | Inst::FinishString { dst, .. } => {
286 poison(&mut objects, dst, 1);
287 poison(&mut funcs, dst, 1);
288 }
289 // ADR 0052's two, and the same answer for the same reason:
290 // `AllocBuffer` always allocates `Program::buffer_layout` and
291 // `FinishBuffer` answers the store its owner was holding,
292 // relabelled to a `Str` object. Neither is a layout this pass
293 // reasons about, only a `Repr`.
294 Inst::AllocBuffer { dst, .. } | Inst::FinishBuffer { dst, .. } => {
295 poison(&mut objects, dst, 1);
296 poison(&mut funcs, dst, 1);
297 }
298 // Neither writes a frame slot: `WriteByte` writes a byte of
299 // the object `bytes` already names, and `CopyBytes` writes
300 // into the object its `args` table's `dst` already names.
301 Inst::WriteByte { .. } | Inst::CopyBytes { .. } => {}
302 // Nor do the growable appends: what each changes is the store
303 // the owner in `buffer` names, and the owner's own length word.
304 Inst::AppendByte { .. } | Inst::AppendBytes { .. } => {}
305 // Forming the address of a slot is also a write to it, as
306 // far as this is concerned: a `var` argument is that address
307 // handed to a callee, and what the callee stores through it
308 // lands in this frame. The checker holds the two to one type
309 // and so to one layout, but a static claim about a slot
310 // should not rest on an argument made somewhere else.
311 Inst::AddrOfSlot { dst, slot } => {
312 poison(&mut objects, dst, 1);
313 poison(&mut funcs, dst, 1);
314 poison(&mut objects, slot, 1);
315 poison(&mut funcs, slot, 1);
316 }
317 Inst::AddrOfField { dst, .. } => {
318 poison(&mut objects, dst, 1);
319 poison(&mut funcs, dst, 1);
320 }
321 Inst::AddrOfElem { dst, .. } | Inst::AddrOfPart { dst, .. } => {
322 poison(&mut objects, dst, 1);
323 poison(&mut funcs, dst, 1);
324 }
325 Inst::Copy { dst, layout, .. }
326 | Inst::Load { dst, layout, .. }
327 | Inst::LoadField { dst, layout, .. }
328 | Inst::LoadElem { dst, layout, .. }
329 | Inst::Unbox { dst, layout, .. } => {
330 poison(&mut objects, dst, words(layout));
331 poison(&mut funcs, dst, words(layout));
332 }
333 Inst::Call { dst, callee, .. } => {
334 let width = match self.program.functions.get(callee.index()) {
335 Some(target) => words(target.returns),
336 None => 1,
337 };
338 poison(&mut objects, dst, width);
339 poison(&mut funcs, dst, width);
340 }
341 Inst::CallClosure { dst, result, .. } => {
342 poison(&mut objects, dst, words(result));
343 poison(&mut funcs, dst, words(result));
344 }
345 Inst::CallHost { dst, op, .. } | Inst::CallResource { dst, op, .. } => {
346 let width = match self.program.host_ops.get(op.index()) {
347 Some(op) => words(op.result),
348 None => 1,
349 };
350 poison(&mut objects, dst, width);
351 poison(&mut funcs, dst, width);
352 }
353 Inst::CallBuiltin { dst, builtin, .. } => {
354 let width = match self.program.builtins.get(builtin.index()) {
355 Some(builtin) => words(builtin.result),
356 None => 1,
357 };
358 poison(&mut objects, dst, width);
359 poison(&mut funcs, dst, width);
360 }
361 }
362 }
363 (
364 objects.into_iter().map(Option::flatten).collect(),
365 funcs.into_iter().map(Option::flatten).collect(),
366 )
367 }
368
369 fn fault(&mut self, pc: Option<usize>, what: impl Into<String>) {
370 self.faults.push(Invalid {
371 function: self.function.qualified(),
372 pc,
373 what: what.into(),
374 });
375 }
376
377 /// The frame's own invariants: the parameters fit, the answer's layout
378 /// exists, the spans line up, the reference map is the one the reprs
379 /// imply, and every name is of a location and a range this function has.
380 fn check_frame(&mut self) {
381 let size = self.function.frame_size();
382 let mut at = 0;
383 for (index, param) in self.function.params.clone().into_iter().enumerate() {
384 if !self.layout_exists(None, param) {
385 continue;
386 }
387 let width = self.program.layout(param).width();
388 if !self.fits(None, at, param, &format!("parameter {index}")) {
389 return;
390 }
391 at += width;
392 }
393 if !self.layout_exists(None, self.function.returns) {
394 return;
395 }
396 if self.function.spans.len() != self.function.code.len() {
397 self.fault(
398 None,
399 format!(
400 "has {} instructions but {} spans",
401 self.function.code.len(),
402 self.function.spans.len()
403 ),
404 );
405 }
406 let expected = RefMap::of(&self.function.reprs);
407 if expected != self.function.refs {
408 self.fault(
409 None,
410 "reference map disagrees with the frame's reprs, so a collection would \
411 scan the wrong slots"
412 .to_string(),
413 );
414 }
415 for capture in self.function.captures.clone() {
416 if !self.layout_exists(None, capture.layout) {
417 continue;
418 }
419 let name = capture.name.clone();
420 self.fits(
421 None,
422 capture.slot,
423 capture.layout,
424 &format!("capture `{name}`"),
425 );
426 }
427 // Nothing runs a local — it is read when a person asks what a frame
428 // holds — so what is checked is that it *names* something that
429 // exists: a location the frame has, over a range of this function's
430 // code. A local pointing past either would be a debugger's answer
431 // about a slot or an instruction that is not there.
432 for index in 0..self.function.locals.len() {
433 // One `Local` at a time rather than `self.function.locals.clone()`:
434 // the loop body needs `&mut self` for `fault`, which a borrow of
435 // the table itself would still be holding, but a name and four
436 // `Copy` fields cost far less than a second copy of the table.
437 let local = self.function.locals[index].clone();
438 let name = local.name;
439 if self.layout_exists(None, local.layout) {
440 self.fits(None, local.slot, local.layout, &format!("local `{name}`"));
441 }
442 if local.from > local.to {
443 self.fault(
444 None,
445 format!(
446 "local `{name}` is bound at {} and freed at {}",
447 local.from, local.to
448 ),
449 );
450 } else if local.to as usize > self.function.code.len() {
451 self.fault(
452 None,
453 format!(
454 "local `{name}` is live to {} and the function has {} instructions",
455 local.to,
456 self.function.code.len()
457 ),
458 );
459 }
460 }
461 // The same question about the same kind of table, asked of the bodies
462 // an expansion wrote here. `Inlined` is `Local`'s shape and is read by
463 // the same readers — an error's chain, a backtrace, a profile — so a
464 // range or a slot that names nothing is the same fault, and it is one
465 // nothing at run time would notice: an expansion is not executed
466 // *through* its record, it is merely described by it.
467 for index in 0..self.function.inlined.len() {
468 let held = self.function.inlined[index].clone();
469 let callee = held.callee.index();
470 let name = match self.program.functions.get(callee) {
471 Some(function) => function.qualified(),
472 None => {
473 self.fault(
474 None,
475 format!("an expanded body names function {callee}, which is not one"),
476 );
477 continue;
478 }
479 };
480 if held.from > held.to {
481 self.fault(
482 None,
483 format!(
484 "the expansion of `{name}` runs from {} to {}",
485 held.from, held.to
486 ),
487 );
488 } else if held.to as usize > self.function.code.len() {
489 self.fault(
490 None,
491 format!(
492 "the expansion of `{name}` ends at {} and the function has {} \
493 instructions",
494 held.to,
495 self.function.code.len()
496 ),
497 );
498 }
499 for local in held.locals {
500 let bound = local.name;
501 if self.layout_exists(None, local.layout) {
502 self.fits(
503 None,
504 local.slot,
505 local.layout,
506 &format!("local `{bound}` of the expanded `{name}`"),
507 );
508 }
509 if local.from > local.to || local.to as usize > self.function.code.len() {
510 self.fault(
511 None,
512 format!(
513 "local `{bound}` of the expanded `{name}` is live from {} to {}, \
514 and the function has {} instructions",
515 local.from,
516 local.to,
517 self.function.code.len()
518 ),
519 );
520 }
521 }
522 }
523 let _ = size;
524 }
525
526 /// A function whose last instruction can fall through has nowhere to go.
527 fn check_falls_off_the_end(&mut self) {
528 let last = self.function.code.len().checked_sub(1);
529 let ends = matches!(
530 last.map(|pc| &self.function.code[pc]),
531 Some(Inst::Return { .. } | Inst::Jump { .. } | Inst::Switch { .. } | Inst::Trap { .. })
532 );
533 if !ends {
534 self.fault(
535 last,
536 "the last instruction can fall through, and there is nothing after it",
537 );
538 }
539 }
540
541 fn check_inst(&mut self, pc: usize) {
542 let inst = self.function.code[pc].clone();
543 let at = Some(pc);
544 match inst {
545 Inst::Unit { dst } => self.expect(at, dst, &[Repr::Unit]),
546 Inst::Bool { dst, .. } => self.expect(at, dst, &[Repr::Bool]),
547 Inst::Int { dst, .. } => self.expect(at, dst, &[Repr::Int, Repr::Duration]),
548 // The one place a case index is written, and the only check that
549 // it names a case of the enum it claims to. `Inst::Int` could
550 // write the same word and be bounded against nothing.
551 Inst::Tag { dst, layout, case } => {
552 self.expect(at, dst, &[Repr::Tag]);
553 if self.in_range(at, layout.index(), self.program.layouts.len(), "layout") {
554 match &self.program.layout(layout).shape {
555 crate::layout::Shape::Enum { cases, .. } => {
556 if case.index() >= cases.len() {
557 let count = cases.len();
558 self.fault(
559 at,
560 format!(
561 "names {case} of {}, which has {count} case(s)",
562 self.program.layout(layout).name
563 ),
564 );
565 }
566 }
567 _ => self.fault(
568 at,
569 format!(
570 "writes a case of {}, which is not an enum",
571 self.program.layout(layout).name
572 ),
573 ),
574 }
575 }
576 }
577 Inst::FuncRef { dst, callee } => {
578 if !self.in_range(at, callee.index(), self.program.functions.len(), "function") {
579 return;
580 }
581 self.expect(at, dst, &[Repr::Int]);
582 }
583 Inst::Float { dst, .. } => self.expect(at, dst, &[Repr::Float]),
584 Inst::Str { dst, text } => {
585 self.expect(at, dst, &[Repr::Ref]);
586 self.in_range(at, text.index(), self.program.strings.len(), "string");
587 }
588 Inst::Copy { dst, src, layout } => {
589 if self.layout_exists(at, layout) {
590 self.fits(at, dst, layout, "the destination of a copy");
591 self.fits(at, src, layout, "the source of a copy");
592 }
593 }
594 Inst::Clear { slot, layout } => {
595 if self.layout_exists(at, layout) {
596 self.fits(at, slot, layout, "what a clear zeroes");
597 }
598 }
599 Inst::Neg { num, dst, a } => {
600 let want = Self::numeric(num);
601 self.expect(at, dst, want);
602 self.expect(at, a, want);
603 }
604 Inst::Arith { num, dst, a, b, .. } => {
605 let want = Self::numeric(num);
606 self.expect(at, dst, want);
607 self.expect(at, a, want);
608 self.expect(at, b, want);
609 }
610 // The same claims `Inst::Arith` makes, less the one about an
611 // operand that is not there. `Num` is not a field: an immediate is
612 // an `i64`, so the reading is the integer one, and a `Duration` is
613 // nanoseconds and admitted for the same reason it is there.
614 Inst::ArithImm { dst, a, .. } => {
615 let want = Self::numeric(Num::Int);
616 self.expect(at, dst, want);
617 self.expect(at, a, want);
618 }
619 Inst::CmpImm { dst, a, .. } => {
620 self.expect(at, dst, &[Repr::Bool]);
621 self.expect(at, a, &[Repr::Int, Repr::Duration]);
622 }
623 Inst::Cmp { on, dst, a, b, .. } => {
624 self.expect(at, dst, &[Repr::Bool]);
625 let want: &[Repr] = match on {
626 Compare::Int => &[Repr::Int, Repr::Duration],
627 Compare::Float => &[Repr::Float],
628 Compare::Bool => &[Repr::Bool],
629 Compare::Str => &[Repr::Ref],
630 // `is` compares words, and the only words whose identity
631 // is a language-level question are references.
632 Compare::Identity => &[Repr::Ref],
633 Compare::Tag => &[Repr::Tag],
634 };
635 self.expect(at, a, want);
636 self.expect(at, b, want);
637 }
638 Inst::Not { dst, a } => {
639 self.expect(at, dst, &[Repr::Bool]);
640 self.expect(at, a, &[Repr::Bool]);
641 }
642 Inst::Convert { to, dst, a } => {
643 let (from, into) = match to {
644 crate::inst::Convert::IntToFloat => (Repr::Int, Repr::Float),
645 crate::inst::Convert::FloatToInt => (Repr::Float, Repr::Int),
646 };
647 self.expect(at, a, &[from]);
648 self.expect(at, dst, &[into]);
649 }
650 Inst::Jump { to } => self.target(at, to),
651 Inst::BranchFalse { cond, to } => {
652 self.expect(at, cond, &[Repr::Bool]);
653 self.target(at, to);
654 }
655 Inst::Switch { on, table } => {
656 // The discriminant of an enum location is its first word and
657 // is an `Int`; so is the layout id a `dyn` dispatch switches
658 // on. Nothing else is dispatched on, and a slot's `Repr` is
659 // the strongest thing a static check has to say about which
660 // word this is — a location's extent is a fact about the
661 // instruction that produced the word, not about the frame.
662 self.expect(at, on, &[Repr::Tag, Repr::Int]);
663 if self.in_range(at, table.index(), self.program.tables.len(), "table") {
664 let table = self.program.table(table).clone();
665 for to in table.targets.iter().chain(std::iter::once(&table.default)) {
666 self.target(at, *to);
667 }
668 }
669 }
670 Inst::Return { src } => {
671 let returns = self.function.returns;
672 if self.layout_exists(at, returns) {
673 self.fits(at, src, returns, "what is returned");
674 }
675 }
676 Inst::Call { dst, callee, args } => {
677 if !self.in_range(at, callee.index(), self.program.functions.len(), "function") {
678 return;
679 }
680 let target = self.program.function(callee);
681 let returns = target.returns;
682 let params = target.params.clone();
683 let name = target.qualified();
684 if self.layout_exists(at, returns) {
685 self.fits(at, dst, returns, "the destination of a call");
686 }
687 self.check_args(at, args, ¶ms, &name);
688 }
689 // The callee is a word read out of an object, and the answer's
690 // layout is not: the checker settled this call against the
691 // callee's function type, so how wide the destination has to be
692 // is as static here as at any other call. It is checked the same
693 // way, from the layout the instruction carries.
694 Inst::CallClosure {
695 dst,
696 closure,
697 args,
698 result,
699 } => {
700 self.expect(at, closure, &[Repr::Ref]);
701 if self.layout_exists(at, result) {
702 self.fits(at, dst, result, "the answer of a closure call");
703 }
704 self.each_arg(at, args);
705 }
706 Inst::CallHost { dst, op, args } => {
707 if self.in_range(at, op.index(), self.program.host_ops.len(), "host op") {
708 let result = self.program.host_op(op).result;
709 if self.layout_exists(at, result) {
710 self.fits(at, dst, result, "the answer of a host call");
711 }
712 }
713 self.each_arg(at, args);
714 }
715 // The receiver is a `Repr::Host` word and never an argument: the
716 // registry takes the handle as the thing being addressed and the
717 // host is handed only what follows it. Whether the word names a
718 // resource this run holds is the machine's question, because a
719 // handle is a name the *host* minted and nothing static can say
720 // which one a slot will hold.
721 Inst::CallResource {
722 dst,
723 receiver,
724 op,
725 args,
726 } => {
727 self.expect(at, receiver, &[Repr::Host]);
728 if self.in_range(at, op.index(), self.program.host_ops.len(), "host op") {
729 let held = self.program.host_op(op).clone();
730 if held.resource.is_none() {
731 let named = held.qualified();
732 self.fault(
733 at,
734 format!(
735 "is addressed to a resource, but `{named}` names no resource kind"
736 ),
737 );
738 }
739 if self.layout_exists(at, held.result) {
740 self.fits(at, dst, held.result, "the answer of a host call");
741 }
742 }
743 self.each_arg(at, args);
744 }
745 Inst::CallBuiltin { dst, builtin, args } => {
746 if self.in_range(at, builtin.index(), self.program.builtins.len(), "builtin") {
747 let result = self.program.builtin(builtin).result;
748 if self.layout_exists(at, result) {
749 self.fits(at, dst, result, "the answer of a builtin");
750 }
751 }
752 self.each_arg(at, args);
753 }
754 Inst::Alloc { dst, layout, len } => {
755 self.expect(at, dst, &[Repr::Ref]);
756 if self.layout_exists(at, layout) {
757 let described = self.program.layout(layout);
758 // A box's payload is one word of `LayoutId` and then the
759 // value that layout describes, so its width is in the
760 // header rather than in the shape — and `Alloc` sizes an
761 // object by its shape. Allocating one here would make a
762 // box of a two-word value one word short and the copy
763 // into it would run off the end of the object.
764 // `Inst::Box` is the only correct allocator for one,
765 // because it is the only one that is told what is going
766 // in.
767 if matches!(described.shape, Shape::Boxed) {
768 let name = described.name.clone();
769 self.fault(
770 at,
771 format!(
772 "allocates a `{name}`, whose width the header carries and \
773 the shape does not; a box is allocated by `box`, which \
774 knows what is going into it"
775 ),
776 );
777 }
778 }
779 if let Len::Slot(slot) = len {
780 self.expect(at, slot, &[Repr::Int]);
781 }
782 }
783 Inst::LoadField {
784 dst,
785 obj,
786 at: word,
787 layout,
788 } => {
789 self.expect(at, obj, &[Repr::Ref]);
790 if self.layout_exists(at, layout) {
791 self.fits(at, dst, layout, "what a field is read into");
792 self.reaches(at, obj, word, layout, "read");
793 }
794 }
795 Inst::StoreField {
796 obj,
797 at: word,
798 src,
799 layout,
800 } => {
801 self.expect(at, obj, &[Repr::Ref]);
802 if self.layout_exists(at, layout) {
803 self.fits(at, src, layout, "what a field is written from");
804 self.reaches(at, obj, word, layout, "written");
805 }
806 self.check_closure_callee(at, obj, word, src);
807 }
808 Inst::LoadElem {
809 dst,
810 obj,
811 index,
812 layout,
813 } => {
814 self.expect(at, obj, &[Repr::Ref]);
815 self.expect(at, index, &[Repr::Int]);
816 if self.layout_exists(at, layout) {
817 self.fits(at, dst, layout, "what an element is read into");
818 }
819 }
820 Inst::StoreElem {
821 obj,
822 index,
823 src,
824 layout,
825 } => {
826 self.expect(at, obj, &[Repr::Ref]);
827 self.expect(at, index, &[Repr::Int]);
828 if self.layout_exists(at, layout) {
829 self.fits(at, src, layout, "what an element is written from");
830 }
831 }
832 Inst::ByteAt {
833 dst,
834 obj,
835 at: offset,
836 } => {
837 self.expect(at, obj, &[Repr::Ref]);
838 self.expect(at, offset, &[Repr::Int]);
839 self.expect(at, dst, &[Repr::Int]);
840 }
841 Inst::AllocBytes { dst, len } => {
842 self.expect(at, dst, &[Repr::Ref]);
843 self.expect(at, len, &[Repr::Int]);
844 }
845 Inst::WriteByte {
846 bytes,
847 at: offset,
848 value,
849 } => {
850 self.expect(at, bytes, &[Repr::Ref]);
851 self.expect(at, offset, &[Repr::Int]);
852 self.expect(at, value, &[Repr::Int]);
853 }
854 Inst::CopyBytes { args } => self.check_copy_bytes_args(at, args),
855 Inst::FinishString { dst, bytes } => {
856 self.expect(at, dst, &[Repr::Ref]);
857 self.expect(at, bytes, &[Repr::Ref]);
858 }
859 Inst::AllocBuffer { dst, capacity } => {
860 self.expect(at, dst, &[Repr::Ref]);
861 self.expect(at, capacity, &[Repr::Int]);
862 }
863 Inst::AppendByte { buffer, value } => {
864 self.expect(at, buffer, &[Repr::Ref]);
865 self.expect(at, value, &[Repr::Int]);
866 }
867 Inst::AppendBytes { args } => self.check_append_bytes_args(at, args),
868 Inst::FinishBuffer { dst, buffer } => {
869 self.expect(at, dst, &[Repr::Ref]);
870 self.expect(at, buffer, &[Repr::Ref]);
871 }
872 Inst::Len { dst, obj } => {
873 self.expect(at, obj, &[Repr::Ref]);
874 self.expect(at, dst, &[Repr::Int]);
875 }
876 Inst::LayoutOf { dst, obj } => {
877 self.expect(at, obj, &[Repr::Ref]);
878 self.expect(at, dst, &[Repr::Int]);
879 }
880 Inst::AddrOfSlot { dst, slot } => {
881 self.expect(at, dst, &[Repr::Addr]);
882 self.repr(at, slot);
883 }
884 Inst::AddrOfField { dst, obj, at: word } => {
885 self.expect(at, dst, &[Repr::Addr]);
886 self.expect(at, obj, &[Repr::Ref]);
887 self.reaches_word(at, obj, word, 1, "addressed");
888 }
889 Inst::AddrOfElem {
890 dst,
891 obj,
892 index,
893 layout,
894 } => {
895 self.expect(at, dst, &[Repr::Addr]);
896 self.expect(at, obj, &[Repr::Ref]);
897 self.expect(at, index, &[Repr::Int]);
898 self.layout_exists(at, layout);
899 }
900 // Nothing bounds `at` against the value the address names. A
901 // frame records what each slot *holds* and not how far the value
902 // an address points into reaches, so the extent is a fact about
903 // the instruction that formed the address rather than about this
904 // function — the same limit `Inst::Switch`'s operand is under.
905 Inst::AddrOfPart { dst, addr, .. } => {
906 self.expect(at, dst, &[Repr::Addr]);
907 self.expect(at, addr, &[Repr::Addr]);
908 }
909 Inst::Load { dst, addr, layout } => {
910 self.expect(at, addr, &[Repr::Addr]);
911 if self.layout_exists(at, layout) {
912 self.fits(at, dst, layout, "what a load answers");
913 }
914 }
915 Inst::Store { addr, src, layout } => {
916 self.expect(at, addr, &[Repr::Addr]);
917 if self.layout_exists(at, layout) {
918 self.fits(at, src, layout, "what a store writes");
919 }
920 }
921 Inst::Box { dst, src, layout } => {
922 self.expect(at, dst, &[Repr::Ref]);
923 if self.layout_exists(at, layout) {
924 self.fits(at, src, layout, "what is boxed");
925 }
926 }
927 Inst::Unbox { dst, src, layout } => {
928 self.expect(at, src, &[Repr::Ref]);
929 if self.layout_exists(at, layout) {
930 self.fits(at, dst, layout, "what a box is opened into");
931 }
932 }
933 // ---- tasks ---------------------------------------------------
934 Inst::ScopeEnter { dst, name } => {
935 self.expect(at, dst, &[Repr::Scope]);
936 self.in_range(at, name.index(), self.program.strings.len(), "string");
937 }
938 Inst::ScopeCancel { scope } => self.expect(at, scope, &[Repr::Scope]),
939 // The error location is the *enclosing* function's `Err`
940 // payload, not the child's answer: what a failing child gives
941 // the scope is a value to pass on, and where it goes is decided
942 // by the function the scope was written in. The machine holds
943 // the child's own layout to this one and refuses a disagreement,
944 // because a run of words copied at the wrong width is the one
945 // fault this crate exists to make loud.
946 Inst::ScopeLeave {
947 scope,
948 failed,
949 error,
950 layout,
951 } => {
952 self.expect(at, scope, &[Repr::Scope]);
953 self.expect(at, failed, &[Repr::Bool]);
954 if self.layout_exists(at, layout) {
955 self.fits(at, error, layout, "what a failing child leaves");
956 }
957 }
958 Inst::Spawn {
959 dst,
960 scope,
961 closure,
962 answer,
963 } => {
964 self.expect(at, dst, &[Repr::Task]);
965 self.expect(at, scope, &[Repr::Scope]);
966 self.expect(at, closure, &[Repr::Ref]);
967 self.layout_exists(at, answer);
968 }
969 Inst::Await { dst, task, answer } => {
970 self.expect(at, task, &[Repr::Task]);
971 if self.layout_exists(at, answer) {
972 self.fits(at, dst, answer, "what an await answers");
973 }
974 }
975 Inst::Cancel { task } => self.expect(at, task, &[Repr::Task]),
976 // The words go into an object of the same shape a spawned
977 // task's answer goes into, so the same question is asked of
978 // them: that the location they are read out of is as wide as
979 // the layout says.
980 Inst::Settled { dst, src, answer } => {
981 self.expect(at, dst, &[Repr::Task]);
982 if self.layout_exists(at, answer) {
983 self.fits(at, src, answer, "what a settled task answers");
984 }
985 }
986
987 // ---- cells ---------------------------------------------------
988 // A cell is an ordinary object in the run's heap, so the operand
989 // is an ordinary `Repr::Ref` word. That the two come in pairs is
990 // not checked here: which cells a path is holding is a fact about
991 // control flow, and this is a fact about one instruction — the
992 // same limit `Inst::ScopeCancel` is under.
993 Inst::SharedLock { cell } | Inst::SharedUnlock { cell } => {
994 self.expect(at, cell, &[Repr::Ref])
995 }
996
997 Inst::Trap { message } => {
998 self.in_range(at, message.index(), self.program.strings.len(), "string");
999 }
1000 Inst::AssertFailed { message } => {
1001 self.expect(at, message, &[Repr::Ref]);
1002 }
1003 }
1004 }
1005
1006 fn numeric(num: Num) -> &'static [Repr] {
1007 match num {
1008 // A `Duration` is nanoseconds, and nanoseconds add like
1009 // integers. Only the boundary cares what the answer is called.
1010 Num::Int => &[Repr::Int, Repr::Duration],
1011 Num::Float => &[Repr::Float],
1012 }
1013 }
1014
1015 /// Whether `layout` names an entry of the program's layout table.
1016 fn layout_exists(&mut self, at: Option<usize>, layout: LayoutId) -> bool {
1017 self.in_range(at, layout.index(), self.program.layouts.len(), "layout")
1018 }
1019
1020 /// Whether the location at `slot` is a value of `layout`: it is inside
1021 /// the frame, and its words are the layout's words in order.
1022 ///
1023 /// This is the check the whole representation turns on. A location is a
1024 /// base slot and a layout, and the frame's per-slot reprs are what a
1025 /// collection reads — so a location whose words disagree with what is
1026 /// being moved into it is a reference the collector will miss or a
1027 /// scalar it will follow.
1028 fn fits(&mut self, at: Option<usize>, slot: Slot, layout: LayoutId, what: &str) -> bool {
1029 let words = self.program.layout(layout).words.clone();
1030 let name = self.program.layout(layout).name.clone();
1031 let size = self.function.frame_size();
1032 if slot as u64 + words.len() as u64 > size as u64 {
1033 self.fault(
1034 at,
1035 format!(
1036 "{what} is `{name}`, {} words at slot {slot}, and the frame has {size}",
1037 words.len()
1038 ),
1039 );
1040 return false;
1041 }
1042 for (offset, want) in words.iter().enumerate() {
1043 let found = self.function.reprs[slot as usize + offset];
1044 if found != *want {
1045 self.fault(
1046 at,
1047 format!(
1048 "{what} is `{name}`, whose word {offset} is {want}, but slot {} holds \
1049 {found}",
1050 slot as usize + offset
1051 ),
1052 );
1053 return false;
1054 }
1055 }
1056 true
1057 }
1058
1059 /// What slot `slot` holds, reporting a slot outside the frame.
1060 fn repr(&mut self, at: Option<usize>, slot: Slot) -> Option<Repr> {
1061 match self.function.repr(slot) {
1062 Some(repr) => Some(repr),
1063 None => {
1064 let size = self.function.frame_size();
1065 self.fault(at, format!("names slot {slot}, outside a frame of {size}"));
1066 None
1067 }
1068 }
1069 }
1070
1071 fn expect(&mut self, at: Option<usize>, slot: Slot, want: &[Repr]) {
1072 let Some(found) = self.repr(at, slot) else {
1073 return;
1074 };
1075 if !want.contains(&found) {
1076 let names: Vec<&str> = want.iter().map(|repr| repr.name()).collect();
1077 self.fault(
1078 at,
1079 format!(
1080 "slot {slot} holds {found}, but this wants {}",
1081 names.join(" or ")
1082 ),
1083 );
1084 }
1085 }
1086
1087 fn target(&mut self, at: Option<usize>, to: u32) {
1088 if to as usize >= self.function.code.len() {
1089 let len = self.function.code.len();
1090 self.fault(at, format!("jumps to {to}, past the {len} instructions"));
1091 }
1092 }
1093
1094 fn in_range(&mut self, at: Option<usize>, index: usize, len: usize, what: &str) -> bool {
1095 if index >= len {
1096 self.fault(at, format!("names {what} {index}, and there are {len}"));
1097 false
1098 } else {
1099 true
1100 }
1101 }
1102
1103 /// Whether a field access at word `word` of `obj` stays inside the
1104 /// object, where what `obj` holds is a static fact.
1105 ///
1106 /// The width is the layout being moved, so this is the whole run and not
1107 /// only its first word: reading a two-word `Point` out of the last word
1108 /// of an object reads one word of whatever the allocator put after it.
1109 fn reaches(&mut self, at: Option<usize>, obj: Slot, word: u32, layout: LayoutId, what: &str) {
1110 let width = self.program.layout(layout).width();
1111 self.reaches_word(at, obj, word, width, what);
1112 }
1113
1114 /// The same, for a run of a width the caller already knows.
1115 ///
1116 /// Silent where the object's layout is not static, or where it is but the
1117 /// header's `len` is what decides how many payload words it has: a
1118 /// `Shape::Str` or a `Shape::Elements` object is as long as it was
1119 /// allocated, and only the machine has the header to ask. Those are the
1120 /// accesses the machine's own bounds check answers.
1121 fn reaches_word(&mut self, at: Option<usize>, obj: Slot, word: u32, width: u32, what: &str) {
1122 let Some(Some(id)) = self.objects.get(obj as usize).copied() else {
1123 return;
1124 };
1125 let described = self.program.layout(id);
1126 let Some(words) = described.fixed_payload_words(&self.program.layouts) else {
1127 return;
1128 };
1129 if word as u64 + width as u64 > words as u64 {
1130 let name = described.name.clone();
1131 self.fault(
1132 at,
1133 format!("{what} {width} word(s) at word {word} of a `{name}`, which has {words}"),
1134 );
1135 }
1136 }
1137
1138 /// When `obj` is known to be a [`Shape::Closure`] and `word` is its
1139 /// callee field, checks that `src` is a known [`Inst::FuncRef`] naming
1140 /// the same callee the closure's own layout does.
1141 ///
1142 /// This is the comparison the module doc calls out: a closure's callee
1143 /// is carried twice, once in its [`Shape::Closure::function`] and once in
1144 /// the word [`Inst::FuncRef`] writes into its environment, and until this
1145 /// nothing checked the two agreed. It is silent whenever either half is
1146 /// not a static fact — `obj`'s layout from [`Check::objects`], `src`'s
1147 /// callee from [`Check::funcs`] — for the reason [`Check::slot_facts`]
1148 /// declines to guess there: a slot written by more than one thing, or by
1149 /// something this analysis was not taught, answers `None` rather than a
1150 /// wrong guess.
1151 fn check_closure_callee(&mut self, at: Option<usize>, obj: Slot, word: u32, src: Slot) {
1152 let Some(Some(layout_id)) = self.objects.get(obj as usize).copied() else {
1153 return;
1154 };
1155 let described = self.program.layout(layout_id);
1156 let Shape::Closure { function, .. } = &described.shape else {
1157 return;
1158 };
1159 // Payload word 0 is the callee's `FunctionId`; see `Shape::Closure`.
1160 if word != 0 {
1161 return;
1162 }
1163 let Some(Some(callee)) = self.funcs.get(src as usize).copied() else {
1164 return;
1165 };
1166 if callee != *function {
1167 let name = described.name.clone();
1168 // Symbolic, not `FunctionId`'s bare `Display` — the whole point
1169 // issue #275 makes of this message, which is otherwise the last
1170 // place in the crate a diagnostic still named a function by its
1171 // position in `Program::functions`. The fallback to the raw id
1172 // mirrors `print::name_of`'s for a `LayoutId`: this runs over a
1173 // program the verifier has not yet vouched for, so a fault about
1174 // a callee that is itself out of range should say so rather than
1175 // panic indexing into the table it is complaining about.
1176 let named = |id: FunctionId| match self.program.functions.get(id.index()) {
1177 Some(f) => format!("@{}", f.qualified()),
1178 None => id.to_string(),
1179 };
1180 self.fault(
1181 at,
1182 format!(
1183 "stores {} into the callee field of a `{name}` closure, whose layout \
1184 names {}",
1185 named(callee),
1186 named(*function)
1187 ),
1188 );
1189 }
1190 }
1191
1192 /// Every argument is a value location of the layout it names, and that
1193 /// location is inside the frame.
1194 ///
1195 /// This is what an argument carrying its layout buys the verifier. It
1196 /// used to check only that the slot existed, because a slot was the whole
1197 /// of what an argument was — so a call passing the last slot of a frame
1198 /// as a two-word `Point` was checked by nothing, and the machine read the
1199 /// frame above it.
1200 fn each_arg(&mut self, at: Option<usize>, args: crate::ArgsId) {
1201 if !self.in_range(at, args.index(), self.program.args.len(), "argument list") {
1202 return;
1203 }
1204 for (index, arg) in self.program.arg_list(args).to_vec().into_iter().enumerate() {
1205 if self.layout_exists(at, arg.layout) {
1206 self.fits(at, arg.slot, arg.layout, &format!("argument {index}"));
1207 }
1208 }
1209 }
1210
1211 /// The same, where the callee declares what it takes: each argument's
1212 /// layout is the parameter's, and its location is a value of it.
1213 ///
1214 /// The layouts are compared rather than only the locations' words,
1215 /// because two layouts can have the same words and not be the same
1216 /// family — an `Error` and a `String` are both one `Repr::Ref` — and it
1217 /// is the argument's layout that the machine hands a builtin and a host.
1218 /// The copy into the callee's frame is made at the *parameter's* width:
1219 /// the frame being written is the callee's, and only `Function::params`
1220 /// is a fact about the callee. This is what makes the two agree.
1221 fn check_args(
1222 &mut self,
1223 at: Option<usize>,
1224 args: crate::ArgsId,
1225 want: &[LayoutId],
1226 name: &str,
1227 ) {
1228 if !self.in_range(at, args.index(), self.program.args.len(), "argument list") {
1229 return;
1230 }
1231 let passed = self.program.arg_list(args).to_vec();
1232 if passed.len() != want.len() {
1233 self.fault(
1234 at,
1235 format!(
1236 "passes {} arguments to `{name}`, which declares {}",
1237 passed.len(),
1238 want.len()
1239 ),
1240 );
1241 return;
1242 }
1243 for (index, (arg, layout)) in passed.into_iter().zip(want).enumerate() {
1244 if !self.layout_exists(at, *layout) {
1245 continue;
1246 }
1247 if arg.layout != *layout {
1248 let passed = self.name_of(arg.layout);
1249 let declared = self.program.layout(*layout).name.clone();
1250 self.fault(
1251 at,
1252 format!(
1253 "argument {index} of `{name}` is passed as a `{passed}`, and the \
1254 parameter is a `{declared}`"
1255 ),
1256 );
1257 continue;
1258 }
1259 self.fits(
1260 at,
1261 arg.slot,
1262 *layout,
1263 &format!("argument {index} of `{name}`"),
1264 );
1265 }
1266 }
1267
1268 /// [`Inst::CopyBytes`]'s five arguments: `dst`, `dst_at`, `src`,
1269 /// `src_at`, `len`, in that order.
1270 ///
1271 /// Checked by `Repr` rather than by [`Self::check_args`]'s declared
1272 /// [`LayoutId`], because `dst` and `src` do not have one: `src` may be a
1273 /// `String` or another [`crate::Shape::Bytes`] run, and which of the two
1274 /// is a run-time fact rather than something a lowering could declare the
1275 /// way a call declares its parameters. What is static is that both are
1276 /// references and the other three are integers, so that is what this
1277 /// asks.
1278 fn check_copy_bytes_args(&mut self, at: Option<usize>, args: crate::ArgsId) {
1279 if !self.in_range(at, args.index(), self.program.args.len(), "argument list") {
1280 return;
1281 }
1282 const NAMES: [&str; 5] = ["dst", "dst_at", "src", "src_at", "len"];
1283 const WANTS: [Repr; 5] = [Repr::Ref, Repr::Int, Repr::Ref, Repr::Int, Repr::Int];
1284 let passed = self.program.arg_list(args).to_vec();
1285 if passed.len() != NAMES.len() {
1286 self.fault(
1287 at,
1288 format!(
1289 "copies bytes with {} argument(s), and this needs {} ({})",
1290 passed.len(),
1291 NAMES.len(),
1292 NAMES.join(", ")
1293 ),
1294 );
1295 return;
1296 }
1297 for (arg, want) in passed.iter().zip(WANTS) {
1298 self.expect(at, arg.slot, &[want]);
1299 }
1300 }
1301
1302 /// [`Inst::AppendBytes`]'s four arguments: `buffer`, `src`, `from`, `to`,
1303 /// in that order.
1304 ///
1305 /// Checked by `Repr` rather than by declared [`LayoutId`], for
1306 /// [`Self::check_copy_bytes_args`]'s reason: `src` may be a `String` or a
1307 /// [`crate::Shape::Bytes`] run and which of the two is a run-time fact. So
1308 /// is whether `buffer` names a real owner; what is static is that both are
1309 /// references and both offsets are integers.
1310 fn check_append_bytes_args(&mut self, at: Option<usize>, args: crate::ArgsId) {
1311 if !self.in_range(at, args.index(), self.program.args.len(), "argument list") {
1312 return;
1313 }
1314 const NAMES: [&str; 4] = ["buffer", "src", "from", "to"];
1315 const WANTS: [Repr; 4] = [Repr::Ref, Repr::Ref, Repr::Int, Repr::Int];
1316 let passed = self.program.arg_list(args).to_vec();
1317 if passed.len() != NAMES.len() {
1318 self.fault(
1319 at,
1320 format!(
1321 "appends bytes with {} argument(s), and this needs {} ({})",
1322 passed.len(),
1323 NAMES.len(),
1324 NAMES.join(", ")
1325 ),
1326 );
1327 return;
1328 }
1329 for (arg, want) in passed.iter().zip(WANTS) {
1330 self.expect(at, arg.slot, &[want]);
1331 }
1332 }
1333
1334 /// What a layout is called, or its id where the table is too short.
1335 fn name_of(&self, layout: LayoutId) -> String {
1336 match self.program.layouts.get(layout.index()) {
1337 Some(held) => held.name.to_string(),
1338 None => layout.to_string(),
1339 }
1340 }
1341}
1342
1343/// The id of the function being checked is carried so that a future fault
1344/// can name it by id as well as by name; nothing reads it yet.
1345impl Check<'_> {
1346 #[allow(dead_code)]
1347 fn id(&self) -> FunctionId {
1348 self.id
1349 }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use std::sync::Arc;
1355
1356 use cove_diag::{FileId, Span};
1357
1358 use super::*;
1359 use crate::inst::{ArithOp, CmpOp, Compare, Inst, Num};
1360 use crate::layout::{Case, Layout, Shape};
1361 use crate::program::{Arg, Function, HostOp, Local, Table, TableId};
1362 use crate::{ArgsId, HostOpId};
1363
1364 const INT: LayoutId = LayoutId(0);
1365 const STR: LayoutId = LayoutId(1);
1366 const POINT: LayoutId = LayoutId(2);
1367 /// `[disc: Int, Ref]`, the shape an `Option<String>` has.
1368 const ANSWER: LayoutId = LayoutId(3);
1369 /// A second two-`Int` struct: the same words as [`POINT`] and a different
1370 /// family, which is what an argument's layout is checked against.
1371 const PAIR: LayoutId = LayoutId(4);
1372 const BOXED: LayoutId = LayoutId(5);
1373 /// A closure over nothing, whose layout says its callee is `FunctionId(1)`
1374 /// — the second function [`program`] is given, in the tests that need
1375 /// one. See [`Check::check_closure_callee`].
1376 const CLOSURE: LayoutId = LayoutId(6);
1377 /// A two-case enum, for the checks a case index needs an enum to make.
1378 const ENUM: LayoutId = LayoutId(7);
1379
1380 fn layouts() -> Vec<Layout> {
1381 vec![
1382 Layout::word("Int", Repr::Int),
1383 Layout::object("String", Shape::Str),
1384 Layout::inline(
1385 "Point",
1386 Shape::Struct {
1387 fields: Vec::new(),
1388 opaque: false,
1389 },
1390 vec![Repr::Int, Repr::Int],
1391 ),
1392 Layout::inline(
1393 "Option",
1394 Shape::Enum {
1395 cases: Vec::new(),
1396 payload: vec![Repr::Ref],
1397 },
1398 vec![Repr::Int, Repr::Ref],
1399 ),
1400 Layout::inline(
1401 "Pair",
1402 Shape::Struct {
1403 fields: Vec::new(),
1404 opaque: false,
1405 },
1406 vec![Repr::Int, Repr::Int],
1407 ),
1408 Layout::object("Any", Shape::Boxed),
1409 Layout::object(
1410 "closure g",
1411 Shape::Closure {
1412 function: FunctionId(1),
1413 captures: Vec::new(),
1414 },
1415 ),
1416 Layout::inline(
1417 "m.E",
1418 Shape::Enum {
1419 cases: vec![
1420 Case {
1421 name: Arc::from("A"),
1422 parts: Vec::new(),
1423 },
1424 Case {
1425 name: Arc::from("B"),
1426 parts: Vec::new(),
1427 },
1428 ],
1429 payload: vec![Repr::Int],
1430 },
1431 vec![Repr::Tag, Repr::Int],
1432 ),
1433 ]
1434 }
1435
1436 fn span() -> Span {
1437 Span::new(FileId(0), 0, 0)
1438 }
1439
1440 fn function(reprs: Vec<Repr>, returns: LayoutId, code: Vec<Inst>) -> Function {
1441 Function {
1442 module: Arc::from("m"),
1443 name: Arc::from("f"),
1444 params: Vec::new(),
1445 spans: vec![span(); code.len()],
1446 refs: RefMap::of(&reprs),
1447 reprs,
1448 returns,
1449 captures: Vec::new(),
1450 code,
1451 locals: Vec::new(),
1452 inlined: Vec::new(),
1453 span: span(),
1454 is_async: false,
1455 stub: false,
1456 }
1457 }
1458
1459 fn program(functions: Vec<Function>) -> Program {
1460 Program {
1461 functions,
1462 layouts: layouts(),
1463 str_layout: STR,
1464 boxed_layout: BOXED,
1465 ..Program::default()
1466 }
1467 }
1468
1469 fn faults(program: &Program) -> Vec<String> {
1470 match verify(program) {
1471 Ok(()) => Vec::new(),
1472 Err(items) => items.into_iter().map(|item| item.what).collect(),
1473 }
1474 }
1475
1476 /// A resource operation is addressed to a `Repr::Host` word, and the
1477 /// operation it names has to be one a resource answers.
1478 ///
1479 /// Neither is a fact about the *handle*: which resource a word names is
1480 /// the host's business and nothing static can say it. What is static is
1481 /// that the receiver holds a name at all and that the call site settled a
1482 /// resource kind, and both are lowering bugs rather than program faults.
1483 #[test]
1484 fn a_resource_call_is_addressed_to_a_host_word_and_names_a_resource() {
1485 let mut held = program(vec![function(
1486 vec![Repr::Int, Repr::Int],
1487 INT,
1488 vec![
1489 Inst::CallResource {
1490 dst: 0,
1491 receiver: 1,
1492 op: HostOpId(0),
1493 args: ArgsId(0),
1494 },
1495 Inst::Return { src: 0 },
1496 ],
1497 )]);
1498 held.args.push(Vec::new());
1499 held.host_ops.push(HostOp {
1500 module: Arc::from("files"),
1501 operation: Arc::from("write"),
1502 resource: None,
1503 result: INT,
1504 });
1505 assert_eq!(
1506 faults(&held),
1507 vec![
1508 "slot 1 holds int, but this wants host".to_string(),
1509 "is addressed to a resource, but `files.write` names no resource kind".to_string(),
1510 ]
1511 );
1512 }
1513
1514 /// The same call, well formed.
1515 #[test]
1516 fn a_resource_call_that_names_a_kind_and_a_handle_is_well_formed() {
1517 let mut held = program(vec![function(
1518 vec![Repr::Int, Repr::Host],
1519 INT,
1520 vec![
1521 Inst::CallResource {
1522 dst: 0,
1523 receiver: 1,
1524 op: HostOpId(0),
1525 args: ArgsId(0),
1526 },
1527 Inst::Return { src: 0 },
1528 ],
1529 )]);
1530 held.args.push(Vec::new());
1531 held.host_ops.push(HostOp {
1532 module: Arc::from("files"),
1533 operation: Arc::from("write"),
1534 resource: Some(Arc::from("Writer")),
1535 result: INT,
1536 });
1537 assert_eq!(faults(&held), Vec::<String>::new());
1538 assert_eq!(held.host_op(HostOpId(0)).qualified(), "files.Writer.write");
1539 }
1540
1541 #[test]
1542 fn a_well_formed_function_has_nothing_to_say_about_it() {
1543 let f = function(
1544 vec![Repr::Int, Repr::Int],
1545 POINT,
1546 vec![Inst::Return { src: 0 }],
1547 );
1548 assert_eq!(faults(&program(vec![f])), Vec::<String>::new());
1549 }
1550
1551 #[test]
1552 fn a_copy_whose_destination_is_not_the_layout_s_words_is_a_fault() {
1553 // The whole representation turns on this: a location is a base slot
1554 // and a layout, and a copy of the wrong width is a reference the
1555 // collector will miss or a scalar it will follow.
1556 let f = function(
1557 vec![Repr::Int, Repr::Ref, Repr::Int, Repr::Int, Repr::Unit],
1558 INT,
1559 vec![
1560 Inst::Copy {
1561 dst: 0,
1562 src: 2,
1563 layout: POINT,
1564 },
1565 Inst::Return { src: 4 },
1566 ],
1567 );
1568 assert_eq!(
1569 faults(&program(vec![f])),
1570 vec![
1571 "the destination of a copy is `Point`, whose word 1 is int, but slot 1 holds ref"
1572 .to_string(),
1573 "what is returned is `Int`, whose word 0 is int, but slot 4 holds unit".to_string(),
1574 ]
1575 );
1576 }
1577
1578 #[test]
1579 fn a_location_that_runs_off_the_end_of_the_frame_is_a_fault() {
1580 let f = function(
1581 vec![Repr::Int, Repr::Int],
1582 INT,
1583 vec![
1584 Inst::Copy {
1585 dst: 1,
1586 src: 0,
1587 layout: POINT,
1588 },
1589 Inst::Return { src: 0 },
1590 ],
1591 );
1592 assert_eq!(
1593 faults(&program(vec![f])),
1594 vec!["the destination of a copy is `Point`, 2 words at slot 1, and the frame has 2"]
1595 );
1596 }
1597
1598 #[test]
1599 fn a_reference_map_that_disagrees_with_the_reprs_is_a_fault() {
1600 // A collection walks frames using the map, so a lowering that wrote
1601 // a reference into a slot the map calls an `Int` would produce a
1602 // dangling reference at the next collection.
1603 let mut f = function(vec![Repr::Ref], STR, vec![Inst::Return { src: 0 }]);
1604 f.refs = RefMap::of(&[Repr::Int]);
1605 assert_eq!(
1606 faults(&program(vec![f])),
1607 vec![
1608 "reference map disagrees with the frame's reprs, so a collection would scan the \
1609 wrong slots"
1610 ]
1611 );
1612 }
1613
1614 /// A local names a location and a stretch of code, and both have to be
1615 /// there. Nothing runs one — it is read when a person asks what a frame
1616 /// holds — so the fault it prevents is not a wrong answer at a
1617 /// collection but a debugger reading a slot or an instruction that does
1618 /// not exist.
1619 #[test]
1620 fn a_local_outside_the_frame_or_past_the_last_instruction_is_a_fault() {
1621 let mut f = function(
1622 vec![Repr::Int, Repr::Int],
1623 INT,
1624 vec![Inst::Return { src: 0 }],
1625 );
1626 f.locals = vec![
1627 Local {
1628 name: Arc::from("wide"),
1629 slot: 1,
1630 layout: POINT,
1631 from: 0,
1632 to: 1,
1633 },
1634 Local {
1635 name: Arc::from("late"),
1636 slot: 0,
1637 layout: INT,
1638 from: 0,
1639 to: 4,
1640 },
1641 ];
1642 assert_eq!(
1643 faults(&program(vec![f])),
1644 vec![
1645 "local `wide` is `Point`, 2 words at slot 1, and the frame has 2".to_string(),
1646 "local `late` is live to 4 and the function has 1 instructions".to_string(),
1647 ]
1648 );
1649 }
1650
1651 /// And a range has to be one: `[from, to)` is half-open, so `from > to`
1652 /// is not an empty binding but a table nothing can be read out of.
1653 #[test]
1654 fn a_local_bound_after_it_is_freed_is_a_fault() {
1655 let mut f = function(vec![Repr::Int], INT, vec![Inst::Return { src: 0 }]);
1656 f.locals = vec![Local {
1657 name: Arc::from("backwards"),
1658 slot: 0,
1659 layout: INT,
1660 from: 1,
1661 to: 0,
1662 }];
1663 assert_eq!(
1664 faults(&program(vec![f])),
1665 vec!["local `backwards` is bound at 1 and freed at 0"]
1666 );
1667 }
1668
1669 #[test]
1670 fn a_call_whose_arguments_are_not_the_callee_s_parameters_is_a_fault() {
1671 let mut callee = function(
1672 vec![Repr::Int, Repr::Int, Repr::Int],
1673 INT,
1674 vec![Inst::Return { src: 2 }],
1675 );
1676 callee.params = vec![POINT];
1677 callee.name = Arc::from("g");
1678 let caller = function(
1679 vec![Repr::Int, Repr::Ref, Repr::Int],
1680 INT,
1681 vec![
1682 Inst::Call {
1683 dst: 0,
1684 callee: FunctionId(0),
1685 args: crate::ArgsId(0),
1686 },
1687 Inst::Return { src: 0 },
1688 ],
1689 );
1690 let mut held = program(vec![callee, caller]);
1691 held.args = vec![vec![Arg {
1692 slot: 1,
1693 layout: POINT,
1694 }]];
1695 assert_eq!(
1696 faults(&held),
1697 vec![
1698 "argument 0 of `m.g` is `Point`, whose word 0 is int, but slot 1 holds ref"
1699 .to_string()
1700 ]
1701 );
1702 }
1703
1704 #[test]
1705 fn a_call_that_passes_the_wrong_number_of_arguments_is_a_fault() {
1706 let mut callee = function(
1707 vec![Repr::Int, Repr::Int],
1708 INT,
1709 vec![Inst::Return { src: 1 }],
1710 );
1711 callee.params = vec![INT];
1712 callee.name = Arc::from("g");
1713 let caller = function(
1714 vec![Repr::Int],
1715 INT,
1716 vec![
1717 Inst::Call {
1718 dst: 0,
1719 callee: FunctionId(0),
1720 args: crate::ArgsId(0),
1721 },
1722 Inst::Return { src: 0 },
1723 ],
1724 );
1725 let mut held = program(vec![callee, caller]);
1726 held.args = vec![Vec::new()];
1727 assert_eq!(
1728 faults(&held),
1729 vec!["passes 0 arguments to `m.g`, which declares 1"]
1730 );
1731 }
1732
1733 /// The whole of why a discriminant is a `Repr` of its own.
1734 ///
1735 /// A tag and an `Int` are the same bits in the same kind of word, and
1736 /// before this they were the same *type*, so nothing stopped an enum's
1737 /// case index being added to. Nothing rejects it at run time either —
1738 /// the machine adds two words — so the only place it can be caught is
1739 /// here.
1740 #[test]
1741 fn a_tag_cannot_be_added_to() {
1742 let f = function(
1743 vec![Repr::Int, Repr::Ref, Repr::Tag],
1744 ANSWER,
1745 vec![
1746 Inst::Arith {
1747 num: Num::Int,
1748 op: ArithOp::Add,
1749 dst: 0,
1750 a: 2,
1751 b: 0,
1752 },
1753 Inst::Return { src: 0 },
1754 ],
1755 );
1756 assert_eq!(
1757 faults(&program(vec![f])),
1758 vec!["slot 2 holds tag, but this wants int or duration"]
1759 );
1760 }
1761
1762 /// And cannot be ordered, or compared against an integer.
1763 ///
1764 /// Equality is refused with the rest: two tags are compared by
1765 /// dispatching on one, which is what [`Inst::Switch`] is, and a tag
1766 /// against an `Int` is the confusion this separation exists to name.
1767 #[test]
1768 fn a_tag_cannot_be_ordered_or_compared_as_an_integer() {
1769 let ordered = function(
1770 vec![Repr::Int, Repr::Ref, Repr::Tag, Repr::Bool],
1771 ANSWER,
1772 vec![
1773 Inst::Cmp {
1774 on: Compare::Int,
1775 op: CmpOp::Lt,
1776 dst: 3,
1777 a: 2,
1778 b: 0,
1779 },
1780 Inst::Return { src: 0 },
1781 ],
1782 );
1783 assert_eq!(
1784 faults(&program(vec![ordered])),
1785 vec!["slot 2 holds tag, but this wants int or duration"]
1786 );
1787
1788 let equal = function(
1789 vec![Repr::Int, Repr::Ref, Repr::Tag, Repr::Bool],
1790 ANSWER,
1791 vec![
1792 Inst::Cmp {
1793 on: Compare::Int,
1794 op: CmpOp::Eq,
1795 dst: 3,
1796 a: 2,
1797 b: 0,
1798 },
1799 Inst::Return { src: 0 },
1800 ],
1801 );
1802 assert_eq!(
1803 faults(&program(vec![equal])),
1804 vec!["slot 2 holds tag, but this wants int or duration"]
1805 );
1806 }
1807
1808 /// What a tag *is* accepted by: the instruction that writes one, and the
1809 /// one that dispatches on it. A test that only showed the refusals would
1810 /// pass if the whole family were rejected.
1811 #[test]
1812 fn a_tag_is_written_and_dispatched_on() {
1813 let f = function(
1814 vec![Repr::Int, Repr::Ref, Repr::Tag],
1815 ANSWER,
1816 vec![
1817 Inst::Tag {
1818 dst: 2,
1819 layout: ENUM,
1820 case: crate::CaseId(1),
1821 },
1822 Inst::Switch {
1823 on: 2,
1824 table: TableId(0),
1825 },
1826 Inst::Return { src: 0 },
1827 ],
1828 );
1829 let mut held = program(vec![f]);
1830 held.tables = vec![Table {
1831 targets: vec![2, 2],
1832 default: 2,
1833 }];
1834 assert_eq!(faults(&held), Vec::<String>::new());
1835 }
1836
1837 /// A case index is bounded against the enum the same instruction names,
1838 /// which is the check the untyped integer path had no way to make.
1839 #[test]
1840 fn a_tag_naming_a_case_the_enum_does_not_have_is_a_fault() {
1841 let f = function(
1842 vec![Repr::Int, Repr::Ref, Repr::Tag],
1843 ANSWER,
1844 vec![
1845 Inst::Tag {
1846 dst: 2,
1847 layout: ENUM,
1848 case: crate::CaseId(7),
1849 },
1850 Inst::Return { src: 0 },
1851 ],
1852 );
1853 assert_eq!(
1854 faults(&program(vec![f])),
1855 vec!["names case7 of m.E, which has 2 case(s)"]
1856 );
1857 }
1858
1859 #[test]
1860 fn a_switch_on_something_that_is_not_a_discriminant_word_is_a_fault() {
1861 // The discriminant of an enum location is its first word and is an
1862 // `Int`; so is the layout id a `dyn` dispatch switches on. A slot's
1863 // `Repr` is the strongest thing a static check has to say about
1864 // which word this is.
1865 let f = function(
1866 vec![Repr::Int, Repr::Ref],
1867 ANSWER,
1868 vec![
1869 Inst::Switch {
1870 on: 1,
1871 table: TableId(0),
1872 },
1873 Inst::Return { src: 0 },
1874 ],
1875 );
1876 let mut held = program(vec![f]);
1877 held.tables = vec![Table {
1878 targets: vec![1],
1879 default: 1,
1880 }];
1881 assert_eq!(
1882 faults(&held),
1883 vec!["slot 1 holds ref, but this wants tag or int"]
1884 );
1885 }
1886
1887 #[test]
1888 fn a_jump_that_lands_past_the_last_instruction_is_a_fault() {
1889 let f = function(
1890 vec![Repr::Int],
1891 INT,
1892 vec![Inst::Jump { to: 9 }, Inst::Return { src: 0 }],
1893 );
1894 assert_eq!(
1895 faults(&program(vec![f])),
1896 vec!["jumps to 9, past the 2 instructions"]
1897 );
1898 }
1899
1900 #[test]
1901 fn an_id_outside_its_table_is_a_fault() {
1902 let f = function(
1903 vec![Repr::Ref],
1904 STR,
1905 vec![
1906 Inst::Str {
1907 dst: 0,
1908 text: crate::StrId(3),
1909 },
1910 Inst::Return { src: 0 },
1911 ],
1912 );
1913 assert_eq!(
1914 faults(&program(vec![f])),
1915 vec!["names string 3, and there are 0"]
1916 );
1917 }
1918
1919 #[test]
1920 fn a_body_whose_last_instruction_falls_through_is_a_fault() {
1921 let f = function(vec![Repr::Int], INT, vec![Inst::Int { dst: 0, value: 1 }]);
1922 assert_eq!(
1923 faults(&program(vec![f])),
1924 vec!["the last instruction can fall through, and there is nothing after it"]
1925 );
1926 }
1927
1928 /// An argument used to be checked only for existing, because it was a
1929 /// slot and a slot cannot run off the end of anything. It carries the
1930 /// layout of the location it names now, so a two-word argument at the
1931 /// last slot of a frame is a fault here rather than a read of the frame
1932 /// above at run time.
1933 #[test]
1934 fn an_argument_that_runs_off_the_end_of_the_frame_is_a_fault() {
1935 let f = function(
1936 vec![Repr::Int, Repr::Int, Repr::Bool],
1937 INT,
1938 vec![
1939 Inst::CallBuiltin {
1940 dst: 0,
1941 builtin: crate::BuiltinId(0),
1942 args: crate::ArgsId(0),
1943 },
1944 Inst::Return { src: 0 },
1945 ],
1946 );
1947 let mut held = program(vec![f]);
1948 held.builtins = vec![crate::Builtin {
1949 receiver: Arc::from("Any"),
1950 operation: Arc::from("equals"),
1951 result: INT,
1952 }];
1953 held.args = vec![vec![Arg {
1954 slot: 2,
1955 layout: POINT,
1956 }]];
1957 assert_eq!(
1958 faults(&held),
1959 vec!["argument 0 is `Point`, 2 words at slot 2, and the frame has 3"]
1960 );
1961 }
1962
1963 /// A closure call's destination is checked like every other call's.
1964 ///
1965 /// Which body the call enters is a run-time fact and how wide its answer
1966 /// is, is not: the checker settled the call against the callee's function
1967 /// type, so `Inst::CallClosure` carries the layout and this asks the same
1968 /// `fits` question of it. Before it did, a two-word answer written into
1969 /// the last slot of a frame was checked by nothing here, and the machine
1970 /// wrote the frame above it.
1971 #[test]
1972 fn a_closure_calls_answer_that_runs_off_the_end_of_the_frame_is_a_fault() {
1973 let mut held = program(vec![function(
1974 vec![Repr::Int, Repr::Ref, Repr::Int],
1975 INT,
1976 vec![
1977 Inst::CallClosure {
1978 dst: 2,
1979 closure: 1,
1980 args: ArgsId(0),
1981 result: POINT,
1982 },
1983 Inst::Return { src: 0 },
1984 ],
1985 )]);
1986 held.args.push(Vec::new());
1987 assert_eq!(
1988 faults(&held),
1989 vec![
1990 "the answer of a closure call is `Point`, 2 words at slot 2, and the frame has 3"
1991 .to_string()
1992 ]
1993 );
1994 }
1995
1996 /// And the same call whose destination is a location of that layout has
1997 /// nothing said about it. `Point` is two `Int` words and slots 0 and 1
1998 /// are two.
1999 #[test]
2000 fn a_closure_call_whose_answer_fits_its_destination_is_well_formed() {
2001 let mut held = program(vec![function(
2002 vec![Repr::Int, Repr::Int, Repr::Ref],
2003 INT,
2004 vec![
2005 Inst::CallClosure {
2006 dst: 0,
2007 closure: 2,
2008 args: ArgsId(0),
2009 result: POINT,
2010 },
2011 Inst::Return { src: 0 },
2012 ],
2013 )]);
2014 held.args.push(Vec::new());
2015 assert_eq!(faults(&held), Vec::<String>::new());
2016 }
2017
2018 /// Two layouts can have the same words and not be the same family, and it
2019 /// is the argument's layout the machine hands a builtin and a host — so
2020 /// the layouts are compared and not only the locations' reprs.
2021 #[test]
2022 fn an_argument_passed_as_another_family_than_the_parameter_is_a_fault() {
2023 let mut callee = function(
2024 vec![Repr::Int, Repr::Int],
2025 INT,
2026 vec![Inst::Return { src: 0 }],
2027 );
2028 callee.params = vec![POINT];
2029 callee.name = Arc::from("g");
2030 let caller = function(
2031 vec![Repr::Int, Repr::Int],
2032 INT,
2033 vec![
2034 Inst::Call {
2035 dst: 0,
2036 callee: FunctionId(0),
2037 args: crate::ArgsId(0),
2038 },
2039 Inst::Return { src: 0 },
2040 ],
2041 );
2042 let mut held = program(vec![callee, caller]);
2043 held.args = vec![vec![Arg {
2044 slot: 0,
2045 layout: PAIR,
2046 }]];
2047 assert_eq!(
2048 faults(&held),
2049 vec!["argument 0 of `m.g` is passed as a `Pair`, and the parameter is a `Point`"]
2050 );
2051 }
2052
2053 /// A box's width is in the header its allocator writes and not in its
2054 /// shape, so `Alloc` would size one by the wrong thing: a box of a
2055 /// two-word value would be a word short and the copy into it would run
2056 /// off the end of the object. `Inst::Box` is the only correct allocator
2057 /// for one, because it is the only one that is told what is going in.
2058 #[test]
2059 fn allocating_a_box_by_its_shape_is_a_fault() {
2060 let f = function(
2061 vec![Repr::Ref],
2062 STR,
2063 vec![
2064 Inst::Alloc {
2065 dst: 0,
2066 layout: BOXED,
2067 len: Len::Fixed,
2068 },
2069 Inst::Return { src: 0 },
2070 ],
2071 );
2072 assert_eq!(
2073 faults(&program(vec![f])),
2074 vec![
2075 "allocates a `Any`, whose width the header carries and the shape does not; a box \
2076 is allocated by `box`, which knows what is going into it"
2077 ]
2078 );
2079 }
2080
2081 /// A field access is bounded against the object wherever the slot holding
2082 /// it is written by allocations alone, all naming one layout — which is
2083 /// what a lowering that allocates an object and reads its fields does.
2084 /// Without it a `Copy` at the top of a frame reads the frame above and
2085 /// the machine's own header check is the only thing left.
2086 #[test]
2087 fn a_field_past_an_object_of_a_known_layout_is_a_fault() {
2088 let f = function(
2089 vec![Repr::Ref, Repr::Int, Repr::Int],
2090 INT,
2091 vec![
2092 Inst::Alloc {
2093 dst: 0,
2094 layout: POINT,
2095 len: Len::Fixed,
2096 },
2097 Inst::LoadField {
2098 dst: 1,
2099 obj: 0,
2100 at: 1,
2101 layout: PAIR,
2102 },
2103 Inst::Return { src: 1 },
2104 ],
2105 );
2106 assert_eq!(
2107 faults(&program(vec![f])),
2108 vec!["read 2 word(s) at word 1 of a `Point`, which has 2"]
2109 );
2110 }
2111
2112 /// And it says nothing where it cannot: a slot a copy wrote holds
2113 /// whatever the source held, and a `Shape::Str` object is as long as it
2114 /// was allocated. Both are the machine's to answer, from the header.
2115 #[test]
2116 fn a_field_of_an_object_whose_layout_is_not_static_is_left_to_the_machine() {
2117 let f = function(
2118 vec![Repr::Ref, Repr::Ref, Repr::Int],
2119 INT,
2120 vec![
2121 Inst::Alloc {
2122 dst: 0,
2123 layout: POINT,
2124 len: Len::Fixed,
2125 },
2126 Inst::Copy {
2127 dst: 1,
2128 src: 0,
2129 layout: STR,
2130 },
2131 Inst::LoadField {
2132 dst: 2,
2133 obj: 1,
2134 at: 9,
2135 layout: INT,
2136 },
2137 Inst::Str {
2138 dst: 0,
2139 text: crate::StrId(0),
2140 },
2141 Inst::LoadField {
2142 dst: 2,
2143 obj: 0,
2144 at: 9,
2145 layout: INT,
2146 },
2147 Inst::Return { src: 2 },
2148 ],
2149 );
2150 let mut held = program(vec![f]);
2151 held.strings = vec![Arc::from("x")];
2152 // The first `LoadField` names a slot two allocations disagree about
2153 // and the second an object whose payload the header decides.
2154 assert_eq!(faults(&held), Vec::<String>::new());
2155 }
2156
2157 /// A closure's callee is carried twice — once in
2158 /// [`Shape::Closure::function`], the typed fact, and once in the word
2159 /// [`Inst::FuncRef`] writes into its environment's callee field — and
2160 /// until [`Check::check_closure_callee`] nothing compared them. `m.g` is
2161 /// what [`CLOSURE`]'s layout says the environment holds; the body writes
2162 /// `m.f` into it instead.
2163 ///
2164 /// [Issue #275](https://github.com/myuon/cove/issues/275) is why the
2165 /// message names them `@m.f` and `@m.g` rather than `fn0` and `fn1`: a
2166 /// program's second function is given a name of its own, `g`, distinct
2167 /// from [`function`]'s hard-coded `f`, purely so this message has two
2168 /// different symbols to tell apart rather than `m.f` disagreeing with
2169 /// itself.
2170 #[test]
2171 fn a_closures_environment_naming_a_different_callee_than_its_layout_is_a_fault() {
2172 let f = function(
2173 vec![Repr::Ref, Repr::Int],
2174 INT,
2175 vec![
2176 Inst::Alloc {
2177 dst: 0,
2178 layout: CLOSURE,
2179 len: Len::Fixed,
2180 },
2181 Inst::FuncRef {
2182 dst: 1,
2183 callee: FunctionId(0),
2184 },
2185 Inst::StoreField {
2186 obj: 0,
2187 at: 0,
2188 src: 1,
2189 layout: INT,
2190 },
2191 Inst::Return { src: 1 },
2192 ],
2193 );
2194 let mut other = function(vec![Repr::Int], INT, vec![Inst::Return { src: 0 }]);
2195 other.name = Arc::from("g");
2196 assert_eq!(
2197 faults(&program(vec![f, other])),
2198 vec![
2199 "stores @m.f into the callee field of a `closure g` closure, whose layout names @m.g"
2200 ]
2201 );
2202 }
2203
2204 /// The same shape, agreeing: `f#0`'s environment says `f#0`.
2205 #[test]
2206 fn a_closures_environment_naming_its_own_layouts_callee_is_well_formed() {
2207 let f = function(
2208 vec![Repr::Ref, Repr::Int],
2209 INT,
2210 vec![
2211 Inst::Alloc {
2212 dst: 0,
2213 layout: CLOSURE,
2214 len: Len::Fixed,
2215 },
2216 Inst::FuncRef {
2217 dst: 1,
2218 callee: FunctionId(1),
2219 },
2220 Inst::StoreField {
2221 obj: 0,
2222 at: 0,
2223 src: 1,
2224 layout: INT,
2225 },
2226 Inst::Return { src: 1 },
2227 ],
2228 );
2229 let other = function(vec![Repr::Int], INT, vec![Inst::Return { src: 0 }]);
2230 assert_eq!(faults(&program(vec![f, other])), Vec::<String>::new());
2231 }
2232
2233 #[test]
2234 fn a_clear_agrees_with_the_layout_it_zeroes() {
2235 let f = function(
2236 vec![Repr::Int, Repr::Ref, Repr::Unit],
2237 INT,
2238 vec![
2239 Inst::Clear {
2240 slot: 0,
2241 layout: ANSWER,
2242 },
2243 Inst::Clear {
2244 slot: 1,
2245 layout: ANSWER,
2246 },
2247 Inst::Return { src: 0 },
2248 ],
2249 );
2250 // The first is right — `[Int, Ref]` is what an `Option` is — and the
2251 // second names the same layout one word along, where it is not.
2252 assert_eq!(
2253 faults(&program(vec![f])),
2254 vec!["what a clear zeroes is `Option`, whose word 0 is int, but slot 1 holds ref"]
2255 );
2256 }
2257}