Skip to main content

shape_vm/mir/
field_analysis.rs

1//! Field-level definite assignment and liveness analysis on MIR.
2//!
3//! Supplements the AST-level "optimistic hoisting" pre-pass (Phase 1) with a
4//! flow-sensitive MIR analysis (Phase 2) that uses the CFG with dominators.
5//! Phase 1 runs before compilation to collect fields; Phase 2 validates them
6//! per-function after MIR lowering, detecting conditionally-initialized and
7//! dead fields. Tracks which TypedObject
8//! fields are definitely initialized, conditionally initialized, live (have
9//! future reads), or dead (written but never read) at each program point.
10//!
11//! The analysis has two phases:
12//! 1. **Forward**: Definite initialization — which fields are guaranteed to be
13//!    assigned on every path from the entry block to a given point.
14//! 2. **Backward**: Field liveness — which (slot, field) pairs will be read on
15//!    some path from the current point to function exit.
16
17use super::cfg::ControlFlowGraph;
18use super::types::*;
19use std::collections::{HashMap, HashSet};
20
21/// A (slot, field) pair identifying a specific field on a specific local.
22pub type FieldKey = (SlotId, FieldIdx);
23
24/// Results of field-level analysis for a single MIR function.
25#[derive(Debug)]
26pub struct FieldAnalysis {
27    /// Fields that are definitely initialized at the *entry* of each block.
28    pub definitely_initialized: HashMap<BasicBlockId, HashSet<FieldKey>>,
29    /// Fields that are live (have future reads) at the *entry* of each block.
30    pub field_liveness: HashMap<BasicBlockId, HashSet<FieldKey>>,
31    /// Fields that are assigned but never read anywhere in the function.
32    pub dead_fields: HashSet<FieldKey>,
33    /// Fields that are initialized on some but not all paths to a use point.
34    pub conditionally_initialized: HashSet<FieldKey>,
35    /// Fields eligible for TypedObject schema hoisting: written on any path
36    /// and not dead.  Keyed by slot, with the list of field indices to hoist.
37    pub hoisted_fields: HashMap<SlotId, Vec<FieldIdx>>,
38    /// MIR-authoritative hoisting recommendations: maps each slot to pairs of
39    /// (field_index, field_name) for schema construction. Populated when
40    /// field names are available from the lowering result.
41    pub hoisting_recommendations: HashMap<SlotId, Vec<(FieldIdx, String)>>,
42}
43
44/// Input bundle for field analysis.
45pub struct FieldAnalysisInput<'a> {
46    pub mir: &'a MirFunction,
47    pub cfg: &'a ControlFlowGraph,
48}
49
50/// Run field-level definite-assignment and liveness analysis.
51pub fn analyze_fields(input: &FieldAnalysisInput) -> FieldAnalysis {
52    let mir = input.mir;
53    let cfg = input.cfg;
54
55    // Step 1: Collect all field writes and reads across the whole function.
56    let (block_writes, block_reads, all_writes, all_reads) = collect_field_accesses(mir);
57
58    // Step 2: Forward dataflow — definite initialization.
59    let definitely_initialized = compute_definite_initialization(mir, cfg, &block_writes);
60
61    // Step 3: Backward dataflow — field liveness.
62    let field_liveness = compute_field_liveness(mir, cfg, &block_writes, &block_reads);
63
64    // Step 4: Dead fields = written but never read anywhere.
65    let dead_fields: HashSet<FieldKey> = all_writes.difference(&all_reads).cloned().collect();
66
67    // Step 5: Conditionally initialized = initialized on some paths but not all
68    // paths to a use point (i.e., the field is read somewhere, and at the entry
69    // of some block that reads it, it is NOT definitely initialized).
70    let conditionally_initialized =
71        compute_conditionally_initialized(mir, &block_reads, &definitely_initialized, &all_writes);
72
73    // Step 6: Hoisted fields = written on any path and not dead.
74    // These are candidates for inclusion in the TypedObject schema at
75    // object-creation time so the schema doesn't need runtime migration.
76    let mut hoisted_fields: HashMap<SlotId, Vec<FieldIdx>> = HashMap::new();
77    for key in &all_writes {
78        if !dead_fields.contains(key) {
79            hoisted_fields.entry(key.0).or_default().push(key.1);
80        }
81    }
82
83    FieldAnalysis {
84        definitely_initialized,
85        field_liveness,
86        dead_fields,
87        conditionally_initialized,
88        hoisted_fields,
89        hoisting_recommendations: HashMap::new(), // populated by caller with field names
90    }
91}
92
93/// Collect per-block field writes and reads, plus global write/read sets.
94///
95/// Returns `(block_writes, block_reads, all_writes, all_reads)`.
96fn collect_field_accesses(
97    mir: &MirFunction,
98) -> (
99    HashMap<BasicBlockId, HashSet<FieldKey>>,
100    HashMap<BasicBlockId, HashSet<FieldKey>>,
101    HashSet<FieldKey>,
102    HashSet<FieldKey>,
103) {
104    let mut block_writes: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
105    let mut block_reads: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
106    let mut all_writes = HashSet::new();
107    let mut all_reads = HashSet::new();
108
109    for block in &mir.blocks {
110        let writes = block_writes.entry(block.id).or_default();
111        let reads = block_reads.entry(block.id).or_default();
112
113        for stmt in &block.statements {
114            collect_statement_field_accesses(&stmt.kind, writes, reads);
115        }
116        // Terminators can also read fields (e.g., a field used as a call arg).
117        collect_terminator_field_reads(&block.terminator.kind, reads);
118
119        all_writes.extend(writes.iter().cloned());
120        all_reads.extend(reads.iter().cloned());
121    }
122
123    (block_writes, block_reads, all_writes, all_reads)
124}
125
126/// Extract field writes and reads from a single statement.
127fn collect_statement_field_accesses(
128    kind: &StatementKind,
129    writes: &mut HashSet<FieldKey>,
130    reads: &mut HashSet<FieldKey>,
131) {
132    match kind {
133        StatementKind::Assign(place, rvalue) => {
134            // Check for field write: `slot.field = ...`
135            if let Some(key) = extract_field_key(place) {
136                writes.insert(key);
137            }
138            // The assignment target might also be a deeper read (e.g., `a.x.y = ...`
139            // reads `a.x`). We only track one level of field for now, but we should
140            // still record reads from the rvalue side.
141            collect_rvalue_field_reads(rvalue, reads);
142        }
143        StatementKind::Drop(place) => {
144            // A drop reads the place.
145            if let Some(key) = extract_field_key(place) {
146                reads.insert(key);
147            }
148        }
149        StatementKind::TaskBoundary(ops, ..)
150        | StatementKind::ClosureCapture { operands: ops, .. }
151        | StatementKind::ArrayStore { operands: ops, .. }
152        | StatementKind::ObjectStore { operands: ops, .. }
153        | StatementKind::EnumStore { operands: ops, .. } => {
154            for op in ops {
155                collect_operand_field_reads(op, reads);
156            }
157        }
158        StatementKind::Nop => {}
159    }
160}
161
162/// Extract field reads from an rvalue.
163fn collect_rvalue_field_reads(rvalue: &Rvalue, reads: &mut HashSet<FieldKey>) {
164    match rvalue {
165        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
166            collect_operand_field_reads(op, reads);
167        }
168        Rvalue::Borrow(_, place) => {
169            if let Some(key) = extract_field_key(place) {
170                reads.insert(key);
171            }
172        }
173        Rvalue::BinaryOp(_, lhs, rhs) => {
174            collect_operand_field_reads(lhs, reads);
175            collect_operand_field_reads(rhs, reads);
176        }
177        Rvalue::Aggregate(ops) => {
178            for op in ops {
179                collect_operand_field_reads(op, reads);
180            }
181        }
182    }
183}
184
185/// Extract field reads from an operand.
186fn collect_operand_field_reads(op: &Operand, reads: &mut HashSet<FieldKey>) {
187    match op {
188        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
189            if let Some(key) = extract_field_key(place) {
190                reads.insert(key);
191            }
192        }
193        Operand::Constant(_) => {}
194    }
195}
196
197/// Extract field reads from a terminator.
198fn collect_terminator_field_reads(kind: &TerminatorKind, reads: &mut HashSet<FieldKey>) {
199    match kind {
200        TerminatorKind::SwitchBool { operand, .. } => {
201            collect_operand_field_reads(operand, reads);
202        }
203        TerminatorKind::Call { func, args, .. } => {
204            collect_operand_field_reads(func, reads);
205            for arg in args {
206                collect_operand_field_reads(arg, reads);
207            }
208        }
209        TerminatorKind::Goto(_) | TerminatorKind::Return | TerminatorKind::Unreachable => {}
210    }
211}
212
213/// Extract the `(SlotId, FieldIdx)` from a `Place::Field(Place::Local(slot), idx)`.
214/// Returns `None` for non-field places or nested field paths (we only track
215/// single-level fields).
216fn extract_field_key(place: &Place) -> Option<FieldKey> {
217    match place {
218        Place::Field(base, idx) => match base.as_ref() {
219            Place::Local(slot) => Some((*slot, *idx)),
220            _ => None,
221        },
222        _ => None,
223    }
224}
225
226// ── Forward dataflow: definite initialization ──────────────────────────
227
228/// Compute which fields are definitely initialized at the entry of each block.
229///
230/// Lattice: `HashSet<FieldKey>` with intersection as meet.
231///   - Entry block: empty set (nothing initialized).
232///   - Transfer: `out[B] = in[B] ∪ writes[B]` (once a field is written, it
233///     stays initialized on that path).
234///   - Merge at join points: `in[B] = ∩ out[P] for all predecessors P`.
235///     A field is definitely initialized only if ALL predecessor paths
236///     initialize it.
237fn compute_definite_initialization(
238    mir: &MirFunction,
239    cfg: &ControlFlowGraph,
240    block_writes: &HashMap<BasicBlockId, HashSet<FieldKey>>,
241) -> HashMap<BasicBlockId, HashSet<FieldKey>> {
242    let rpo = cfg.reverse_postorder();
243    let entry = mir.entry_block();
244
245    // `init_out[B]` = definitely initialized fields at the *exit* of block B.
246    let mut init_in: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
247    let mut init_out: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
248
249    // Collect the universe of all field keys for the "TOP" element.
250    // For definite init, top = all fields (intersection identity), bottom = empty.
251    let universe: HashSet<FieldKey> = block_writes.values().flatten().cloned().collect();
252
253    // Initialize: entry gets empty (nothing initialized); all others get TOP.
254    for block in &mir.blocks {
255        if block.id == entry {
256            init_in.insert(block.id, HashSet::new());
257        } else {
258            init_in.insert(block.id, universe.clone());
259        }
260    }
261
262    // Apply transfer for initial out values.
263    for block in &mir.blocks {
264        let in_set = init_in.get(&block.id).cloned().unwrap_or_default();
265        let writes = block_writes.get(&block.id).cloned().unwrap_or_default();
266        let out_set: HashSet<FieldKey> = in_set.union(&writes).cloned().collect();
267        init_out.insert(block.id, out_set);
268    }
269
270    // Iterate until fixpoint.
271    let mut changed = true;
272    while changed {
273        changed = false;
274
275        for &block_id in &rpo {
276            // Merge: intersect out-sets of all predecessors.
277            let preds = cfg.predecessors(block_id);
278            let new_in = if block_id == entry {
279                HashSet::new()
280            } else if preds.is_empty() {
281                // Unreachable block — leave as universe (won't affect results).
282                universe.clone()
283            } else {
284                let mut merged = init_out
285                    .get(&preds[0])
286                    .cloned()
287                    .unwrap_or_else(|| universe.clone());
288                for &pred in &preds[1..] {
289                    let pred_out = init_out
290                        .get(&pred)
291                        .cloned()
292                        .unwrap_or_else(|| universe.clone());
293                    merged = merged.intersection(&pred_out).cloned().collect();
294                }
295                merged
296            };
297
298            // Transfer: out = in ∪ writes
299            let writes = block_writes.get(&block_id).cloned().unwrap_or_default();
300            let new_out: HashSet<FieldKey> = new_in.union(&writes).cloned().collect();
301
302            if new_in != *init_in.get(&block_id).unwrap_or(&HashSet::new()) {
303                changed = true;
304                init_in.insert(block_id, new_in);
305            }
306            if new_out != *init_out.get(&block_id).unwrap_or(&HashSet::new()) {
307                changed = true;
308                init_out.insert(block_id, new_out);
309            }
310        }
311    }
312
313    init_in
314}
315
316// ── Backward dataflow: field liveness ──────────────────────────────────
317
318/// Compute which fields are live (have future reads) at the entry of each block.
319///
320/// Standard backward liveness:
321///   - `live_out[B] = ∪ live_in[S] for all successors S`.
322///   - `live_in[B] = (live_out[B] − kill[B]) ∪ use[B]`
323///     where `kill[B]` is the set of fields definitely overwritten (we don't
324///     kill in this analysis since a write doesn't prevent an earlier read from
325///     being live — it's the same as standard variable liveness but for fields).
326///
327///   Actually for field liveness we use a simpler model:
328///   - `use[B]` = fields read in block B.
329///   - `def[B]` = fields written in block B (kills liveness for fields defined
330///     before being read within the same block).
331///   - `live_in[B] = (live_out[B] − def_before_use[B]) ∪ use[B]`
332fn compute_field_liveness(
333    mir: &MirFunction,
334    cfg: &ControlFlowGraph,
335    _block_writes: &HashMap<BasicBlockId, HashSet<FieldKey>>,
336    _block_reads: &HashMap<BasicBlockId, HashSet<FieldKey>>,
337) -> HashMap<BasicBlockId, HashSet<FieldKey>> {
338    let rpo = cfg.reverse_postorder();
339
340    // For each block, compute `use_before_def` and `def_before_use`.
341    // A field is "used before def" if it appears as a read before any write
342    // in the same block. A field is "def before use" if it's written before
343    // any read in the same block.
344    let mut use_before_def: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
345    let mut def_before_use: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
346
347    for block in &mir.blocks {
348        let (ubd, dbu) = compute_block_use_def_order(block);
349        use_before_def.insert(block.id, ubd);
350        def_before_use.insert(block.id, dbu);
351    }
352
353    let mut live_in: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
354    let mut live_out: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
355
356    for block in &mir.blocks {
357        live_in.insert(block.id, HashSet::new());
358        live_out.insert(block.id, HashSet::new());
359    }
360
361    let mut changed = true;
362    while changed {
363        changed = false;
364
365        // Process in reverse of RPO for efficient backward analysis.
366        for &block_id in rpo.iter().rev() {
367            // live_out[B] = ∪ live_in[S] for all successors S
368            let mut new_live_out: HashSet<FieldKey> = HashSet::new();
369            for &succ in cfg.successors(block_id) {
370                if let Some(succ_in) = live_in.get(&succ) {
371                    new_live_out.extend(succ_in.iter().cloned());
372                }
373            }
374
375            // live_in[B] = (live_out[B] − def_before_use[B]) ∪ use_before_def[B]
376            let dbu = def_before_use.get(&block_id).cloned().unwrap_or_default();
377            let ubd = use_before_def.get(&block_id).cloned().unwrap_or_default();
378
379            let mut new_live_in: HashSet<FieldKey> =
380                new_live_out.difference(&dbu).cloned().collect();
381            new_live_in.extend(ubd.iter().cloned());
382
383            if new_live_in != *live_in.get(&block_id).unwrap_or(&HashSet::new()) {
384                changed = true;
385                live_in.insert(block_id, new_live_in);
386            }
387            if new_live_out != *live_out.get(&block_id).unwrap_or(&HashSet::new()) {
388                changed = true;
389                live_out.insert(block_id, new_live_out);
390            }
391        }
392    }
393
394    live_in
395}
396
397/// For a single block, compute `(use_before_def, def_before_use)`.
398///
399/// Walk statements in order. For each field key:
400/// - If the first access is a read, it goes into `use_before_def`.
401/// - If the first access is a write, it goes into `def_before_use`.
402fn compute_block_use_def_order(block: &BasicBlock) -> (HashSet<FieldKey>, HashSet<FieldKey>) {
403    let mut use_before_def = HashSet::new();
404    let mut def_before_use = HashSet::new();
405    let mut seen = HashSet::new();
406
407    for stmt in &block.statements {
408        // Collect reads from this statement.
409        let mut stmt_reads = HashSet::new();
410        let mut stmt_writes = HashSet::new();
411
412        match &stmt.kind {
413            StatementKind::Assign(place, rvalue) => {
414                // Reads from the rvalue come first (executed before the write).
415                collect_rvalue_field_reads(rvalue, &mut stmt_reads);
416                if let Some(key) = extract_field_key(place) {
417                    stmt_writes.insert(key);
418                }
419            }
420            StatementKind::Drop(place) => {
421                if let Some(key) = extract_field_key(place) {
422                    stmt_reads.insert(key);
423                }
424            }
425            StatementKind::TaskBoundary(ops, ..)
426            | StatementKind::ClosureCapture { operands: ops, .. }
427            | StatementKind::ArrayStore { operands: ops, .. }
428            | StatementKind::ObjectStore { operands: ops, .. }
429            | StatementKind::EnumStore { operands: ops, .. } => {
430                for op in ops {
431                    collect_operand_field_reads(op, &mut stmt_reads);
432                }
433            }
434            StatementKind::Nop => {}
435        }
436
437        // Reads before writes within the same statement.
438        for key in &stmt_reads {
439            if !seen.contains(key) {
440                use_before_def.insert(*key);
441                seen.insert(*key);
442            }
443        }
444        for key in &stmt_writes {
445            if !seen.contains(key) {
446                def_before_use.insert(*key);
447                seen.insert(*key);
448            }
449        }
450    }
451
452    // Also account for terminator reads.
453    let mut term_reads = HashSet::new();
454    collect_terminator_field_reads(&block.terminator.kind, &mut term_reads);
455    for key in &term_reads {
456        if !seen.contains(key) {
457            use_before_def.insert(*key);
458            // seen.insert not needed — last pass
459        }
460    }
461
462    (use_before_def, def_before_use)
463}
464
465// ── Conditional initialization detection ───────────────────────────────
466
467/// A field is conditionally initialized if it is written on at least one path
468/// but at some block where it is read, it is NOT in the definitely-initialized
469/// set.
470fn compute_conditionally_initialized(
471    mir: &MirFunction,
472    block_reads: &HashMap<BasicBlockId, HashSet<FieldKey>>,
473    definitely_initialized: &HashMap<BasicBlockId, HashSet<FieldKey>>,
474    all_writes: &HashSet<FieldKey>,
475) -> HashSet<FieldKey> {
476    let mut conditionally = HashSet::new();
477
478    for block in &mir.blocks {
479        let reads = match block_reads.get(&block.id) {
480            Some(r) => r,
481            None => continue,
482        };
483        let init = definitely_initialized
484            .get(&block.id)
485            .cloned()
486            .unwrap_or_default();
487
488        for key in reads {
489            // The field is written somewhere (it's in all_writes) but not
490            // definitely initialized at this read point.
491            if all_writes.contains(key) && !init.contains(key) {
492                conditionally.insert(*key);
493            }
494        }
495    }
496
497    conditionally
498}
499
500// ── Tests ──────────────────────────────────────────────────────────────
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::mir::cfg::ControlFlowGraph;
506
507    fn span() -> shape_ast::ast::Span {
508        shape_ast::ast::Span { start: 0, end: 1 }
509    }
510
511    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
512        MirStatement {
513            kind,
514            span: span(),
515            point: Point(point),
516        }
517    }
518
519    fn make_terminator(kind: TerminatorKind) -> Terminator {
520        Terminator { kind, span: span() }
521    }
522
523    fn field_place(slot: u16, field: u16) -> Place {
524        Place::Field(Box::new(Place::Local(SlotId(slot))), FieldIdx(field))
525    }
526
527    // ── Test: unconditional initialization ─────────────────────────────
528
529    #[test]
530    fn test_unconditional_field_init() {
531        // bb0: _0.0 = 1; _0.1 = 2; return
532        // Both fields are definitely initialized at exit of bb0.
533        let mir = MirFunction {
534            name: "test".to_string(),
535            blocks: vec![BasicBlock {
536                id: BasicBlockId(0),
537                statements: vec![
538                    make_stmt(
539                        StatementKind::Assign(
540                            field_place(0, 0),
541                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
542                        ),
543                        0,
544                    ),
545                    make_stmt(
546                        StatementKind::Assign(
547                            field_place(0, 1),
548                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
549                        ),
550                        1,
551                    ),
552                ],
553                terminator: make_terminator(TerminatorKind::Return),
554            }],
555            num_locals: 1,
556            param_slots: vec![],
557            param_reference_kinds: vec![],
558            local_types: vec![LocalTypeInfo::NonCopy],
559            span: span(),
560        };
561
562        let cfg = ControlFlowGraph::build(&mir);
563        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
564
565        // At entry of bb0, nothing is initialized (correct: writes happen inside).
566        let init_at_entry = result
567            .definitely_initialized
568            .get(&BasicBlockId(0))
569            .cloned()
570            .unwrap_or_default();
571        assert!(init_at_entry.is_empty());
572
573        // Both fields were written but never read → dead fields.
574        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
575        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(1))));
576
577        // No conditional initialization (there's only one path).
578        assert!(result.conditionally_initialized.is_empty());
579    }
580
581    // ── Test: conditional initialization (if/else) ─────────────────────
582
583    #[test]
584    fn test_conditional_field_init() {
585        // bb0: if cond goto bb1 else bb2
586        // bb1: _0.0 = 1; goto bb3
587        // bb2: goto bb3
588        // bb3: use _0.0; return
589        //
590        // _0.0 is only initialized in bb1, not bb2 → conditionally initialized.
591        let mir = MirFunction {
592            name: "test".to_string(),
593            blocks: vec![
594                BasicBlock {
595                    id: BasicBlockId(0),
596                    statements: vec![],
597                    terminator: make_terminator(TerminatorKind::SwitchBool {
598                        operand: Operand::Constant(MirConstant::Bool(true)),
599                        true_bb: BasicBlockId(1),
600                        false_bb: BasicBlockId(2),
601                    }),
602                },
603                BasicBlock {
604                    id: BasicBlockId(1),
605                    statements: vec![make_stmt(
606                        StatementKind::Assign(
607                            field_place(0, 0),
608                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
609                        ),
610                        0,
611                    )],
612                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
613                },
614                BasicBlock {
615                    id: BasicBlockId(2),
616                    statements: vec![],
617                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
618                },
619                BasicBlock {
620                    id: BasicBlockId(3),
621                    statements: vec![make_stmt(
622                        StatementKind::Assign(
623                            Place::Local(SlotId(1)),
624                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
625                        ),
626                        1,
627                    )],
628                    terminator: make_terminator(TerminatorKind::Return),
629                },
630            ],
631            num_locals: 2,
632            param_slots: vec![],
633            param_reference_kinds: vec![],
634            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
635            span: span(),
636        };
637
638        let cfg = ControlFlowGraph::build(&mir);
639        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
640
641        // At bb3 entry, _0.0 should NOT be definitely initialized (missing from bb2 path).
642        let init_at_bb3 = result
643            .definitely_initialized
644            .get(&BasicBlockId(3))
645            .cloned()
646            .unwrap_or_default();
647        assert!(
648            !init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
649            "field should not be definitely initialized at join point"
650        );
651
652        // _0.0 should be conditionally initialized.
653        assert!(
654            result
655                .conditionally_initialized
656                .contains(&(SlotId(0), FieldIdx(0))),
657            "field should be conditionally initialized"
658        );
659    }
660
661    // ── Test: both branches initialize → definitely initialized ────────
662
663    #[test]
664    fn test_both_branches_init() {
665        // bb0: if cond goto bb1 else bb2
666        // bb1: _0.0 = 1; goto bb3
667        // bb2: _0.0 = 2; goto bb3
668        // bb3: use _0.0; return
669        //
670        // _0.0 is initialized on ALL paths → definitely initialized at bb3.
671        let mir = MirFunction {
672            name: "test".to_string(),
673            blocks: vec![
674                BasicBlock {
675                    id: BasicBlockId(0),
676                    statements: vec![],
677                    terminator: make_terminator(TerminatorKind::SwitchBool {
678                        operand: Operand::Constant(MirConstant::Bool(true)),
679                        true_bb: BasicBlockId(1),
680                        false_bb: BasicBlockId(2),
681                    }),
682                },
683                BasicBlock {
684                    id: BasicBlockId(1),
685                    statements: vec![make_stmt(
686                        StatementKind::Assign(
687                            field_place(0, 0),
688                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
689                        ),
690                        0,
691                    )],
692                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
693                },
694                BasicBlock {
695                    id: BasicBlockId(2),
696                    statements: vec![make_stmt(
697                        StatementKind::Assign(
698                            field_place(0, 0),
699                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
700                        ),
701                        1,
702                    )],
703                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
704                },
705                BasicBlock {
706                    id: BasicBlockId(3),
707                    statements: vec![make_stmt(
708                        StatementKind::Assign(
709                            Place::Local(SlotId(1)),
710                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
711                        ),
712                        2,
713                    )],
714                    terminator: make_terminator(TerminatorKind::Return),
715                },
716            ],
717            num_locals: 2,
718            param_slots: vec![],
719            param_reference_kinds: vec![],
720            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
721            span: span(),
722        };
723
724        let cfg = ControlFlowGraph::build(&mir);
725        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
726
727        // At bb3 entry, _0.0 SHOULD be definitely initialized.
728        let init_at_bb3 = result
729            .definitely_initialized
730            .get(&BasicBlockId(3))
731            .cloned()
732            .unwrap_or_default();
733        assert!(
734            init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
735            "field should be definitely initialized when both branches write it"
736        );
737
738        // Not conditionally initialized (all paths cover it).
739        assert!(
740            !result
741                .conditionally_initialized
742                .contains(&(SlotId(0), FieldIdx(0))),
743        );
744
745        // Not dead (it's read in bb3).
746        assert!(
747            !result.dead_fields.contains(&(SlotId(0), FieldIdx(0))),
748            "field is read so should not be dead"
749        );
750    }
751
752    // ── Test: dead field detection ─────────────────────────────────────
753
754    #[test]
755    fn test_dead_field() {
756        // bb0: _0.0 = 1; _0.1 = 2; use _0.0; return
757        // _0.1 is written but never read → dead.
758        let mir = MirFunction {
759            name: "test".to_string(),
760            blocks: vec![BasicBlock {
761                id: BasicBlockId(0),
762                statements: vec![
763                    make_stmt(
764                        StatementKind::Assign(
765                            field_place(0, 0),
766                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
767                        ),
768                        0,
769                    ),
770                    make_stmt(
771                        StatementKind::Assign(
772                            field_place(0, 1),
773                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
774                        ),
775                        1,
776                    ),
777                    make_stmt(
778                        StatementKind::Assign(
779                            Place::Local(SlotId(1)),
780                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
781                        ),
782                        2,
783                    ),
784                ],
785                terminator: make_terminator(TerminatorKind::Return),
786            }],
787            num_locals: 2,
788            param_slots: vec![],
789            param_reference_kinds: vec![],
790            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
791            span: span(),
792        };
793
794        let cfg = ControlFlowGraph::build(&mir);
795        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
796
797        // _0.0 is read → not dead.
798        assert!(!result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
799        // _0.1 is written but never read → dead.
800        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(1))));
801    }
802
803    // ── Test: field liveness ───────────────────────────────────────────
804
805    #[test]
806    fn test_field_liveness() {
807        // bb0: _0.0 = 1; goto bb1
808        // bb1: _1 = _0.0; return
809        //
810        // _0.0 should be live at exit of bb0 (read in bb1).
811        let mir = MirFunction {
812            name: "test".to_string(),
813            blocks: vec![
814                BasicBlock {
815                    id: BasicBlockId(0),
816                    statements: vec![make_stmt(
817                        StatementKind::Assign(
818                            field_place(0, 0),
819                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
820                        ),
821                        0,
822                    )],
823                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
824                },
825                BasicBlock {
826                    id: BasicBlockId(1),
827                    statements: vec![make_stmt(
828                        StatementKind::Assign(
829                            Place::Local(SlotId(1)),
830                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
831                        ),
832                        1,
833                    )],
834                    terminator: make_terminator(TerminatorKind::Return),
835                },
836            ],
837            num_locals: 2,
838            param_slots: vec![],
839            param_reference_kinds: vec![],
840            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
841            span: span(),
842        };
843
844        let cfg = ControlFlowGraph::build(&mir);
845        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
846
847        // _0.0 should be live at entry of bb1 (read there).
848        let live_bb1 = result
849            .field_liveness
850            .get(&BasicBlockId(1))
851            .cloned()
852            .unwrap_or_default();
853        assert!(
854            live_bb1.contains(&(SlotId(0), FieldIdx(0))),
855            "field should be live at entry of block where it is read"
856        );
857
858        // _0.0 should NOT be live at entry of bb0 (it is defined there before
859        // any read within bb0, and the liveness from bb1 propagates back but
860        // is killed by the def in bb0 — but since bb0 only writes, not reads,
861        // and the write is a def_before_use, liveness is killed).
862        // Actually the field is written in bb0 (def_before_use), so live_in[bb0]
863        // should NOT contain it: live_out[bb0] has it, but def_before_use kills it.
864        let live_bb0 = result
865            .field_liveness
866            .get(&BasicBlockId(0))
867            .cloned()
868            .unwrap_or_default();
869        assert!(
870            !live_bb0.contains(&(SlotId(0), FieldIdx(0))),
871            "field defined before use in bb0 should not be live at bb0 entry"
872        );
873    }
874
875    // ── Test: loop-based initialization ────────────────────────────────
876
877    #[test]
878    fn test_loop_init() {
879        // bb0: goto bb1
880        // bb1 (loop header): if cond goto bb2 else bb3
881        // bb2 (loop body): _0.0 = 1; goto bb1
882        // bb3 (exit): use _0.0; return
883        //
884        // _0.0 is only initialized inside the loop body, so at bb3 entry
885        // it is NOT definitely initialized (bb1 can come from bb0 where
886        // _0.0 wasn't written).
887        let mir = MirFunction {
888            name: "test".to_string(),
889            blocks: vec![
890                BasicBlock {
891                    id: BasicBlockId(0),
892                    statements: vec![],
893                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
894                },
895                BasicBlock {
896                    id: BasicBlockId(1),
897                    statements: vec![],
898                    terminator: make_terminator(TerminatorKind::SwitchBool {
899                        operand: Operand::Constant(MirConstant::Bool(true)),
900                        true_bb: BasicBlockId(2),
901                        false_bb: BasicBlockId(3),
902                    }),
903                },
904                BasicBlock {
905                    id: BasicBlockId(2),
906                    statements: vec![make_stmt(
907                        StatementKind::Assign(
908                            field_place(0, 0),
909                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
910                        ),
911                        0,
912                    )],
913                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
914                },
915                BasicBlock {
916                    id: BasicBlockId(3),
917                    statements: vec![make_stmt(
918                        StatementKind::Assign(
919                            Place::Local(SlotId(1)),
920                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
921                        ),
922                        1,
923                    )],
924                    terminator: make_terminator(TerminatorKind::Return),
925                },
926            ],
927            num_locals: 2,
928            param_slots: vec![],
929            param_reference_kinds: vec![],
930            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
931            span: span(),
932        };
933
934        let cfg = ControlFlowGraph::build(&mir);
935        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
936
937        // At bb3 entry (after loop exit), _0.0 is NOT definitely initialized
938        // because the path bb0 → bb1 → bb3 never writes _0.0.
939        let init_at_bb3 = result
940            .definitely_initialized
941            .get(&BasicBlockId(3))
942            .cloned()
943            .unwrap_or_default();
944        assert!(
945            !init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
946            "field initialized only in loop body should not be definitely initialized at loop exit"
947        );
948
949        // It IS conditionally initialized (written in bb2, read in bb3).
950        assert!(
951            result
952                .conditionally_initialized
953                .contains(&(SlotId(0), FieldIdx(0))),
954        );
955    }
956
957    // ── Test: empty function ───────────────────────────────────────────
958
959    #[test]
960    fn test_empty_function() {
961        let mir = MirFunction {
962            name: "empty".to_string(),
963            blocks: vec![BasicBlock {
964                id: BasicBlockId(0),
965                statements: vec![],
966                terminator: make_terminator(TerminatorKind::Return),
967            }],
968            num_locals: 0,
969            param_slots: vec![],
970            param_reference_kinds: vec![],
971            local_types: vec![],
972            span: span(),
973        };
974
975        let cfg = ControlFlowGraph::build(&mir);
976        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
977
978        assert!(result.dead_fields.is_empty());
979        assert!(result.conditionally_initialized.is_empty());
980    }
981
982    // ── Test: multiple slots ───────────────────────────────────────────
983
984    #[test]
985    fn test_multiple_slots() {
986        // bb0: _0.0 = 1; _1.0 = 2; _2 = _0.0 + _1.0; return
987        // Both fields are read → not dead.
988        let mir = MirFunction {
989            name: "test".to_string(),
990            blocks: vec![BasicBlock {
991                id: BasicBlockId(0),
992                statements: vec![
993                    make_stmt(
994                        StatementKind::Assign(
995                            field_place(0, 0),
996                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
997                        ),
998                        0,
999                    ),
1000                    make_stmt(
1001                        StatementKind::Assign(
1002                            field_place(1, 0),
1003                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
1004                        ),
1005                        1,
1006                    ),
1007                    make_stmt(
1008                        StatementKind::Assign(
1009                            Place::Local(SlotId(2)),
1010                            Rvalue::BinaryOp(
1011                                BinOp::Add,
1012                                Operand::Copy(field_place(0, 0)),
1013                                Operand::Copy(field_place(1, 0)),
1014                            ),
1015                        ),
1016                        2,
1017                    ),
1018                ],
1019                terminator: make_terminator(TerminatorKind::Return),
1020            }],
1021            num_locals: 3,
1022            param_slots: vec![],
1023            param_reference_kinds: vec![],
1024            local_types: vec![
1025                LocalTypeInfo::NonCopy,
1026                LocalTypeInfo::NonCopy,
1027                LocalTypeInfo::Copy,
1028            ],
1029            span: span(),
1030        };
1031
1032        let cfg = ControlFlowGraph::build(&mir);
1033        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
1034
1035        // Both fields are read → not dead.
1036        assert!(!result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
1037        assert!(!result.dead_fields.contains(&(SlotId(1), FieldIdx(0))));
1038    }
1039}