1use rustc_hash::{FxHashMap, FxHashSet};
8
9use crate::{
10 context::Context,
11 space::{LocalMemorySpaceId, MemorySpaceId},
12 value::{BlockId, BlockParamId, InstructionId, LocalValueId, insn::Mnemonic},
13};
14
15fn missing_type_temp_space(
16 ctx: &Context<'_>,
17 function: crate::value::FunctionId,
18 type_id: crate::types::TypeId,
19) -> Option<crate::value::TempSpaceId> {
20 let MemorySpaceId::Temp(space) = ctx.shared.types.space_of(type_id)? else {
21 return None;
22 };
23 (space.func != function || usize::from(space.local) >= ctx.bodies[function].temp_spaces.len())
24 .then_some(space)
25}
26
27pub fn verify_body_arena_integrity(ctx: &Context<'_>) -> Vec<String> {
34 verify_body_arena_integrity_scoped(ctx, None)
35}
36
37pub fn verify_body_arena_integrity_scoped(
41 ctx: &Context<'_>,
42 scope: Option<&FxHashSet<crate::value::FunctionId>>,
43) -> Vec<String> {
44 let mut out = Vec::new();
45
46 for body in ctx.bodies.iter() {
47 if scope.is_some_and(|set| !set.contains(&body.id)) {
48 continue;
49 }
50 let fid = body.id;
51 let live_blocks: FxHashSet<_> = body.blocks.iter().map(|block| block.id).collect();
52 let live_insns: FxHashSet<_> = body.insns.iter().map(|insn| insn.id).collect();
53 let live_params: FxHashSet<_> = body.params.iter().map(|param| param.id).collect();
54 let live_edges: FxHashSet<_> = body.edges.iter().map(|edge| edge.id).collect();
55
56 for temp in body.temps.iter() {
57 if usize::from(temp.space) >= body.temp_spaces.len() {
58 out.push(format!(
59 "function {fid:?}: temporary {:?} references missing temporary space {:?}",
60 crate::value::TempId::new(fid, temp.id),
61 crate::value::TempSpaceId::new(fid, temp.space)
62 ));
63 }
64 }
65
66 let mut roster_count = FxHashMap::default();
67 for &local in &body.roster {
68 *roster_count.entry(local).or_insert(0usize) += 1;
69 if !live_blocks.contains(&local) {
70 out.push(format!(
71 "function {fid:?}: roster references removed block {:?}",
72 BlockId::new(fid, local)
73 ));
74 }
75 }
76 for (&local, &count) in &roster_count {
77 if count != 1 {
78 out.push(format!(
79 "function {fid:?}: block {:?} appears {count} times in the roster",
80 BlockId::new(fid, local)
81 ));
82 }
83 }
84 for &local in &live_blocks {
85 let count = roster_count.get(&local).copied().unwrap_or(0);
86 if count != 1 {
87 out.push(format!(
88 "function {fid:?}: live block {:?} has roster count {count}",
89 BlockId::new(fid, local)
90 ));
91 }
92 }
93
94 if let Some(root) = body.root_id() {
95 let root_id = BlockId::new(fid, root);
96 if !live_blocks.contains(&root) {
97 out.push(format!(
98 "function {fid:?}: root {root_id:?} has no live block payload"
99 ));
100 } else if roster_count.get(&root).copied().unwrap_or(0) != 1 {
101 out.push(format!(
102 "function {fid:?}: root {root_id:?} is not rostered exactly once"
103 ));
104 }
105 }
106
107 let mut insn_membership: FxHashMap<_, Vec<_>> = FxHashMap::default();
108 let mut param_membership: FxHashMap<_, Vec<(crate::value::LocalBlockId, usize)>> =
109 FxHashMap::default();
110
111 for block_entry in body.blocks.iter() {
112 let local = block_entry.id;
113 let block_id = BlockId::new(fid, local);
114 let block = &*block_entry;
115
116 for &insn_local in &block.instructions {
120 insn_membership.entry(insn_local).or_default().push(local);
121 let insn_id = InstructionId::new(fid, insn_local);
122 if !live_insns.contains(&insn_local) {
123 out.push(format!(
124 "block {block_id:?} references removed instruction {insn_id:?}"
125 ));
126 continue;
127 }
128 if body.insns[insn_local].parent != Some(local) {
129 out.push(format!(
130 "block {block_id:?} contains {insn_id:?}, whose parent is {:?}",
131 body.insns[insn_local].parent
132 ));
133 }
134 }
135
136 for (index, ¶m_local) in block.params.iter().enumerate() {
137 param_membership
138 .entry(param_local)
139 .or_default()
140 .push((local, index));
141 let param_id = BlockParamId::new(fid, param_local);
142 if !live_params.contains(¶m_local) {
143 out.push(format!(
144 "block {block_id:?} references removed parameter {param_id:?}"
145 ));
146 continue;
147 }
148 let param = &body.params[param_local];
149 if param.parent != Some(local) {
150 out.push(format!(
151 "block {block_id:?} contains {param_id:?}, whose parent is {:?}",
152 param.parent
153 ));
154 }
155 if param.index != index {
156 out.push(format!(
157 "block {block_id:?} contains {param_id:?} at index {index}, but payload index is {}",
158 param.index
159 ));
160 }
161 }
162
163 for &edge_id in &block.edges {
164 if !live_edges.contains(&edge_id) {
165 out.push(format!(
166 "block {block_id:?} references removed CFG edge {edge_id:?}"
167 ));
168 continue;
169 }
170 let edge = &body.edges[edge_id];
171 if edge.from != block_id.local && edge.to != block_id.local {
172 out.push(format!(
173 "block {block_id:?} lists non-incident CFG edge {edge_id:?} ({:?} -> {:?})",
174 edge.from, edge.to
175 ));
176 }
177 }
178 }
179
180 for insn_entry in body.insns.iter() {
181 let local = insn_entry.id;
182 let insn_id = InstructionId::new(fid, local);
183 let memberships = insn_membership
184 .get(&local)
185 .map(Vec::as_slice)
186 .unwrap_or(&[]);
187 if let Some(parent) = insn_entry.parent {
188 if !live_blocks.contains(&parent) {
189 out.push(format!(
190 "instruction {insn_id:?} has removed parent {:?}",
191 BlockId::new(fid, parent)
192 ));
193 }
194 if memberships != [parent] {
195 out.push(format!(
196 "instruction {insn_id:?} has parent {:?} but block memberships {memberships:?}",
197 BlockId::new(fid, parent)
198 ));
199 }
200 } else if !memberships.is_empty() {
201 out.push(format!(
202 "detached instruction {insn_id:?} still appears in blocks {memberships:?}"
203 ));
204 }
205
206 if let Some(space) = missing_type_temp_space(ctx, fid, insn_entry.type_id) {
207 out.push(format!(
208 "instruction {insn_id:?} result type references missing temporary space {space:?}"
209 ));
210 }
211
212 for arg in insn_entry.mnemonic().args() {
213 let missing = match arg {
214 LocalValueId::Instruction(id) => !live_insns.contains(&id),
215 LocalValueId::BlockParam(id) => !live_params.contains(&id),
216 LocalValueId::BasicBlock(id) => !live_blocks.contains(&id),
217 LocalValueId::Temp(id) => usize::from(id) >= body.temps.len(),
218 _ => false,
219 };
220 if missing {
221 out.push(format!(
222 "instruction {insn_id:?} references removed local value {:?}",
223 arg.qualify(fid)
224 ));
225 }
226 if arg == LocalValueId::Instruction(local) {
232 out.push(format!(
233 "instruction {insn_id:?} references itself as an operand"
234 ));
235 }
236 }
237
238 let mnemonic_space = match insn_entry.mnemonic() {
239 Mnemonic::Load(load) => Some(load.space),
240 Mnemonic::Store(store) => Some(store.space),
241 _ => None,
242 };
243 if let Some(LocalMemorySpaceId::Temp(space)) = mnemonic_space
244 && usize::from(space) >= body.temp_spaces.len()
245 {
246 out.push(format!(
247 "instruction {insn_id:?} references missing temporary space {:?}",
248 crate::value::TempSpaceId::new(fid, space)
249 ));
250 }
251
252 let mut check_target = |target| {
253 if !live_blocks.contains(&target) {
254 out.push(format!(
255 "instruction {insn_id:?} targets removed block {:?}",
256 BlockId::new(fid, target)
257 ));
258 }
259 };
260 match insn_entry.mnemonic() {
261 Mnemonic::Branch(branch) => check_target(branch.target),
262 Mnemonic::CBranch(branch) => {
263 check_target(branch.success_block);
264 check_target(branch.failure_block);
265 }
266 _ => {}
267 }
268 }
269
270 for param_entry in body.params.iter() {
271 let local = param_entry.id;
272 let param_id = BlockParamId::new(fid, local);
273 let memberships = param_membership
274 .get(&local)
275 .map(Vec::as_slice)
276 .unwrap_or(&[]);
277 match param_entry.parent {
278 Some(parent) if !live_blocks.contains(&parent) => out.push(format!(
279 "parameter {param_id:?} has removed parent {:?}",
280 BlockId::new(fid, parent)
281 )),
282 None => out.push(format!("live parameter {param_id:?} is detached")),
283 Some(_) => {}
284 }
285 if memberships.len() != 1 {
286 out.push(format!(
287 "live parameter {param_id:?} has {} block memberships",
288 memberships.len()
289 ));
290 }
291 if let Some(space) = missing_type_temp_space(ctx, fid, param_entry.type_id) {
292 out.push(format!(
293 "parameter {param_id:?} type references missing temporary space {space:?}"
294 ));
295 }
296 if let Some(LocalValueId::Temp(temp)) = param_entry.origin
297 && usize::from(temp) >= body.temps.len()
298 {
299 out.push(format!(
300 "parameter {param_id:?} origin references missing temporary {:?}",
301 crate::value::TempId::new(fid, temp)
302 ));
303 }
304 }
305
306 for edge_entry in body.edges.iter() {
307 let edge_id = edge_entry.id;
308 let edge = &*edge_entry;
309 let from_live = live_blocks.contains(&edge.from);
312 let to_live = live_blocks.contains(&edge.to);
313 if !from_live || !to_live {
314 out.push(format!(
315 "function {fid:?}: edge {edge_id:?} has removed endpoint ({:?} -> {:?})",
316 edge.from, edge.to
317 ));
318 continue;
319 }
320 if !body.blocks[edge.from].edges.contains(&edge_id) {
321 out.push(format!(
322 "edge {edge_id:?} is missing from source block {:?}",
323 edge.from
324 ));
325 }
326 if !body.blocks[edge.to].edges.contains(&edge_id) {
327 out.push(format!(
328 "edge {edge_id:?} is missing from target block {:?}",
329 edge.to
330 ));
331 }
332 }
333
334 for (name, value) in body.names.entries() {
335 let live = match value {
336 LocalValueId::BasicBlock(id) => live_blocks.contains(&id),
337 LocalValueId::Instruction(id) => live_insns.contains(&id),
338 LocalValueId::BlockParam(id) => live_params.contains(&id),
339 LocalValueId::Temp(id) => usize::from(id) < body.temps.len(),
340 _ => false,
341 };
342 if !live {
343 out.push(format!(
344 "function {fid:?}: local name {name:?} references absent or non-local value {value:?}"
345 ));
346 }
347 }
348
349 for (&value, users) in &body.users {
350 let key_live = match value {
351 LocalValueId::Instruction(id) => live_insns.contains(&id),
352 LocalValueId::BlockParam(id) => live_params.contains(&id),
353 LocalValueId::BasicBlock(id) => live_blocks.contains(&id),
354 LocalValueId::Temp(id) => usize::from(id) < body.temps.len(),
355 _ => true,
356 };
357 if !key_live {
358 out.push(format!(
359 "function {fid:?}: users map contains removed key {:?}",
360 value.qualify(fid)
361 ));
362 }
363 for &user in users {
364 if !live_insns.contains(&user) {
365 out.push(format!(
366 "function {fid:?}: users map for {:?} references removed instruction {:?}",
367 value.qualify(fid),
368 InstructionId::new(fid, user)
369 ));
370 }
371 }
372 }
373
374 let stats = body.arena_stats();
375 let max_issued = u32::MAX as usize + 1;
376 for (kind, issued) in [
377 ("instruction", stats.instructions.issued),
378 ("block", stats.blocks.issued),
379 ("parameter", stats.params.issued),
380 ("edge", stats.edges.issued),
381 ] {
382 if issued > max_issued {
383 out.push(format!(
384 "function {fid:?}: {kind} arena issued cursor {issued} exceeds u32 ID space"
385 ));
386 }
387 }
388 }
389
390 out
391}
392
393#[cfg(test)]
394mod tests {
395 use crate::value::QCodeMut;
396 use std::borrow::Cow;
397
398 use wazabin_qcode_macro::qcode;
399
400 use super::*;
401 use crate::value::{BasicBlock, FunctionBody, LocalTempSpaceId, Temp, ValueId};
402
403 fn fixture() -> Context<'static> {
404 let mut ctx = Context::new();
405 qcode!(
406 ctx,
407 "
408 fn f:
409 <entry @x:i64>
410 %y = i64 @x + i64 1;
411 goto <exit>;
412 <exit>
413 return at i64 %y;
414 "
415 );
416 ctx
417 }
418
419 fn assert_has(ctx: &Context<'_>, needle: &str) {
420 let diagnostics = verify_body_arena_integrity(ctx);
421 assert!(
422 diagnostics.iter().any(|d| d.contains(needle)),
423 "expected diagnostic containing {needle:?}, got {diagnostics:#?}"
424 );
425 }
426
427 #[test]
428 fn valid_body_is_clean() {
429 let ctx = fixture();
430 assert_eq!(verify_body_arena_integrity(&ctx), Vec::<String>::new());
431 }
432
433 #[test]
434 fn reports_self_referential_instruction() {
435 let mut ctx = fixture();
436 let f = ctx.function_ids()[0];
437 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
438 let y = BasicBlock::from_id(&ctx, entry).instruction_ids()[0];
440 let x = ctx
441 .instruction(y)
442 .mnemonic()
443 .args()
444 .into_iter()
445 .next()
446 .unwrap();
447 ctx.bodies[f]
448 .insn_mut(y)
449 .mnemonic_mut()
450 .replace_value(x, LocalValueId::Instruction(y.local));
451
452 assert_has(&ctx, "references itself as an operand");
453 }
454
455 #[test]
456 fn reports_removed_root_without_indexing_it() {
457 let mut ctx = fixture();
458 let f = ctx.function_ids()[0];
459 let dead = BasicBlock::make(&mut ctx, f).id;
460 ctx.delete_block(dead);
461 ctx.function_mut(f).set_root_id(Some(dead.local));
462
463 assert_has(&ctx, "root");
464 }
465
466 #[test]
467 fn reports_duplicate_roster_membership() {
468 let mut ctx = fixture();
469 let f = ctx.function_ids()[0];
470 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
471 ctx.bodies[f].roster.push(entry.local);
472
473 assert_has(&ctx, "appears 2 times in the roster");
474 }
475
476 #[test]
477 fn reports_unrostered_block() {
478 let mut ctx = fixture();
479 let f = ctx.function_ids()[0];
480 let block = BasicBlock::make(&mut ctx, f).id;
481 ctx.bodies[f].roster.retain(|&local| local != block.local);
482
483 assert_has(&ctx, "roster count 0");
484 }
485
486 #[test]
487 fn reports_instruction_membership_and_parent_disagreement() {
488 let mut ctx = fixture();
489 let f = ctx.function_ids()[0];
490 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
491 let y = ctx
492 .block(entry)
493 .instructions
494 .iter()
495 .copied()
496 .find(|&id| !ctx.bodies[f].insns[id].mnemonic().is_terminator())
497 .expect("value instruction");
498 ctx.block_mut(entry).instructions.push(y);
499
500 assert_has(&ctx, "block memberships");
501 }
502
503 #[test]
504 fn permits_live_detached_instruction() {
505 let mut ctx = fixture();
506 let f = ctx.function_ids()[0];
507 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
508 let detached = ctx
509 .block(entry)
510 .instructions
511 .iter()
512 .copied()
513 .find(|&id| !ctx.bodies[f].insns[id].mnemonic().is_terminator())
514 .expect("value instruction");
515 ctx.block_mut(entry)
516 .instructions
517 .retain(|&id| id != detached);
518 ctx.bodies[f].insns[detached].parent = None;
519
520 assert_eq!(verify_body_arena_integrity(&ctx), Vec::<String>::new());
521 }
522
523 #[test]
524 fn reports_stale_instruction_and_parameter_membership() {
525 let mut ctx = fixture();
526 let f = ctx.function_ids()[0];
527 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
528 let insn = ctx
529 .block(entry)
530 .instructions
531 .iter()
532 .copied()
533 .find(|&id| !ctx.bodies[f].insns[id].mnemonic().is_terminator())
534 .expect("value instruction");
535 let param = ctx.block(entry).params[0];
536 ctx.bodies[f].insns.remove(insn);
537 ctx.bodies[f].params.remove(param);
538
539 assert_has(&ctx, "references removed instruction");
540 assert_has(&ctx, "references removed parameter");
541 assert_has(&ctx, "references removed local value");
542 assert_has(&ctx, "users map for");
543 }
544
545 #[test]
546 fn reports_parameter_index_and_parent_disagreement() {
547 let mut ctx = fixture();
548 let f = ctx.function_ids()[0];
549 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
550 let param = BlockParamId::new(f, ctx.block(entry).params[0]);
551 ctx.block_param_mut(param).index = 7;
552 ctx.block_param_mut(param).parent = None;
553
554 assert_has(&ctx, "payload index is 7");
555 assert_has(&ctx, "live parameter");
556 }
557
558 #[test]
559 fn reports_missing_edge_adjacency() {
560 let mut ctx = fixture();
561 let f = ctx.function_ids()[0];
562 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
563 let edge = *ctx.block(entry).edges.iter().next().expect("edge");
564 let target = BlockId::new(f, ctx.edge(f, edge).to);
565 ctx.block_mut(target).edges.remove(&edge);
566
567 assert_has(&ctx, "missing from target block");
568 }
569
570 #[test]
571 fn reports_stale_and_non_incident_adjacency_without_indexing() {
572 let mut ctx = fixture();
573 let f = ctx.function_ids()[0];
574 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
575 let edge = *ctx.block(entry).edges.iter().next().expect("edge");
576 let unrelated = BasicBlock::make(&mut ctx, f).id;
577 ctx.block_mut(unrelated).edges.insert(edge);
578
579 assert_has(&ctx, "lists non-incident CFG edge");
580
581 ctx.bodies[f].edges.remove(edge);
582 assert_has(&ctx, "references removed CFG edge");
583 }
584
585 #[test]
590 fn reports_branch_targeting_removed_block() {
591 let mut ctx = fixture();
592 let f = ctx.function_ids()[0];
593 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
594 let edge = *ctx.block(entry).edges.iter().next().expect("edge");
595 let target = BlockId::new(f, ctx.edge(f, edge).to);
596 ctx.delete_block(target);
597
598 assert_has(&ctx, "targets removed block");
599 }
600
601 #[test]
602 fn reports_stale_local_name_and_users_entries() {
603 let mut ctx = fixture();
604 let f = ctx.function_ids()[0];
605 let dead_block = BasicBlock::make(&mut ctx, f).id;
606 ctx.delete_block(dead_block);
607 ctx.bodies[f]
608 .names
609 .register(
610 Cow::Borrowed("stale"),
611 ValueId::BasicBlock(dead_block).localize(f),
612 None,
613 )
614 .expect("register corruption fixture");
615
616 let live_block = FunctionBody::from_id(&ctx, f).root().expect("root").id;
617 let dead_param = BasicBlock::from_id_mut(&mut ctx, live_block)
618 .push_param(8)
619 .id;
620 ctx.block_mut(live_block).params.pop();
621 ctx.remove_block_param(dead_param);
622 ctx.bodies[f]
623 .users
624 .insert(ValueId::BlockParam(dead_param).strip_func(), Vec::new());
625
626 assert_has(&ctx, "local name \"stale\"");
627 assert_has(&ctx, "users map contains removed key");
628 }
629
630 #[test]
631 fn reports_temporary_with_missing_local_space() {
632 let mut ctx = fixture();
633 let f = ctx.function_ids()[0];
634 ctx.bodies[f]
635 .temps
636 .push(Temp::new(0, 8, LocalTempSpaceId::from(7)));
637
638 assert_has(&ctx, "references missing temporary space");
639 }
640
641 #[test]
642 fn reports_dangling_temporary_operands_spaces_origins_and_types() {
643 use crate::value::insn::Load;
644
645 let mut ctx = fixture();
646 let f = ctx.function_ids()[0];
647 let entry = FunctionBody::from_id(&ctx, f).root().expect("root").id;
648 let insn = ctx
649 .block(entry)
650 .instructions
651 .iter()
652 .copied()
653 .find(|&id| !ctx.bodies[f].insns[id].mnemonic().is_terminator())
654 .expect("value instruction");
655 let missing_temp = crate::value::LocalTempId::from(0);
656 let missing_space = LocalTempSpaceId::from(0);
657 *ctx.bodies[f].insns[insn].mnemonic_mut() = Mnemonic::Load(Load {
658 space: LocalMemorySpaceId::Temp(missing_space),
659 ptr: LocalValueId::Temp(missing_temp),
660 size: 8,
661 });
662 ctx.bodies[f].insns[insn].type_id = ctx.shared.types.get_or_make_space_address(
663 8,
664 MemorySpaceId::Temp(crate::value::TempSpaceId::new(f, missing_space)),
665 );
666 let param = ctx.block(entry).params[0];
667 ctx.bodies[f].params[param].origin = Some(LocalValueId::Temp(missing_temp));
668
669 assert_has(&ctx, "references removed local value");
670 assert_has(&ctx, "references missing temporary space");
671 assert_has(&ctx, "result type references missing temporary space");
672 assert_has(&ctx, "origin references missing temporary");
673 }
674}