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        Rvalue::EnumTest { operand, .. }
183        | Rvalue::EnumPayload { operand, .. }
184        | Rvalue::TypePatternTest { operand, .. }
185        | Rvalue::EnumDiscriminantTest { operand, .. } => {
186            collect_operand_field_reads(operand, reads);
187        }
188    }
189}
190
191/// Extract field reads from an operand.
192fn collect_operand_field_reads(op: &Operand, reads: &mut HashSet<FieldKey>) {
193    match op {
194        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
195            if let Some(key) = extract_field_key(place) {
196                reads.insert(key);
197            }
198        }
199        Operand::Constant(_) => {}
200    }
201}
202
203/// Extract field reads from a terminator.
204fn collect_terminator_field_reads(kind: &TerminatorKind, reads: &mut HashSet<FieldKey>) {
205    match kind {
206        TerminatorKind::SwitchBool { operand, .. } => {
207            collect_operand_field_reads(operand, reads);
208        }
209        TerminatorKind::Call { func, args, .. } => {
210            collect_operand_field_reads(func, reads);
211            for arg in args {
212                collect_operand_field_reads(arg, reads);
213            }
214        }
215        TerminatorKind::Goto(_) | TerminatorKind::Return | TerminatorKind::Unreachable => {}
216    }
217}
218
219/// Extract the `(SlotId, FieldIdx)` from a `Place::Field(Place::Local(slot), idx)`.
220/// Returns `None` for non-field places or nested field paths (we only track
221/// single-level fields).
222fn extract_field_key(place: &Place) -> Option<FieldKey> {
223    match place {
224        Place::Field(base, idx) => match base.as_ref() {
225            Place::Local(slot) => Some((*slot, *idx)),
226            _ => None,
227        },
228        _ => None,
229    }
230}
231
232// ── Forward dataflow: definite initialization ──────────────────────────
233
234/// Compute which fields are definitely initialized at the entry of each block.
235///
236/// Lattice: `HashSet<FieldKey>` with intersection as meet.
237///   - Entry block: empty set (nothing initialized).
238///   - Transfer: `out[B] = in[B] ∪ writes[B]` (once a field is written, it
239///     stays initialized on that path).
240///   - Merge at join points: `in[B] = ∩ out[P] for all predecessors P`.
241///     A field is definitely initialized only if ALL predecessor paths
242///     initialize it.
243fn compute_definite_initialization(
244    mir: &MirFunction,
245    cfg: &ControlFlowGraph,
246    block_writes: &HashMap<BasicBlockId, HashSet<FieldKey>>,
247) -> HashMap<BasicBlockId, HashSet<FieldKey>> {
248    let rpo = cfg.reverse_postorder();
249    let entry = mir.entry_block();
250
251    // `init_out[B]` = definitely initialized fields at the *exit* of block B.
252    let mut init_in: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
253    let mut init_out: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
254
255    // Collect the universe of all field keys for the "TOP" element.
256    // For definite init, top = all fields (intersection identity), bottom = empty.
257    let universe: HashSet<FieldKey> = block_writes.values().flatten().cloned().collect();
258
259    // Initialize: entry gets empty (nothing initialized); all others get TOP.
260    for block in &mir.blocks {
261        if block.id == entry {
262            init_in.insert(block.id, HashSet::new());
263        } else {
264            init_in.insert(block.id, universe.clone());
265        }
266    }
267
268    // Apply transfer for initial out values.
269    for block in &mir.blocks {
270        let in_set = init_in.get(&block.id).cloned().unwrap_or_default();
271        let writes = block_writes.get(&block.id).cloned().unwrap_or_default();
272        let out_set: HashSet<FieldKey> = in_set.union(&writes).cloned().collect();
273        init_out.insert(block.id, out_set);
274    }
275
276    // Iterate until fixpoint.
277    let mut changed = true;
278    while changed {
279        changed = false;
280
281        for &block_id in &rpo {
282            // Merge: intersect out-sets of all predecessors.
283            let preds = cfg.predecessors(block_id);
284            let new_in = if block_id == entry {
285                HashSet::new()
286            } else if preds.is_empty() {
287                // Unreachable block — leave as universe (won't affect results).
288                universe.clone()
289            } else {
290                let mut merged = init_out
291                    .get(&preds[0])
292                    .cloned()
293                    .unwrap_or_else(|| universe.clone());
294                for &pred in &preds[1..] {
295                    let pred_out = init_out
296                        .get(&pred)
297                        .cloned()
298                        .unwrap_or_else(|| universe.clone());
299                    merged = merged.intersection(&pred_out).cloned().collect();
300                }
301                merged
302            };
303
304            // Transfer: out = in ∪ writes
305            let writes = block_writes.get(&block_id).cloned().unwrap_or_default();
306            let new_out: HashSet<FieldKey> = new_in.union(&writes).cloned().collect();
307
308            if new_in != *init_in.get(&block_id).unwrap_or(&HashSet::new()) {
309                changed = true;
310                init_in.insert(block_id, new_in);
311            }
312            if new_out != *init_out.get(&block_id).unwrap_or(&HashSet::new()) {
313                changed = true;
314                init_out.insert(block_id, new_out);
315            }
316        }
317    }
318
319    init_in
320}
321
322// ── Backward dataflow: field liveness ──────────────────────────────────
323
324/// Compute which fields are live (have future reads) at the entry of each block.
325///
326/// Standard backward liveness:
327///   - `live_out[B] = ∪ live_in[S] for all successors S`.
328///   - `live_in[B] = (live_out[B] − kill[B]) ∪ use[B]`
329///     where `kill[B]` is the set of fields definitely overwritten (we don't
330///     kill in this analysis since a write doesn't prevent an earlier read from
331///     being live — it's the same as standard variable liveness but for fields).
332///
333///   Actually for field liveness we use a simpler model:
334///   - `use[B]` = fields read in block B.
335///   - `def[B]` = fields written in block B (kills liveness for fields defined
336///     before being read within the same block).
337///   - `live_in[B] = (live_out[B] − def_before_use[B]) ∪ use[B]`
338fn compute_field_liveness(
339    mir: &MirFunction,
340    cfg: &ControlFlowGraph,
341    _block_writes: &HashMap<BasicBlockId, HashSet<FieldKey>>,
342    _block_reads: &HashMap<BasicBlockId, HashSet<FieldKey>>,
343) -> HashMap<BasicBlockId, HashSet<FieldKey>> {
344    let rpo = cfg.reverse_postorder();
345
346    // For each block, compute `use_before_def` and `def_before_use`.
347    // A field is "used before def" if it appears as a read before any write
348    // in the same block. A field is "def before use" if it's written before
349    // any read in the same block.
350    let mut use_before_def: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
351    let mut def_before_use: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
352
353    for block in &mir.blocks {
354        let (ubd, dbu) = compute_block_use_def_order(block);
355        use_before_def.insert(block.id, ubd);
356        def_before_use.insert(block.id, dbu);
357    }
358
359    let mut live_in: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
360    let mut live_out: HashMap<BasicBlockId, HashSet<FieldKey>> = HashMap::new();
361
362    for block in &mir.blocks {
363        live_in.insert(block.id, HashSet::new());
364        live_out.insert(block.id, HashSet::new());
365    }
366
367    let mut changed = true;
368    while changed {
369        changed = false;
370
371        // Process in reverse of RPO for efficient backward analysis.
372        for &block_id in rpo.iter().rev() {
373            // live_out[B] = ∪ live_in[S] for all successors S
374            let mut new_live_out: HashSet<FieldKey> = HashSet::new();
375            for &succ in cfg.successors(block_id) {
376                if let Some(succ_in) = live_in.get(&succ) {
377                    new_live_out.extend(succ_in.iter().cloned());
378                }
379            }
380
381            // live_in[B] = (live_out[B] − def_before_use[B]) ∪ use_before_def[B]
382            let dbu = def_before_use.get(&block_id).cloned().unwrap_or_default();
383            let ubd = use_before_def.get(&block_id).cloned().unwrap_or_default();
384
385            let mut new_live_in: HashSet<FieldKey> =
386                new_live_out.difference(&dbu).cloned().collect();
387            new_live_in.extend(ubd.iter().cloned());
388
389            if new_live_in != *live_in.get(&block_id).unwrap_or(&HashSet::new()) {
390                changed = true;
391                live_in.insert(block_id, new_live_in);
392            }
393            if new_live_out != *live_out.get(&block_id).unwrap_or(&HashSet::new()) {
394                changed = true;
395                live_out.insert(block_id, new_live_out);
396            }
397        }
398    }
399
400    live_in
401}
402
403/// For a single block, compute `(use_before_def, def_before_use)`.
404///
405/// Walk statements in order. For each field key:
406/// - If the first access is a read, it goes into `use_before_def`.
407/// - If the first access is a write, it goes into `def_before_use`.
408fn compute_block_use_def_order(block: &BasicBlock) -> (HashSet<FieldKey>, HashSet<FieldKey>) {
409    let mut use_before_def = HashSet::new();
410    let mut def_before_use = HashSet::new();
411    let mut seen = HashSet::new();
412
413    for stmt in &block.statements {
414        // Collect reads from this statement.
415        let mut stmt_reads = HashSet::new();
416        let mut stmt_writes = HashSet::new();
417
418        match &stmt.kind {
419            StatementKind::Assign(place, rvalue) => {
420                // Reads from the rvalue come first (executed before the write).
421                collect_rvalue_field_reads(rvalue, &mut stmt_reads);
422                if let Some(key) = extract_field_key(place) {
423                    stmt_writes.insert(key);
424                }
425            }
426            StatementKind::Drop(place) => {
427                if let Some(key) = extract_field_key(place) {
428                    stmt_reads.insert(key);
429                }
430            }
431            StatementKind::TaskBoundary(ops, ..)
432            | StatementKind::ClosureCapture { operands: ops, .. }
433            | StatementKind::ArrayStore { operands: ops, .. }
434            | StatementKind::ObjectStore { operands: ops, .. }
435            | StatementKind::EnumStore { operands: ops, .. } => {
436                for op in ops {
437                    collect_operand_field_reads(op, &mut stmt_reads);
438                }
439            }
440            StatementKind::Nop => {}
441        }
442
443        // Reads before writes within the same statement.
444        for key in &stmt_reads {
445            if !seen.contains(key) {
446                use_before_def.insert(*key);
447                seen.insert(*key);
448            }
449        }
450        for key in &stmt_writes {
451            if !seen.contains(key) {
452                def_before_use.insert(*key);
453                seen.insert(*key);
454            }
455        }
456    }
457
458    // Also account for terminator reads.
459    let mut term_reads = HashSet::new();
460    collect_terminator_field_reads(&block.terminator.kind, &mut term_reads);
461    for key in &term_reads {
462        if !seen.contains(key) {
463            use_before_def.insert(*key);
464            // seen.insert not needed — last pass
465        }
466    }
467
468    (use_before_def, def_before_use)
469}
470
471// ── Conditional initialization detection ───────────────────────────────
472
473/// A field is conditionally initialized if it is written on at least one path
474/// but at some block where it is read, it is NOT in the definitely-initialized
475/// set.
476fn compute_conditionally_initialized(
477    mir: &MirFunction,
478    block_reads: &HashMap<BasicBlockId, HashSet<FieldKey>>,
479    definitely_initialized: &HashMap<BasicBlockId, HashSet<FieldKey>>,
480    all_writes: &HashSet<FieldKey>,
481) -> HashSet<FieldKey> {
482    let mut conditionally = HashSet::new();
483
484    for block in &mir.blocks {
485        let reads = match block_reads.get(&block.id) {
486            Some(r) => r,
487            None => continue,
488        };
489        let init = definitely_initialized
490            .get(&block.id)
491            .cloned()
492            .unwrap_or_default();
493
494        for key in reads {
495            // The field is written somewhere (it's in all_writes) but not
496            // definitely initialized at this read point.
497            if all_writes.contains(key) && !init.contains(key) {
498                conditionally.insert(*key);
499            }
500        }
501    }
502
503    conditionally
504}
505
506// ── Tests ──────────────────────────────────────────────────────────────
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::mir::cfg::ControlFlowGraph;
512
513    fn span() -> shape_ast::ast::Span {
514        shape_ast::ast::Span { start: 0, end: 1 }
515    }
516
517    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
518        MirStatement {
519            kind,
520            span: span(),
521            point: Point(point),
522        }
523    }
524
525    fn make_terminator(kind: TerminatorKind) -> Terminator {
526        Terminator { kind, span: span() }
527    }
528
529    fn field_place(slot: u16, field: u16) -> Place {
530        Place::Field(Box::new(Place::Local(SlotId(slot))), FieldIdx(field))
531    }
532
533    // ── Test: unconditional initialization ─────────────────────────────
534
535    #[test]
536    fn test_unconditional_field_init() {
537        // bb0: _0.0 = 1; _0.1 = 2; return
538        // Both fields are definitely initialized at exit of bb0.
539        let mir = MirFunction {
540            name: "test".to_string(),
541            blocks: vec![BasicBlock {
542                id: BasicBlockId(0),
543                statements: vec![
544                    make_stmt(
545                        StatementKind::Assign(
546                            field_place(0, 0),
547                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
548                        ),
549                        0,
550                    ),
551                    make_stmt(
552                        StatementKind::Assign(
553                            field_place(0, 1),
554                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
555                        ),
556                        1,
557                    ),
558                ],
559                terminator: make_terminator(TerminatorKind::Return),
560            }],
561            num_locals: 1,
562            param_slots: vec![],
563            param_reference_kinds: vec![],
564            local_types: vec![LocalTypeInfo::NonCopy],
565            span: span(),
566            field_name_table: std::collections::HashMap::new(),
567            local_struct_type_names: std::collections::HashMap::new(),
568            local_typed_array_element_types: std::collections::HashMap::new(),
569            local_declared_scalar_types: std::collections::HashMap::new(),
570        };
571
572        let cfg = ControlFlowGraph::build(&mir);
573        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
574
575        // At entry of bb0, nothing is initialized (correct: writes happen inside).
576        let init_at_entry = result
577            .definitely_initialized
578            .get(&BasicBlockId(0))
579            .cloned()
580            .unwrap_or_default();
581        assert!(init_at_entry.is_empty());
582
583        // Both fields were written but never read → dead fields.
584        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
585        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(1))));
586
587        // No conditional initialization (there's only one path).
588        assert!(result.conditionally_initialized.is_empty());
589    }
590
591    // ── Test: conditional initialization (if/else) ─────────────────────
592
593    #[test]
594    fn test_conditional_field_init() {
595        // bb0: if cond goto bb1 else bb2
596        // bb1: _0.0 = 1; goto bb3
597        // bb2: goto bb3
598        // bb3: use _0.0; return
599        //
600        // _0.0 is only initialized in bb1, not bb2 → conditionally initialized.
601        let mir = MirFunction {
602            name: "test".to_string(),
603            blocks: vec![
604                BasicBlock {
605                    id: BasicBlockId(0),
606                    statements: vec![],
607                    terminator: make_terminator(TerminatorKind::SwitchBool {
608                        operand: Operand::Constant(MirConstant::Bool(true)),
609                        true_bb: BasicBlockId(1),
610                        false_bb: BasicBlockId(2),
611                    }),
612                },
613                BasicBlock {
614                    id: BasicBlockId(1),
615                    statements: vec![make_stmt(
616                        StatementKind::Assign(
617                            field_place(0, 0),
618                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
619                        ),
620                        0,
621                    )],
622                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
623                },
624                BasicBlock {
625                    id: BasicBlockId(2),
626                    statements: vec![],
627                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
628                },
629                BasicBlock {
630                    id: BasicBlockId(3),
631                    statements: vec![make_stmt(
632                        StatementKind::Assign(
633                            Place::Local(SlotId(1)),
634                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
635                        ),
636                        1,
637                    )],
638                    terminator: make_terminator(TerminatorKind::Return),
639                },
640            ],
641            num_locals: 2,
642            param_slots: vec![],
643            param_reference_kinds: vec![],
644            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
645            span: span(),
646            field_name_table: std::collections::HashMap::new(),
647            local_struct_type_names: std::collections::HashMap::new(),
648            local_typed_array_element_types: std::collections::HashMap::new(),
649            local_declared_scalar_types: std::collections::HashMap::new(),
650        };
651
652        let cfg = ControlFlowGraph::build(&mir);
653        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
654
655        // At bb3 entry, _0.0 should NOT be definitely initialized (missing from bb2 path).
656        let init_at_bb3 = result
657            .definitely_initialized
658            .get(&BasicBlockId(3))
659            .cloned()
660            .unwrap_or_default();
661        assert!(
662            !init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
663            "field should not be definitely initialized at join point"
664        );
665
666        // _0.0 should be conditionally initialized.
667        assert!(
668            result
669                .conditionally_initialized
670                .contains(&(SlotId(0), FieldIdx(0))),
671            "field should be conditionally initialized"
672        );
673    }
674
675    // ── Test: both branches initialize → definitely initialized ────────
676
677    #[test]
678    fn test_both_branches_init() {
679        // bb0: if cond goto bb1 else bb2
680        // bb1: _0.0 = 1; goto bb3
681        // bb2: _0.0 = 2; goto bb3
682        // bb3: use _0.0; return
683        //
684        // _0.0 is initialized on ALL paths → definitely initialized at bb3.
685        let mir = MirFunction {
686            name: "test".to_string(),
687            blocks: vec![
688                BasicBlock {
689                    id: BasicBlockId(0),
690                    statements: vec![],
691                    terminator: make_terminator(TerminatorKind::SwitchBool {
692                        operand: Operand::Constant(MirConstant::Bool(true)),
693                        true_bb: BasicBlockId(1),
694                        false_bb: BasicBlockId(2),
695                    }),
696                },
697                BasicBlock {
698                    id: BasicBlockId(1),
699                    statements: vec![make_stmt(
700                        StatementKind::Assign(
701                            field_place(0, 0),
702                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
703                        ),
704                        0,
705                    )],
706                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
707                },
708                BasicBlock {
709                    id: BasicBlockId(2),
710                    statements: vec![make_stmt(
711                        StatementKind::Assign(
712                            field_place(0, 0),
713                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
714                        ),
715                        1,
716                    )],
717                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
718                },
719                BasicBlock {
720                    id: BasicBlockId(3),
721                    statements: vec![make_stmt(
722                        StatementKind::Assign(
723                            Place::Local(SlotId(1)),
724                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
725                        ),
726                        2,
727                    )],
728                    terminator: make_terminator(TerminatorKind::Return),
729                },
730            ],
731            num_locals: 2,
732            param_slots: vec![],
733            param_reference_kinds: vec![],
734            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
735            span: span(),
736            field_name_table: std::collections::HashMap::new(),
737            local_struct_type_names: std::collections::HashMap::new(),
738            local_typed_array_element_types: std::collections::HashMap::new(),
739            local_declared_scalar_types: std::collections::HashMap::new(),
740        };
741
742        let cfg = ControlFlowGraph::build(&mir);
743        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
744
745        // At bb3 entry, _0.0 SHOULD be definitely initialized.
746        let init_at_bb3 = result
747            .definitely_initialized
748            .get(&BasicBlockId(3))
749            .cloned()
750            .unwrap_or_default();
751        assert!(
752            init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
753            "field should be definitely initialized when both branches write it"
754        );
755
756        // Not conditionally initialized (all paths cover it).
757        assert!(
758            !result
759                .conditionally_initialized
760                .contains(&(SlotId(0), FieldIdx(0))),
761        );
762
763        // Not dead (it's read in bb3).
764        assert!(
765            !result.dead_fields.contains(&(SlotId(0), FieldIdx(0))),
766            "field is read so should not be dead"
767        );
768    }
769
770    // ── Test: dead field detection ─────────────────────────────────────
771
772    #[test]
773    fn test_dead_field() {
774        // bb0: _0.0 = 1; _0.1 = 2; use _0.0; return
775        // _0.1 is written but never read → dead.
776        let mir = MirFunction {
777            name: "test".to_string(),
778            blocks: vec![BasicBlock {
779                id: BasicBlockId(0),
780                statements: vec![
781                    make_stmt(
782                        StatementKind::Assign(
783                            field_place(0, 0),
784                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
785                        ),
786                        0,
787                    ),
788                    make_stmt(
789                        StatementKind::Assign(
790                            field_place(0, 1),
791                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
792                        ),
793                        1,
794                    ),
795                    make_stmt(
796                        StatementKind::Assign(
797                            Place::Local(SlotId(1)),
798                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
799                        ),
800                        2,
801                    ),
802                ],
803                terminator: make_terminator(TerminatorKind::Return),
804            }],
805            num_locals: 2,
806            param_slots: vec![],
807            param_reference_kinds: vec![],
808            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
809            span: span(),
810            field_name_table: std::collections::HashMap::new(),
811            local_struct_type_names: std::collections::HashMap::new(),
812            local_typed_array_element_types: std::collections::HashMap::new(),
813            local_declared_scalar_types: std::collections::HashMap::new(),
814        };
815
816        let cfg = ControlFlowGraph::build(&mir);
817        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
818
819        // _0.0 is read → not dead.
820        assert!(!result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
821        // _0.1 is written but never read → dead.
822        assert!(result.dead_fields.contains(&(SlotId(0), FieldIdx(1))));
823    }
824
825    // ── Test: field liveness ───────────────────────────────────────────
826
827    #[test]
828    fn test_field_liveness() {
829        // bb0: _0.0 = 1; goto bb1
830        // bb1: _1 = _0.0; return
831        //
832        // _0.0 should be live at exit of bb0 (read in bb1).
833        let mir = MirFunction {
834            name: "test".to_string(),
835            blocks: vec![
836                BasicBlock {
837                    id: BasicBlockId(0),
838                    statements: vec![make_stmt(
839                        StatementKind::Assign(
840                            field_place(0, 0),
841                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
842                        ),
843                        0,
844                    )],
845                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
846                },
847                BasicBlock {
848                    id: BasicBlockId(1),
849                    statements: vec![make_stmt(
850                        StatementKind::Assign(
851                            Place::Local(SlotId(1)),
852                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
853                        ),
854                        1,
855                    )],
856                    terminator: make_terminator(TerminatorKind::Return),
857                },
858            ],
859            num_locals: 2,
860            param_slots: vec![],
861            param_reference_kinds: vec![],
862            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
863            span: span(),
864            field_name_table: std::collections::HashMap::new(),
865            local_struct_type_names: std::collections::HashMap::new(),
866            local_typed_array_element_types: std::collections::HashMap::new(),
867            local_declared_scalar_types: std::collections::HashMap::new(),
868        };
869
870        let cfg = ControlFlowGraph::build(&mir);
871        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
872
873        // _0.0 should be live at entry of bb1 (read there).
874        let live_bb1 = result
875            .field_liveness
876            .get(&BasicBlockId(1))
877            .cloned()
878            .unwrap_or_default();
879        assert!(
880            live_bb1.contains(&(SlotId(0), FieldIdx(0))),
881            "field should be live at entry of block where it is read"
882        );
883
884        // _0.0 should NOT be live at entry of bb0 (it is defined there before
885        // any read within bb0, and the liveness from bb1 propagates back but
886        // is killed by the def in bb0 — but since bb0 only writes, not reads,
887        // and the write is a def_before_use, liveness is killed).
888        // Actually the field is written in bb0 (def_before_use), so live_in[bb0]
889        // should NOT contain it: live_out[bb0] has it, but def_before_use kills it.
890        let live_bb0 = result
891            .field_liveness
892            .get(&BasicBlockId(0))
893            .cloned()
894            .unwrap_or_default();
895        assert!(
896            !live_bb0.contains(&(SlotId(0), FieldIdx(0))),
897            "field defined before use in bb0 should not be live at bb0 entry"
898        );
899    }
900
901    // ── Test: loop-based initialization ────────────────────────────────
902
903    #[test]
904    fn test_loop_init() {
905        // bb0: goto bb1
906        // bb1 (loop header): if cond goto bb2 else bb3
907        // bb2 (loop body): _0.0 = 1; goto bb1
908        // bb3 (exit): use _0.0; return
909        //
910        // _0.0 is only initialized inside the loop body, so at bb3 entry
911        // it is NOT definitely initialized (bb1 can come from bb0 where
912        // _0.0 wasn't written).
913        let mir = MirFunction {
914            name: "test".to_string(),
915            blocks: vec![
916                BasicBlock {
917                    id: BasicBlockId(0),
918                    statements: vec![],
919                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
920                },
921                BasicBlock {
922                    id: BasicBlockId(1),
923                    statements: vec![],
924                    terminator: make_terminator(TerminatorKind::SwitchBool {
925                        operand: Operand::Constant(MirConstant::Bool(true)),
926                        true_bb: BasicBlockId(2),
927                        false_bb: BasicBlockId(3),
928                    }),
929                },
930                BasicBlock {
931                    id: BasicBlockId(2),
932                    statements: vec![make_stmt(
933                        StatementKind::Assign(
934                            field_place(0, 0),
935                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
936                        ),
937                        0,
938                    )],
939                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
940                },
941                BasicBlock {
942                    id: BasicBlockId(3),
943                    statements: vec![make_stmt(
944                        StatementKind::Assign(
945                            Place::Local(SlotId(1)),
946                            Rvalue::Use(Operand::Copy(field_place(0, 0))),
947                        ),
948                        1,
949                    )],
950                    terminator: make_terminator(TerminatorKind::Return),
951                },
952            ],
953            num_locals: 2,
954            param_slots: vec![],
955            param_reference_kinds: vec![],
956            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::Copy],
957            span: span(),
958            field_name_table: std::collections::HashMap::new(),
959            local_struct_type_names: std::collections::HashMap::new(),
960            local_typed_array_element_types: std::collections::HashMap::new(),
961            local_declared_scalar_types: std::collections::HashMap::new(),
962        };
963
964        let cfg = ControlFlowGraph::build(&mir);
965        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
966
967        // At bb3 entry (after loop exit), _0.0 is NOT definitely initialized
968        // because the path bb0 → bb1 → bb3 never writes _0.0.
969        let init_at_bb3 = result
970            .definitely_initialized
971            .get(&BasicBlockId(3))
972            .cloned()
973            .unwrap_or_default();
974        assert!(
975            !init_at_bb3.contains(&(SlotId(0), FieldIdx(0))),
976            "field initialized only in loop body should not be definitely initialized at loop exit"
977        );
978
979        // It IS conditionally initialized (written in bb2, read in bb3).
980        assert!(
981            result
982                .conditionally_initialized
983                .contains(&(SlotId(0), FieldIdx(0))),
984        );
985    }
986
987    // ── Test: empty function ───────────────────────────────────────────
988
989    #[test]
990    fn test_empty_function() {
991        let mir = MirFunction {
992            name: "empty".to_string(),
993            blocks: vec![BasicBlock {
994                id: BasicBlockId(0),
995                statements: vec![],
996                terminator: make_terminator(TerminatorKind::Return),
997            }],
998            num_locals: 0,
999            param_slots: vec![],
1000            param_reference_kinds: vec![],
1001            local_types: vec![],
1002            span: span(),
1003            field_name_table: std::collections::HashMap::new(),
1004            local_struct_type_names: std::collections::HashMap::new(),
1005            local_typed_array_element_types: std::collections::HashMap::new(),
1006            local_declared_scalar_types: std::collections::HashMap::new(),
1007        };
1008
1009        let cfg = ControlFlowGraph::build(&mir);
1010        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
1011
1012        assert!(result.dead_fields.is_empty());
1013        assert!(result.conditionally_initialized.is_empty());
1014    }
1015
1016    // ── Test: multiple slots ───────────────────────────────────────────
1017
1018    #[test]
1019    fn test_multiple_slots() {
1020        // bb0: _0.0 = 1; _1.0 = 2; _2 = _0.0 + _1.0; return
1021        // Both fields are read → not dead.
1022        let mir = MirFunction {
1023            name: "test".to_string(),
1024            blocks: vec![BasicBlock {
1025                id: BasicBlockId(0),
1026                statements: vec![
1027                    make_stmt(
1028                        StatementKind::Assign(
1029                            field_place(0, 0),
1030                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
1031                        ),
1032                        0,
1033                    ),
1034                    make_stmt(
1035                        StatementKind::Assign(
1036                            field_place(1, 0),
1037                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
1038                        ),
1039                        1,
1040                    ),
1041                    make_stmt(
1042                        StatementKind::Assign(
1043                            Place::Local(SlotId(2)),
1044                            Rvalue::BinaryOp(
1045                                BinOp::Add,
1046                                Operand::Copy(field_place(0, 0)),
1047                                Operand::Copy(field_place(1, 0)),
1048                            ),
1049                        ),
1050                        2,
1051                    ),
1052                ],
1053                terminator: make_terminator(TerminatorKind::Return),
1054            }],
1055            num_locals: 3,
1056            param_slots: vec![],
1057            param_reference_kinds: vec![],
1058            local_types: vec![
1059                LocalTypeInfo::NonCopy,
1060                LocalTypeInfo::NonCopy,
1061                LocalTypeInfo::Copy,
1062            ],
1063            span: span(),
1064            field_name_table: std::collections::HashMap::new(),
1065            local_struct_type_names: std::collections::HashMap::new(),
1066            local_typed_array_element_types: std::collections::HashMap::new(),
1067            local_declared_scalar_types: std::collections::HashMap::new(),
1068        };
1069
1070        let cfg = ControlFlowGraph::build(&mir);
1071        let result = analyze_fields(&FieldAnalysisInput { mir: &mir, cfg: &cfg });
1072
1073        // Both fields are read → not dead.
1074        assert!(!result.dead_fields.contains(&(SlotId(0), FieldIdx(0))));
1075        assert!(!result.dead_fields.contains(&(SlotId(1), FieldIdx(0))));
1076    }
1077}