Skip to main content

qcode/
arena_integrity.rs

1//! Structural validation for function-owned arenas.
2//!
3//! This scanner lives in core so it can inspect malformed private arena state
4//! without exposing storage internals as public API. It never indexes an ID
5//! before proving that the corresponding payload is live.
6
7use 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
27/// Validate the ownership and cross-reference invariants of every function
28/// body's block, instruction, parameter, CFG-edge, and temporary arenas.
29///
30/// Diagnostics from this function describe potentially unsafe structural
31/// corruption. Callers should not run higher-level traversals until it returns
32/// an empty vector.
33pub fn verify_body_arena_integrity(ctx: &Context<'_>) -> Vec<String> {
34    verify_body_arena_integrity_scoped(ctx, None)
35}
36
37/// [`verify_body_arena_integrity`], restricted to the functions in `scope`
38/// (`None` means every function). Arena invariants are strictly per-body, so a
39/// caller that knows which functions changed can skip the rest.
40pub 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            // Block ownership is derived from the storing arena (`fid`); there is
117            // no per-block parent field left to disagree with it.
118
119            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, &param_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(&param_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                // A pure instruction that names its own result as an operand is a
227                // self-referential (unsatisfiable) value: an SSA-invariant
228                // violation that makes recursive value-walkers loop. Catch the
229                // direct case here — it is the concrete shape a bad forward mints
230                // (`%x = %y + %x`).
231                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            // Edge endpoints are bare `LocalBlockId`s stored in this body's arena
310            // (stage 1); there is no cross-arena state left to probe.
311            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        // `%y = @x + 1` — rewrite its `@x` operand to `%y` itself.
439        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    // A cross-function edge endpoint is unrepresentable since stage 1: edge
586    // endpoints are bare `LocalBlockId`s carrying no function qualifier, so the
587    // old `reports_cross_function_edge_endpoint` probe and fixture are gone.
588
589    #[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}