Skip to main content

gruel_cfg/
build.rs

1//! AIR to CFG lowering.
2//!
3//! This module converts the structured control flow in AIR (Branch, Loop)
4//! into explicit basic blocks with terminators.
5
6use gruel_air::{
7    Air, AirArgMode, AirInstData, AirPattern, AirPlaceBase, AirPlaceRef, AirProjection, AirRef,
8    AnalyzedFunction, Type, TypeInternPool,
9};
10use gruel_util::{BinOp, CompileWarning, WarningKind};
11use lasso::ThreadedRodeo;
12
13use crate::CfgOutput;
14use crate::inst::{
15    BlockId, Cfg, CfgArgMode, CfgCallArg, CfgInst, CfgInstData, CfgValue, Place, PlaceBase,
16    Projection, Terminator,
17};
18
19/// A traced place: (base, list of (projection, optional index value)).
20type TracedPlace = Option<(PlaceBase, Vec<(Projection, Option<CfgValue>)>)>;
21
22/// The scrutinee location currently being matched against — base slot, the
23/// projected type after applying `projection`, and the projection chain
24/// itself. Threaded through the pattern dispatch tree so each level can
25/// extend the projection without ballooning argument lists.
26#[derive(Clone, Copy)]
27struct Scrutinee<'a> {
28    slot: u32,
29    ty: Type,
30    projection: &'a [Projection],
31}
32
33/// Where to branch on match / mismatch in the pattern dispatch tree.
34#[derive(Clone, Copy)]
35struct BranchTargets {
36    matched: BlockId,
37    unmatched: BlockId,
38}
39
40/// ADR-0052 Phase 1: a pattern is "trivial" for CFG dispatch purposes
41/// when it contributes no runtime test — it's either a wildcard or a
42/// plain binding whose inner pattern (if any) is itself trivial. Binding
43/// introduction for trivial leaves is handled by sema's arm-body setup,
44/// so CFG can skip them.
45fn is_trivial_pattern(p: &AirPattern) -> bool {
46    match p {
47        AirPattern::Wildcard => true,
48        AirPattern::Bind { inner: None, .. } => true,
49        AirPattern::Bind {
50            inner: Some(inner), ..
51        } => is_trivial_pattern(inner),
52        _ => false,
53    }
54}
55
56/// Result of lowering an expression.
57struct ExprResult {
58    /// The value produced (if any - statements like Store don't produce values)
59    value: Option<CfgValue>,
60    /// Whether control flow continues after this expression
61    continuation: Continuation,
62}
63
64/// How control flow continues after an expression.
65enum Continuation {
66    /// Control continues normally (can add more instructions)
67    Continues,
68    /// Control flow diverged (return, break, continue) - no more instructions
69    Diverged,
70}
71
72/// Loop context for break/continue handling.
73struct LoopContext {
74    /// Block to jump to for continue (loop header)
75    header: BlockId,
76    /// Block to jump to for break (loop exit)
77    exit: BlockId,
78    /// The scope depth when entering the loop (before the loop body scope).
79    /// Used to know how many scopes to drop on break/continue.
80    /// For break/continue, we drop scopes from current down to (but not including)
81    /// this depth.
82    scope_depth: usize,
83}
84
85/// Information about a slot that became live in a scope.
86/// Used for drop elaboration.
87#[derive(Debug, Clone)]
88struct LiveSlot {
89    /// The slot number
90    slot: u32,
91    /// The type of value stored in the slot
92    ty: Type,
93    /// The span where the slot became live (for error reporting)
94    span: gruel_util::Span,
95}
96
97/// A live parameter that needs dropping at function exit.
98#[derive(Debug, Clone)]
99struct LiveParam {
100    /// The parameter slot index
101    param_slot: u32,
102    /// The type of the parameter
103    ty: Type,
104}
105
106/// Builder that converts AIR to CFG.
107pub struct CfgBuilder<'a> {
108    air: &'a Air,
109    cfg: Cfg,
110    /// Type intern pool for struct/enum/array lookups (Phase 2B ADR-0024)
111    type_pool: &'a TypeInternPool,
112    /// Interner for resolving Spur-encoded names (e.g. intrinsic names).
113    interner: &'a ThreadedRodeo,
114    /// Current block we're building
115    current_block: BlockId,
116    /// Stack of loop contexts for nested loops
117    loop_stack: Vec<LoopContext>,
118    /// Cache: maps AIR refs to CFG values (for already-lowered instructions)
119    value_cache: Vec<Option<CfgValue>>,
120    /// Warnings collected during CFG construction (e.g., unreachable code)
121    warnings: Vec<CompileWarning>,
122    /// Stack of scopes for drop elaboration.
123    /// Each scope contains the slots that became live in that scope.
124    /// Used to emit StorageDead (and Drop if needed) at scope exit.
125    scope_stack: Vec<Vec<LiveSlot>>,
126    /// Records the scope-stack depth at which each slot's `StorageLive`
127    /// was first registered. Used by `re_add_local_slot` so a reassignment
128    /// inside an inner scope (e.g. inside a for-loop body) puts the slot
129    /// back into its original outer scope rather than the inner one —
130    /// otherwise the inner scope's exit would drop the freshly-stored
131    /// value at the end of every loop iteration.
132    slot_origin_depth: std::collections::HashMap<u32, usize>,
133    /// Parameters that are live and need dropping at function exit.
134    /// Non-inout parameters whose types need drop are added here at function entry.
135    /// When a parameter is consumed (passed to another function, moved into a struct, etc.),
136    /// it is removed from this list to prevent double-drop.
137    live_params: Vec<LiveParam>,
138}
139
140impl<'a> CfgBuilder<'a> {
141    /// Build a CFG from AIR, returning the CFG and any warnings.
142    ///
143    /// The `type_pool` provides struct/enum/array definitions needed for queries like
144    /// `type_needs_drop`.
145    pub fn build(
146        func: &'a AnalyzedFunction,
147        type_pool: &'a TypeInternPool,
148        interner: &'a ThreadedRodeo,
149    ) -> CfgOutput {
150        // Determine which by-value parameters need dropping at function exit.
151        // Inout/borrow parameters are not owned by the callee and must not be dropped.
152        // Destructors must NOT auto-drop their self parameter — the destructor IS the
153        // drop logic for that value.
154        //
155        // Multi-slot ABI parameters (Vec(T), String, Slice(T) — anything where
156        // `abi_slot_count > 1`) occupy several entries in `param_slot_types`
157        // but represent a single Gruel parameter at the source level. We must
158        // emit Drop exactly once per Gruel parameter; emitting once per slot
159        // would synthesize a multi-free at function exit (the fundamental
160        // shape of the bug fixed here, called out in ADR-0072's Phase 3
161        // open question for `Vec(u8)` pass-by-value). Walk the slot table in
162        // chunks: register a `LiveParam` for the first slot of each Gruel
163        // parameter and skip the trailing slots that belong to that same
164        // composite.
165        let live_params: Vec<LiveParam> = if func.is_destructor {
166            Vec::new()
167        } else {
168            let mut out: Vec<LiveParam> = Vec::new();
169            let mut i = 0usize;
170            while i < func.param_slot_types.len() {
171                let ty = func.param_slot_types[i];
172                let by_ref = func
173                    .param_modes
174                    .get(i)
175                    .copied()
176                    .unwrap_or_default()
177                    .is_by_ref();
178                let slot_count = type_pool.abi_slot_count(ty).max(1) as usize;
179                if !by_ref && crate::drop_names::type_needs_drop(ty, type_pool) {
180                    out.push(LiveParam {
181                        param_slot: i as u32,
182                        ty,
183                    });
184                }
185                i += slot_count;
186            }
187            out
188        };
189
190        let mut builder = CfgBuilder {
191            air: &func.air,
192            cfg: Cfg::new(
193                func.air.return_type(),
194                func.num_locals,
195                func.num_param_slots,
196                func.name.to_string(),
197                func.param_modes.clone(),
198                func.param_slot_types.clone(),
199            ),
200            type_pool,
201            interner,
202            current_block: BlockId(0),
203            loop_stack: Vec::new(),
204            value_cache: vec![None; func.air.len()],
205            warnings: Vec::new(),
206            scope_stack: vec![Vec::new()], // Start with one scope for the function body
207            slot_origin_depth: std::collections::HashMap::new(),
208            live_params,
209        };
210
211        // Create entry block
212        builder.current_block = builder.cfg.new_block();
213        builder.cfg.entry = builder.current_block;
214
215        // Find the root (should be Ret as last instruction)
216        if !func.air.is_empty() {
217            let root = AirRef::from_raw((func.air.len() - 1) as u32);
218            builder.lower_inst(root);
219        }
220
221        // Compute predecessor lists
222        builder.cfg.compute_predecessors();
223
224        CfgOutput {
225            cfg: builder.cfg,
226            warnings: builder.warnings,
227        }
228    }
229
230    /// Lower an AIR instruction, returning its result.
231    fn lower_inst(&mut self, air_ref: AirRef) -> ExprResult {
232        // Check cache first
233        if let Some(cached) = self.value_cache[air_ref.as_u32() as usize] {
234            return ExprResult {
235                value: Some(cached),
236                continuation: Continuation::Continues,
237            };
238        }
239
240        let inst = self.air.get(air_ref);
241        let span = inst.span;
242        let ty = inst.ty;
243
244        match &inst.data {
245            AirInstData::Const(v) => {
246                let value = self.emit(CfgInstData::Const(*v), ty, span);
247                self.cache(air_ref, value);
248                ExprResult {
249                    value: Some(value),
250                    continuation: Continuation::Continues,
251                }
252            }
253
254            AirInstData::FloatConst(bits) => {
255                let value = self.emit(CfgInstData::FloatConst(*bits), ty, span);
256                self.cache(air_ref, value);
257                ExprResult {
258                    value: Some(value),
259                    continuation: Continuation::Continues,
260                }
261            }
262
263            AirInstData::BoolConst(v) => {
264                let value = self.emit(CfgInstData::BoolConst(*v), ty, span);
265                self.cache(air_ref, value);
266                ExprResult {
267                    value: Some(value),
268                    continuation: Continuation::Continues,
269                }
270            }
271
272            AirInstData::StringConst(string_id) => {
273                let value = self.emit(CfgInstData::StringConst(*string_id), ty, span);
274                self.cache(air_ref, value);
275                ExprResult {
276                    value: Some(value),
277                    continuation: Continuation::Continues,
278                }
279            }
280
281            AirInstData::BytesConst(bytes_id) => {
282                let value = self.emit(CfgInstData::BytesConst(*bytes_id), ty, span);
283                self.cache(air_ref, value);
284                ExprResult {
285                    value: Some(value),
286                    continuation: Continuation::Continues,
287                }
288            }
289
290            AirInstData::UnitConst => {
291                // Unit constants have no runtime representation.
292                // We emit a dummy const 0 with unit type for uniformity,
293                // but codegen will ignore values of unit type.
294                let value = self.emit(CfgInstData::Const(0), ty, span);
295                self.cache(air_ref, value);
296                ExprResult {
297                    value: Some(value),
298                    continuation: Continuation::Continues,
299                }
300            }
301
302            AirInstData::TypeConst(_) => {
303                // TypeConst instructions are compile-time-only. They can appear in the AIR
304                // in several valid scenarios:
305                // 1. As arguments to generic functions (substituted during specialization)
306                // 2. As the result of comptime type-returning functions (stored in comptime_type_vars)
307                //
308                // At CFG building time, any TypeConst that remains is simply a no-op -
309                // type values don't exist at runtime. We return Unit with no value to indicate
310                // this instruction doesn't produce runtime code.
311                ExprResult {
312                    value: None,
313                    continuation: Continuation::Continues,
314                }
315            }
316
317            AirInstData::CallGeneric { .. } => {
318                // CallGeneric instructions must be specialized (rewritten to Call)
319                // before CFG building. If we reach here, specialization didn't run.
320                //
321                // TODO(ICE): This should be converted to:
322                //   return Err(ice_error!("CallGeneric not specialized", phase: "cfg_builder"));
323                // But that requires refactoring build() to return CompileResult.
324                panic!(
325                    "CallGeneric instruction reached CFG building - this is a compiler bug. \
326                     CallGeneric must be specialized to regular Call before codegen."
327                );
328            }
329
330            AirInstData::Param { index } => {
331                let value = self.emit(CfgInstData::Param { index: *index }, ty, span);
332                self.cache(air_ref, value);
333                ExprResult {
334                    value: Some(value),
335                    continuation: Continuation::Continues,
336                }
337            }
338
339            // Eager binary ops (arithmetic / comparison / bitwise). The
340            // short-circuit `&&` / `||` are handled as separate Bin arms
341            // below because they need to introduce control flow.
342            AirInstData::Bin(
343                op @ (BinOp::Add
344                | BinOp::Sub
345                | BinOp::Mul
346                | BinOp::Div
347                | BinOp::Mod
348                | BinOp::Eq
349                | BinOp::Ne
350                | BinOp::Lt
351                | BinOp::Gt
352                | BinOp::Le
353                | BinOp::Ge
354                | BinOp::BitAnd
355                | BinOp::BitOr
356                | BinOp::BitXor
357                | BinOp::Shl
358                | BinOp::Shr),
359                lhs,
360                rhs,
361            ) => {
362                let Some(lhs_val) = self.lower_value(*lhs) else {
363                    return Self::diverged();
364                };
365                let Some(rhs_val) = self.lower_value(*rhs) else {
366                    return Self::diverged();
367                };
368                let value = self.emit(CfgInstData::Bin(*op, lhs_val, rhs_val), ty, span);
369                self.cache(air_ref, value);
370                ExprResult {
371                    value: Some(value),
372                    continuation: Continuation::Continues,
373                }
374            }
375
376            AirInstData::Bin(BinOp::And, lhs, rhs) => {
377                // Short-circuit: if lhs is false, result is false
378                // We need to create blocks for this
379                let Some(lhs_val) = self.lower_value(*lhs) else {
380                    return Self::diverged();
381                };
382
383                let rhs_block = self.cfg.new_block();
384                let join_block = self.cfg.new_block();
385
386                // Add block parameter for the result
387                let result_param = self.cfg.add_block_param(join_block, Type::BOOL);
388
389                // Branch: if lhs is false, go to join with false; else evaluate rhs
390                let false_val = self.emit(CfgInstData::BoolConst(false), Type::BOOL, span);
391                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
392                let (else_args_start, else_args_len) =
393                    self.cfg.push_extra(std::iter::once(false_val));
394                self.cfg.set_terminator(
395                    self.current_block,
396                    Terminator::Branch {
397                        cond: lhs_val,
398                        then_block: rhs_block,
399                        then_args_start,
400                        then_args_len,
401                        else_block: join_block,
402                        else_args_start,
403                        else_args_len,
404                    },
405                );
406
407                // In rhs_block, evaluate rhs and go to join
408                self.current_block = rhs_block;
409                let Some(rhs_val) = self.lower_value(*rhs) else {
410                    return Self::diverged();
411                };
412                let (args_start, args_len) = self.cfg.push_extra(std::iter::once(rhs_val));
413                self.cfg.set_terminator(
414                    self.current_block,
415                    Terminator::Goto {
416                        target: join_block,
417                        args_start,
418                        args_len,
419                    },
420                );
421
422                // Continue in join block
423                self.current_block = join_block;
424                self.cache(air_ref, result_param);
425                ExprResult {
426                    value: Some(result_param),
427                    continuation: Continuation::Continues,
428                }
429            }
430
431            AirInstData::Bin(BinOp::Or, lhs, rhs) => {
432                // Short-circuit: if lhs is true, result is true
433                let Some(lhs_val) = self.lower_value(*lhs) else {
434                    return Self::diverged();
435                };
436
437                let rhs_block = self.cfg.new_block();
438                let join_block = self.cfg.new_block();
439
440                // Add block parameter for the result
441                let result_param = self.cfg.add_block_param(join_block, Type::BOOL);
442
443                // Branch: if lhs is true, go to join with true; else evaluate rhs
444                let true_val = self.emit(CfgInstData::BoolConst(true), Type::BOOL, span);
445                let (then_args_start, then_args_len) =
446                    self.cfg.push_extra(std::iter::once(true_val));
447                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
448                self.cfg.set_terminator(
449                    self.current_block,
450                    Terminator::Branch {
451                        cond: lhs_val,
452                        then_block: join_block,
453                        then_args_start,
454                        then_args_len,
455                        else_block: rhs_block,
456                        else_args_start,
457                        else_args_len,
458                    },
459                );
460
461                // In rhs_block, evaluate rhs and go to join
462                self.current_block = rhs_block;
463                let Some(rhs_val) = self.lower_value(*rhs) else {
464                    return Self::diverged();
465                };
466                let (args_start, args_len) = self.cfg.push_extra(std::iter::once(rhs_val));
467                self.cfg.set_terminator(
468                    self.current_block,
469                    Terminator::Goto {
470                        target: join_block,
471                        args_start,
472                        args_len,
473                    },
474                );
475
476                // Continue in join block
477                self.current_block = join_block;
478                self.cache(air_ref, result_param);
479                ExprResult {
480                    value: Some(result_param),
481                    continuation: Continuation::Continues,
482                }
483            }
484
485            AirInstData::Unary(op, operand) => {
486                let Some(op_val) = self.lower_value(*operand) else {
487                    return Self::diverged();
488                };
489                let value = self.emit(CfgInstData::Unary(*op, op_val), ty, span);
490                self.cache(air_ref, value);
491                ExprResult {
492                    value: Some(value),
493                    continuation: Continuation::Continues,
494                }
495            }
496
497            // ADR-0062: `&x` / `&mut x` produces the address of the operand
498            // place. Sema has enforced that the operand is an lvalue, so the
499            // AIR shape under it is either a `Load { slot }` (plain local)
500            // or a `PlaceRead { place }` (field / index path). Recover the
501            // place directly rather than loading then taking its address.
502            AirInstData::MakeRef { operand, is_mut } => {
503                let place = match self.lower_air_lvalue_place(*operand) {
504                    Some(p) => p,
505                    None => return Self::diverged(),
506                };
507                let value = self.emit(
508                    CfgInstData::MakeRef {
509                        place,
510                        is_mut: *is_mut,
511                    },
512                    ty,
513                    span,
514                );
515                self.cache(air_ref, value);
516                ExprResult {
517                    value: Some(value),
518                    continuation: Continuation::Continues,
519                }
520            }
521
522            // ADR-0064: lower the slice borrow into a place + range.
523            AirInstData::MakeSlice {
524                base,
525                lo,
526                hi,
527                is_mut,
528            } => {
529                let place = match self.lower_air_lvalue_place(*base) {
530                    Some(p) => p,
531                    None => return Self::diverged(),
532                };
533                // Determine the source array's compile-time length from the
534                // place's root type.
535                let base_ty = self.air.get(*base).ty;
536                let array_len = match base_ty.kind() {
537                    gruel_air::TypeKind::Array(id) => {
538                        let (_elem, len) = self.type_pool.array_def(id);
539                        len
540                    }
541                    _ => 0,
542                };
543                let vec_base = matches!(base_ty.kind(), gruel_air::TypeKind::Vec(_));
544                let lo_val = match lo {
545                    Some(lo) => Some(match self.lower_value(*lo) {
546                        Some(v) => v,
547                        None => return Self::diverged(),
548                    }),
549                    None => None,
550                };
551                let hi_val = match hi {
552                    Some(hi) => Some(match self.lower_value(*hi) {
553                        Some(v) => v,
554                        None => return Self::diverged(),
555                    }),
556                    None => None,
557                };
558                let value = self.emit(
559                    CfgInstData::MakeSlice(Box::new(crate::MakeSliceData {
560                        place,
561                        array_len,
562                        lo: lo_val,
563                        hi: hi_val,
564                        is_mut: *is_mut,
565                        vec_base,
566                    })),
567                    ty,
568                    span,
569                );
570                self.cache(air_ref, value);
571                ExprResult {
572                    value: Some(value),
573                    continuation: Continuation::Continues,
574                }
575            }
576
577            AirInstData::Alloc { slot, init } => {
578                // If the init is a non-copy value from a local/param,
579                // remove it from scope tracking (ownership transfers to the new slot).
580                self.forget_consumed_value(*init);
581
582                let init_result = self.lower_inst(*init);
583                // If init produces a value, use it; otherwise use a dummy Unit value
584                let init_val = init_result
585                    .value
586                    .unwrap_or_else(|| self.emit(CfgInstData::Const(0), Type::UNIT, span));
587                self.emit(
588                    CfgInstData::Alloc {
589                        slot: *slot,
590                        init: init_val,
591                    },
592                    Type::UNIT,
593                    span,
594                );
595                ExprResult {
596                    value: None,
597                    continuation: Continuation::Continues,
598                }
599            }
600
601            AirInstData::Load { slot } => {
602                let value = self.emit(CfgInstData::Load { slot: *slot }, ty, span);
603                self.cache(air_ref, value);
604                ExprResult {
605                    value: Some(value),
606                    continuation: Continuation::Continues,
607                }
608            }
609
610            AirInstData::Store {
611                slot,
612                value,
613                had_live_value,
614            } => {
615                let Some(val) = self.lower_value(*value) else {
616                    return Self::diverged();
617                };
618                let value_ty = self.air.get(*value).ty;
619                // Drop the old value if it was live (not moved) and the type has a destructor.
620                if *had_live_value && self.type_needs_drop(value_ty) {
621                    let old_val = self.emit(CfgInstData::Load { slot: *slot }, value_ty, span);
622                    self.emit(CfgInstData::Drop { value: old_val }, Type::UNIT, span);
623                }
624                // If the old value was not live (reassignment after move), the slot was
625                // removed from scope tracking by forget_local_slot. Re-add it so the new
626                // value gets dropped at scope exit.
627                if !*had_live_value && self.type_needs_drop(value_ty) {
628                    self.re_add_local_slot(*slot, value_ty, span);
629                }
630                self.emit(
631                    CfgInstData::Store {
632                        slot: *slot,
633                        value: val,
634                    },
635                    Type::UNIT,
636                    span,
637                );
638                ExprResult {
639                    value: None,
640                    continuation: Continuation::Continues,
641                }
642            }
643
644            AirInstData::ParamStore { param_slot, value } => {
645                let Some(val) = self.lower_value(*value) else {
646                    return Self::diverged();
647                };
648                self.emit(
649                    CfgInstData::ParamStore {
650                        param_slot: *param_slot,
651                        value: val,
652                    },
653                    Type::UNIT,
654                    span,
655                );
656                ExprResult {
657                    value: None,
658                    continuation: Continuation::Continues,
659                }
660            }
661
662            AirInstData::RefStore { slot, value } => {
663                let Some(val) = self.lower_value(*value) else {
664                    return Self::diverged();
665                };
666                self.emit(
667                    CfgInstData::RefStore {
668                        slot: *slot,
669                        value: val,
670                    },
671                    Type::UNIT,
672                    span,
673                );
674                ExprResult {
675                    value: None,
676                    continuation: Continuation::Continues,
677                }
678            }
679
680            AirInstData::Call {
681                name,
682                args_start,
683                args_len,
684            } => {
685                // Collect AIR call args for later use in forget logic
686                let air_call_args: Vec<_> =
687                    self.air.get_call_args(*args_start, *args_len).collect();
688
689                let mut arg_vals = Vec::new();
690                for arg in &air_call_args {
691                    let Some(value) = self.lower_value(arg.value) else {
692                        return Self::diverged();
693                    };
694                    arg_vals.push(CfgCallArg {
695                        value,
696                        mode: CfgArgMode::from(arg.mode),
697                    });
698                }
699
700                // Forget consumed values: normal (non-inout/borrow) args transfer ownership
701                // to the callee. Remove them from scope/param tracking to prevent double-drop.
702                for arg in &air_call_args {
703                    if arg.mode == AirArgMode::Normal {
704                        self.forget_consumed_value(arg.value);
705                    }
706                }
707
708                // Store args in extra array
709                let (args_start, args_len) = self.cfg.push_call_args(arg_vals);
710                let value = self.emit(
711                    CfgInstData::Call {
712                        name: *name,
713                        args_start,
714                        args_len,
715                    },
716                    ty,
717                    span,
718                );
719                self.cache(air_ref, value);
720                ExprResult {
721                    value: Some(value),
722                    continuation: Continuation::Continues,
723                }
724            }
725
726            AirInstData::Intrinsic {
727                name,
728                args_start,
729                args_len,
730            } => {
731                let mut arg_vals = Vec::new();
732                let arg_refs: Vec<AirRef> = self.air.get_air_refs(*args_start, *args_len).collect();
733                for arg in &arg_refs {
734                    let Some(val) = self.lower_value(*arg) else {
735                        return Self::diverged();
736                    };
737                    arg_vals.push(val);
738                }
739                // ADR-0067: `vec_dispose(self)` consumes its receiver
740                // by-value. Forget the slot so the scope-exit elaboration
741                // doesn't emit a duplicate drop.
742                let intrinsic_name = self.interner.resolve(name);
743                if intrinsic_name == "vec_dispose"
744                    && let Some(first) = arg_refs.first()
745                {
746                    self.forget_consumed_value(*first);
747                }
748                // Store args in extra array
749                let (args_start, args_len) = self.cfg.push_extra(arg_vals);
750                let value = self.emit(
751                    CfgInstData::Intrinsic {
752                        name: *name,
753                        args_start,
754                        args_len,
755                    },
756                    ty,
757                    span,
758                );
759                self.cache(air_ref, value);
760                // ADR-0084: copy the per-`@spawn` bookkeeping from
761                // AIR into CFG so codegen can reach it without a
762                // back-reference to AIR.
763                if intrinsic_name == "spawn"
764                    && let Some(target) = self.air.spawn_target(air_ref)
765                {
766                    self.cfg.record_spawn_target(
767                        value,
768                        crate::inst::SpawnTarget {
769                            worker_fn: target.worker_fn,
770                            arg_type: target.arg_type,
771                            return_type: target.return_type,
772                        },
773                    );
774                }
775                // Diverging intrinsics (e.g. `@panic`, `@compile_error`)
776                // have `Never` type and never return. Mark the current
777                // block unreachable so the Branch / Block lowerings don't
778                // try to wire up a follow-up goto, and so codegen can
779                // emit an `unreachable` terminator.
780                if ty.is_never() {
781                    self.cfg
782                        .set_terminator(self.current_block, Terminator::Unreachable);
783                    return Self::diverged();
784                }
785                ExprResult {
786                    value: Some(value),
787                    continuation: Continuation::Continues,
788                }
789            }
790
791            AirInstData::StructInit {
792                struct_id,
793                fields_start,
794                fields_len,
795                source_order_start,
796            } => {
797                // Evaluate field initializers in SOURCE ORDER (spec 4.0:8)
798                // The source_order tells us which declaration-order index to evaluate at each step
799                let (fields, source_order) =
800                    self.air
801                        .get_struct_init(*fields_start, *fields_len, *source_order_start);
802                let fields: Vec<AirRef> = fields.collect();
803                let source_order: Vec<usize> = source_order.collect();
804
805                let mut lowered_fields: Vec<Option<CfgValue>> = vec![None; fields.len()];
806                for decl_idx in source_order {
807                    let Some(lowered) = self.lower_value(fields[decl_idx]) else {
808                        return Self::diverged();
809                    };
810                    lowered_fields[decl_idx] = Some(lowered);
811                }
812
813                // Forget consumed values to prevent double-drop at scope exit.
814                //
815                // When a non-Copy value (local or param) is moved into a struct field,
816                // the containing struct's drop glue handles freeing it.
817                // We must not also drop the original at scope exit, or we'd get a
818                // double-free.
819                for &field_air_ref in &fields {
820                    self.forget_consumed_value(field_air_ref);
821                }
822
823                // Collect in declaration order for storage layout
824                let field_vals: Vec<CfgValue> = lowered_fields
825                    .into_iter()
826                    .map(|opt: Option<CfgValue>| opt.expect("all fields should be lowered"))
827                    .collect();
828
829                // Store fields in extra array
830                let (fields_start, fields_len) = self.cfg.push_extra(field_vals);
831                let value = self.emit(
832                    CfgInstData::StructInit {
833                        struct_id: *struct_id,
834                        fields_start,
835                        fields_len,
836                    },
837                    ty,
838                    span,
839                );
840                self.cache(air_ref, value);
841                ExprResult {
842                    value: Some(value),
843                    continuation: Continuation::Continues,
844                }
845            }
846
847            AirInstData::FieldGet {
848                base,
849                struct_id,
850                field_index,
851            } => {
852                // ADR-0030 Phase 3: Try to use PlaceRead for field access
853                if let Some(value) = self.lower_place_read(air_ref, ty, span) {
854                    self.cache(air_ref, value);
855                    return ExprResult {
856                        value: Some(value),
857                        continuation: Continuation::Continues,
858                    };
859                }
860
861                // ADR-0030 Phase 6: Spill computed struct to temp, then use PlaceRead
862                // This handles cases like `get_struct().field` or `method().field`
863                // where the base is a computed value, not a local variable.
864                let Some(base_val) = self.lower_value(*base) else {
865                    return Self::diverged();
866                };
867
868                // Allocate a temporary slot for the struct
869                let temp_slot = self.cfg.alloc_temp_local();
870
871                // Emit StorageLive, Alloc to store the computed struct
872                self.emit(
873                    CfgInstData::StorageLive { slot: temp_slot },
874                    Type::UNIT,
875                    span,
876                );
877                self.emit(
878                    CfgInstData::Alloc {
879                        slot: temp_slot,
880                        init: base_val,
881                    },
882                    Type::UNIT,
883                    span,
884                );
885
886                // Create a PlaceRead from the temp slot with Field projection
887                let place = self.cfg.make_place(
888                    PlaceBase::Local(temp_slot),
889                    std::iter::once(Projection::Field {
890                        struct_id: *struct_id,
891                        field_index: *field_index,
892                    }),
893                );
894                let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
895
896                // Emit StorageDead for the temp
897                self.emit(
898                    CfgInstData::StorageDead { slot: temp_slot },
899                    Type::UNIT,
900                    span,
901                );
902
903                self.cache(air_ref, value);
904                ExprResult {
905                    value: Some(value),
906                    continuation: Continuation::Continues,
907                }
908            }
909
910            AirInstData::FieldSet {
911                slot,
912                struct_id,
913                field_index,
914                value,
915            } => {
916                let Some(val) = self.lower_value(*value) else {
917                    return Self::diverged();
918                };
919                self.emit(
920                    CfgInstData::FieldSet {
921                        slot: *slot,
922                        struct_id: *struct_id,
923                        field_index: *field_index,
924                        value: val,
925                    },
926                    Type::UNIT,
927                    span,
928                );
929                ExprResult {
930                    value: None,
931                    continuation: Continuation::Continues,
932                }
933            }
934
935            AirInstData::ParamFieldSet {
936                param_slot,
937                inner_offset,
938                struct_id,
939                field_index,
940                value,
941            } => {
942                let Some(val) = self.lower_value(*value) else {
943                    return Self::diverged();
944                };
945                self.emit(
946                    CfgInstData::ParamFieldSet {
947                        param_slot: *param_slot,
948                        inner_offset: *inner_offset,
949                        struct_id: *struct_id,
950                        field_index: *field_index,
951                        value: val,
952                    },
953                    Type::UNIT,
954                    span,
955                );
956                ExprResult {
957                    value: None,
958                    continuation: Continuation::Continues,
959                }
960            }
961
962            AirInstData::Block {
963                stmts_start,
964                stmts_len,
965                value,
966            } => {
967                // Collect statements into a Vec for iteration (needed for checking remaining)
968                let statements: Vec<AirRef> =
969                    self.air.get_air_refs(*stmts_start, *stmts_len).collect();
970
971                // Check if this is a "wrapper block" that only contains StorageLive statements.
972                // These are synthetic blocks created to pair StorageLive with Alloc, and they
973                // should NOT create a new scope for drop elaboration.
974                let is_storage_live_wrapper = statements.iter().all(|stmt| {
975                    matches!(self.air.get(*stmt).data, AirInstData::StorageLive { .. })
976                });
977
978                // Only push a scope if this is a real syntactic block (not a StorageLive wrapper)
979                if !is_storage_live_wrapper {
980                    self.scope_stack.push(Vec::new());
981                }
982
983                // Lower each statement.
984                //
985                // Design decision: When a statement diverges (break/continue/return), we only
986                // warn about the *first* unreachable statement or value expression following it.
987                // This matches Rust's behavior and avoids flooding the user with redundant
988                // warnings for code like:
989                //   break;
990                //   x = 1;  // warn about this
991                //   y = 2;  // don't warn about this (already covered by first warning)
992                for (i, stmt) in statements.iter().enumerate() {
993                    let result = self.lower_inst(*stmt);
994                    if matches!(result.continuation, Continuation::Diverged) {
995                        // Get the span of the diverging statement for the secondary label
996                        let diverging_span = self.air.get(*stmt).span;
997
998                        // Check if there are remaining statements or a value expression
999                        // that will never be executed
1000                        let remaining = &statements[i + 1..];
1001                        if !remaining.is_empty() {
1002                            // Warn about the first unreachable statement
1003                            let unreachable_stmt = remaining[0];
1004                            let unreachable_span = self.air.get(unreachable_stmt).span;
1005                            self.warnings.push(
1006                                CompileWarning::new(WarningKind::UnreachableCode, unreachable_span)
1007                                    .with_label(
1008                                        "any code following this expression is unreachable",
1009                                        diverging_span,
1010                                    )
1011                                    .with_note(
1012                                        "this warning occurs because the preceding expression \
1013                                         diverges (e.g., returns, breaks, or continues)",
1014                                    ),
1015                            );
1016                        } else {
1017                            // The final value expression is unreachable.
1018                            // However, don't warn about synthetic unit values (created by parser
1019                            // when a block has no trailing expression). These have zero-length
1020                            // spans pointing at the closing brace.
1021                            let value_span = self.air.get(*value).span;
1022                            let is_synthetic = value_span.start == value_span.end;
1023                            if !is_synthetic {
1024                                self.warnings.push(
1025                                    CompileWarning::new(WarningKind::UnreachableCode, value_span)
1026                                        .with_label(
1027                                            "any code following this expression is unreachable",
1028                                            diverging_span,
1029                                        )
1030                                        .with_note(
1031                                            "this warning occurs because the preceding expression \
1032                                             diverges (e.g., returns, breaks, or continues)",
1033                                        ),
1034                                );
1035                            }
1036                        }
1037                        // Note: drops were already emitted by the diverging statement
1038                        // (break/continue/return handle their own drops)
1039                        return ExprResult {
1040                            value: None,
1041                            continuation: Continuation::Diverged,
1042                        };
1043                    }
1044                }
1045
1046                // Lower the final value
1047                let result = self.lower_inst(*value);
1048
1049                // When the block's final value is a `Load` of an in-scope
1050                // local (i.e., the value flowed out via the implicit
1051                // result), mark the local as consumed so the pop-scope
1052                // drop dispatch below skips it. Without this hand-off,
1053                // the pop emits a drop on the local's slot — freeing
1054                // the heap that the just-loaded value points at —
1055                // and any function whose body ends in a bare `v`
1056                // (Vec / String / any heap-owning type) returns a
1057                // dangling pointer to the caller.
1058                if !is_storage_live_wrapper {
1059                    self.forget_consumed_value(*value);
1060                }
1061
1062                // Pop scope and emit StorageDead (with Drop if needed) in reverse order.
1063                // BUT: if the value diverged (break/continue/return), the diverging
1064                // instruction already emitted drops for all scopes via emit_drops_for_all_scopes,
1065                // so we must NOT emit duplicate StorageDead here.
1066                if !is_storage_live_wrapper && let Some(scope_slots) = self.scope_stack.pop() {
1067                    // Only emit scope cleanup if the value didn't diverge
1068                    if !matches!(result.continuation, Continuation::Diverged) {
1069                        for live_slot in scope_slots.into_iter().rev() {
1070                            // Emit Drop for types that need cleanup (e.g., heap-allocated String)
1071                            if self.type_needs_drop(live_slot.ty) {
1072                                let slot_val = self.emit(
1073                                    CfgInstData::Load {
1074                                        slot: live_slot.slot,
1075                                    },
1076                                    live_slot.ty,
1077                                    live_slot.span,
1078                                );
1079                                self.emit(
1080                                    CfgInstData::Drop { value: slot_val },
1081                                    Type::UNIT,
1082                                    live_slot.span,
1083                                );
1084                            }
1085                            self.emit(
1086                                CfgInstData::StorageDead {
1087                                    slot: live_slot.slot,
1088                                },
1089                                Type::UNIT,
1090                                live_slot.span,
1091                            );
1092                        }
1093                    }
1094                }
1095
1096                result
1097            }
1098
1099            AirInstData::Branch {
1100                cond,
1101                then_value,
1102                else_value,
1103            } => {
1104                let Some(cond_val) = self.lower_value(*cond) else {
1105                    return Self::diverged();
1106                };
1107
1108                let then_block = self.cfg.new_block();
1109                let else_block = self.cfg.new_block();
1110                let join_block = self.cfg.new_block();
1111
1112                // Get types for then/else
1113                let then_type = self.air.get(*then_value).ty;
1114                let else_type = else_value.map(|e| self.air.get(e).ty);
1115
1116                // Branch to then/else
1117                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
1118                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
1119                self.cfg.set_terminator(
1120                    self.current_block,
1121                    Terminator::Branch {
1122                        cond: cond_val,
1123                        then_block,
1124                        then_args_start,
1125                        then_args_len,
1126                        else_block,
1127                        else_args_start,
1128                        else_args_len,
1129                    },
1130                );
1131
1132                // Lower then branch
1133                self.current_block = then_block;
1134                let then_result = self.lower_inst(*then_value);
1135                let then_exit_block = self.current_block;
1136                let then_diverged = matches!(then_result.continuation, Continuation::Diverged);
1137
1138                // Lower else branch
1139                self.current_block = else_block;
1140                let else_result = if let Some(else_val) = else_value {
1141                    self.lower_inst(*else_val)
1142                } else {
1143                    // No else - emit unit
1144                    let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1145                    ExprResult {
1146                        value: Some(unit_val),
1147                        continuation: Continuation::Continues,
1148                    }
1149                };
1150                let else_exit_block = self.current_block;
1151                let else_diverged = matches!(else_result.continuation, Continuation::Diverged);
1152
1153                // If both branches diverge, mark join block as unreachable and diverge
1154                if then_diverged && else_diverged {
1155                    self.cfg.set_terminator(join_block, Terminator::Unreachable);
1156                    return ExprResult {
1157                        value: None,
1158                        continuation: Continuation::Diverged,
1159                    };
1160                }
1161
1162                // Determine result type
1163                let result_type = if then_type.is_never() {
1164                    else_type.unwrap_or(Type::UNIT)
1165                } else {
1166                    then_type
1167                };
1168
1169                // Add block parameter for result (if we have a value type)
1170                let result_param = if result_type != Type::UNIT && result_type != Type::NEVER {
1171                    Some(self.cfg.add_block_param(join_block, result_type))
1172                } else {
1173                    None
1174                };
1175
1176                // Wire up non-divergent branches to join
1177                if !then_diverged {
1178                    let args: Vec<CfgValue> = if let Some(val) = then_result.value {
1179                        if result_param.is_some() {
1180                            vec![val]
1181                        } else {
1182                            vec![]
1183                        }
1184                    } else {
1185                        vec![]
1186                    };
1187                    let (args_start, args_len) = self.cfg.push_extra(args);
1188                    self.cfg.set_terminator(
1189                        then_exit_block,
1190                        Terminator::Goto {
1191                            target: join_block,
1192                            args_start,
1193                            args_len,
1194                        },
1195                    );
1196                }
1197
1198                if !else_diverged {
1199                    let args: Vec<CfgValue> = if let Some(val) = else_result.value {
1200                        if result_param.is_some() {
1201                            vec![val]
1202                        } else {
1203                            vec![]
1204                        }
1205                    } else {
1206                        vec![]
1207                    };
1208                    let (args_start, args_len) = self.cfg.push_extra(args);
1209                    self.cfg.set_terminator(
1210                        else_exit_block,
1211                        Terminator::Goto {
1212                            target: join_block,
1213                            args_start,
1214                            args_len,
1215                        },
1216                    );
1217                }
1218
1219                self.current_block = join_block;
1220
1221                if let Some(param) = result_param {
1222                    self.cache(air_ref, param);
1223                }
1224
1225                ExprResult {
1226                    value: result_param,
1227                    continuation: Continuation::Continues,
1228                }
1229            }
1230
1231            AirInstData::Loop { cond, body } => {
1232                let header_block = self.cfg.new_block();
1233                let body_block = self.cfg.new_block();
1234                let exit_block = self.cfg.new_block();
1235
1236                // Jump to header
1237                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1238                self.cfg.set_terminator(
1239                    self.current_block,
1240                    Terminator::Goto {
1241                        target: header_block,
1242                        args_start,
1243                        args_len,
1244                    },
1245                );
1246
1247                // Push loop context with current scope depth.
1248                // The scope depth is captured BEFORE the loop body is lowered,
1249                // so break/continue will drop all slots in scopes created INSIDE the loop.
1250                self.loop_stack.push(LoopContext {
1251                    header: header_block,
1252                    exit: exit_block,
1253                    scope_depth: self.scope_stack.len(),
1254                });
1255
1256                // Lower condition in header
1257                self.current_block = header_block;
1258                let Some(cond_val) = self.lower_value(*cond) else {
1259                    return Self::diverged();
1260                };
1261
1262                // Branch: if true go to body, if false exit
1263                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
1264                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
1265                self.cfg.set_terminator(
1266                    self.current_block,
1267                    Terminator::Branch {
1268                        cond: cond_val,
1269                        then_block: body_block,
1270                        then_args_start,
1271                        then_args_len,
1272                        else_block: exit_block,
1273                        else_args_start,
1274                        else_args_len,
1275                    },
1276                );
1277
1278                // Lower body
1279                self.current_block = body_block;
1280                let body_result = self.lower_inst(*body);
1281
1282                // After body, go back to header (unless diverged)
1283                if !matches!(body_result.continuation, Continuation::Diverged) {
1284                    let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1285                    self.cfg.set_terminator(
1286                        self.current_block,
1287                        Terminator::Goto {
1288                            target: header_block,
1289                            args_start,
1290                            args_len,
1291                        },
1292                    );
1293                }
1294
1295                self.loop_stack.pop();
1296
1297                // Continue after loop
1298                self.current_block = exit_block;
1299
1300                // Loops produce a unit value (for use in unit-returning functions)
1301                let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1302                ExprResult {
1303                    value: Some(unit_val),
1304                    continuation: Continuation::Continues,
1305                }
1306            }
1307
1308            AirInstData::InfiniteLoop { body } => {
1309                // Infinite loop: loop { body }
1310                //
1311                // Structure (2 blocks, not 3):
1312                //   body_block: execute body, then goto body_block
1313                //   exit_block: only reachable via break
1314                //
1315                // Unlike while loops, there's no condition check, so we don't need
1316                // a separate header block. The body_block serves as both the loop
1317                // entry point and the continue target.
1318                let body_block = self.cfg.new_block();
1319                let exit_block = self.cfg.new_block();
1320
1321                // Jump to body
1322                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1323                self.cfg.set_terminator(
1324                    self.current_block,
1325                    Terminator::Goto {
1326                        target: body_block,
1327                        args_start,
1328                        args_len,
1329                    },
1330                );
1331
1332                // Push loop context (body_block is the continue target).
1333                // The scope depth is captured BEFORE the loop body is lowered,
1334                // so break/continue will drop all slots in scopes created INSIDE the loop.
1335                self.loop_stack.push(LoopContext {
1336                    header: body_block,
1337                    exit: exit_block,
1338                    scope_depth: self.scope_stack.len(),
1339                });
1340
1341                // Lower body
1342                self.current_block = body_block;
1343                let body_result = self.lower_inst(*body);
1344
1345                // After body, go back to start (unless diverged via return/break/continue)
1346                if !matches!(body_result.continuation, Continuation::Diverged) {
1347                    let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1348                    self.cfg.set_terminator(
1349                        self.current_block,
1350                        Terminator::Goto {
1351                            target: body_block,
1352                            args_start,
1353                            args_len,
1354                        },
1355                    );
1356                }
1357
1358                self.loop_stack.pop();
1359
1360                // Continue after loop (only reachable via break).
1361                // Set Unreachable as the initial terminator. If there's code after the loop
1362                // (which requires a break to be reachable), the subsequent Ret instruction
1363                // will overwrite this with the correct Return terminator. If there's no break,
1364                // the block is truly unreachable and Unreachable is correct.
1365                self.current_block = exit_block;
1366                self.cfg
1367                    .set_terminator(self.current_block, Terminator::Unreachable);
1368
1369                // Infinite loops have Never type, but if we reach exit_block via break,
1370                // we need a dummy unit value for the loop expression result.
1371                let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1372                ExprResult {
1373                    value: Some(unit_val),
1374                    continuation: Continuation::Continues,
1375                }
1376            }
1377
1378            AirInstData::Match {
1379                scrutinee,
1380                arms_start,
1381                arms_len,
1382            } => {
1383                // Lower the scrutinee
1384                let Some(scrutinee_val) = self.lower_value(*scrutinee) else {
1385                    return Self::diverged();
1386                };
1387                let scrutinee_ty = self.air.get(*scrutinee).ty;
1388
1389                // ADR-0052 Phase 5: a zero-arm match on an uninhabited
1390                // scrutinee is vacuously exhaustive — sema has already
1391                // verified the type is `Never`. Emit an `unreachable`
1392                // terminator and a diverging result so downstream
1393                // control-flow analysis knows this point can't be
1394                // reached.
1395                if *arms_len == 0 {
1396                    self.cfg
1397                        .set_terminator(self.current_block, Terminator::Unreachable);
1398                    return Self::diverged();
1399                }
1400
1401                // Collect arms into a Vec for iteration
1402                let arms: Vec<(AirPattern, AirRef)> =
1403                    self.air.get_match_arms(*arms_start, *arms_len).collect();
1404
1405                // Create blocks for each arm and a join block
1406                let arm_blocks: Vec<_> = arms.iter().map(|_| self.cfg.new_block()).collect();
1407                let join_block = self.cfg.new_block();
1408
1409                // Get result type (from first non-Never arm)
1410                let result_type = arms
1411                    .iter()
1412                    .map(|(_, body)| self.air.get(*body).ty)
1413                    .find(|ty| !ty.is_never())
1414                    .unwrap_or(Type::NEVER);
1415
1416                // ADR-0051 Phase 4b part 2: if any arm uses a recursive
1417                // pattern shape that can't be dispatched by a single flat
1418                // switch, emit cascading projection + conditional branches
1419                // to select the arm block. Otherwise fall through to the
1420                // existing flat-switch terminator setup below. Arm body
1421                // lowering and join wiring is shared between both paths.
1422                let needs_cascading = arms.iter().any(|(p, _)| match p {
1423                    AirPattern::Tuple { .. }
1424                    | AirPattern::Struct { .. }
1425                    | AirPattern::Bind { .. } => true,
1426                    // ADR-0052: enum-variant arms with non-trivial field
1427                    // patterns (refutable nested shapes like `Some(0)` or
1428                    // `Some(Some(v))`) need the cascading descent; the
1429                    // flat switch only dispatches on the discriminant.
1430                    AirPattern::EnumDataVariant { fields, .. } => {
1431                        fields.iter().any(|f| !is_trivial_pattern(f))
1432                    }
1433                    AirPattern::EnumStructVariant { fields, .. } => {
1434                        fields.iter().any(|(_, f)| !is_trivial_pattern(f))
1435                    }
1436                    _ => false,
1437                });
1438                if needs_cascading {
1439                    // Spill scrutinee to a temporary local so cascading
1440                    // sub-tests can re-read field projections multiple times.
1441                    let scr_slot = self.cfg.alloc_temp_local();
1442                    self.emit(
1443                        CfgInstData::StorageLive { slot: scr_slot },
1444                        Type::UNIT,
1445                        span,
1446                    );
1447                    self.emit(
1448                        CfgInstData::Alloc {
1449                            slot: scr_slot,
1450                            init: scrutinee_val,
1451                        },
1452                        Type::UNIT,
1453                        span,
1454                    );
1455
1456                    // Pre-create test blocks and an unreachable fallback for
1457                    // the no-match case. Sema has already checked
1458                    // exhaustiveness, so reaching the fallback is a bug.
1459                    let test_blocks: Vec<_> =
1460                        (0..arms.len()).map(|_| self.cfg.new_block()).collect();
1461                    let fallback = self.cfg.new_block();
1462                    self.cfg.set_terminator(fallback, Terminator::Unreachable);
1463
1464                    // Goto current_block → test_blocks[0].
1465                    let (a_s, a_l) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
1466                    self.cfg.set_terminator(
1467                        self.current_block,
1468                        Terminator::Goto {
1469                            target: test_blocks[0],
1470                            args_start: a_s,
1471                            args_len: a_l,
1472                        },
1473                    );
1474
1475                    for (i, (pattern, _)) in arms.iter().enumerate() {
1476                        let fallthrough = test_blocks.get(i + 1).copied().unwrap_or(fallback);
1477                        self.current_block = test_blocks[i];
1478                        let scrut = Scrutinee {
1479                            slot: scr_slot,
1480                            ty: scrutinee_ty,
1481                            projection: &[],
1482                        };
1483                        let targets = BranchTargets {
1484                            matched: arm_blocks[i],
1485                            unmatched: fallthrough,
1486                        };
1487                        self.emit_pattern_test(scrut, pattern, targets, span);
1488                    }
1489
1490                    // Arm body lowering + join wiring happens at the shared
1491                    // site below. Fake a default value for `default_block`
1492                    // and skip the switch terminator so the rest of the
1493                    // match handler runs as-is.
1494                    return self.lower_match_arm_bodies_and_join(
1495                        &arms,
1496                        &arm_blocks,
1497                        join_block,
1498                        result_type,
1499                        air_ref,
1500                        span,
1501                    );
1502                }
1503
1504                // Create the switch terminator
1505                // Build cases: for each arm, check pattern and jump to corresponding block
1506                let mut switch_cases = Vec::new();
1507                let mut default_block = None;
1508
1509                for (i, (pattern, _)) in arms.iter().enumerate() {
1510                    match pattern {
1511                        AirPattern::Wildcard => {
1512                            default_block = Some(arm_blocks[i]);
1513                            // Wildcard matches everything - any patterns after this are unreachable
1514                            break;
1515                        }
1516                        AirPattern::Int(n) => {
1517                            switch_cases.push((*n, arm_blocks[i]));
1518                        }
1519                        AirPattern::Bool(b) => {
1520                            // Booleans are 0 or 1
1521                            let val = if *b { 1 } else { 0 };
1522                            switch_cases.push((val, arm_blocks[i]));
1523                        }
1524                        AirPattern::EnumVariant { variant_index, .. }
1525                        | AirPattern::EnumUnitVariant { variant_index, .. }
1526                        | AirPattern::EnumDataVariant { variant_index, .. }
1527                        | AirPattern::EnumStructVariant { variant_index, .. } => {
1528                            // Enum variants are matched by their discriminant (variant index).
1529                            // Data- and struct-variant field projections are emitted by sema
1530                            // into the arm body itself (storage_live + field extraction
1531                            // block), so CFG only needs the discriminant dispatch here.
1532                            // Deep nested patterns inside variant fields can't reach us
1533                            // yet — RIR today is flat, so `lower_pattern` produces
1534                            // Bind/Wildcard leaves that CFG doesn't need to inspect.
1535                            // Phase 4 will revisit this when RIR grows nested shapes.
1536                            switch_cases.push((*variant_index as i64, arm_blocks[i]));
1537                        }
1538                        AirPattern::Bind { .. }
1539                        | AirPattern::Tuple { .. }
1540                        | AirPattern::Struct { .. } => {
1541                            // Top-level Bind / Tuple / Struct arms require cascading
1542                            // projection + dispatch (ADR-0051 §3). Phase 2's sema only
1543                            // emits these shapes as leaves inside enum variants, not
1544                            // at the arm root, so nothing produces them today. Phase 4
1545                            // extends RIR and enables the recursive descent here.
1546                            unreachable!(
1547                                "top-level Bind/Tuple/Struct match arms arrive with \
1548                                 ADR-0051 Phase 4; sema does not emit them in Phase 2/3"
1549                            );
1550                        }
1551                    }
1552                }
1553
1554                // If no explicit wildcard, use the last arm as default
1555                // This handles exhaustive matches like `true => ..., false => ...`
1556                // where semantics verified exhaustiveness but we need a default for codegen
1557                let default = default_block.unwrap_or_else(|| {
1558                    // Pop the last case to use as default
1559                    let (_, last_block) = switch_cases
1560                        .pop()
1561                        .expect("match must have at least one arm");
1562                    last_block
1563                });
1564
1565                // Set the switch terminator on current block
1566                let (cases_start, cases_len) = self.cfg.push_switch_cases(switch_cases);
1567                self.cfg.set_terminator(
1568                    self.current_block,
1569                    Terminator::Switch {
1570                        scrutinee: scrutinee_val,
1571                        cases_start,
1572                        cases_len,
1573                        default,
1574                    },
1575                );
1576
1577                // Lower each arm and wire to join block
1578                let mut all_diverged = true;
1579                let mut arm_results = Vec::new();
1580
1581                for (i, (_, body)) in arms.iter().enumerate() {
1582                    self.current_block = arm_blocks[i];
1583                    let body_result = self.lower_inst(*body);
1584                    let exit_block = self.current_block;
1585                    let diverged = matches!(body_result.continuation, Continuation::Diverged);
1586
1587                    if !diverged {
1588                        all_diverged = false;
1589                    }
1590
1591                    arm_results.push((exit_block, body_result, diverged));
1592                }
1593
1594                // If all arms diverge, mark join block unreachable
1595                if all_diverged {
1596                    self.cfg.set_terminator(join_block, Terminator::Unreachable);
1597                    return ExprResult {
1598                        value: None,
1599                        continuation: Continuation::Diverged,
1600                    };
1601                }
1602
1603                // Add block parameter for result (if we have a value type)
1604                let result_param = if result_type != Type::UNIT && result_type != Type::NEVER {
1605                    Some(self.cfg.add_block_param(join_block, result_type))
1606                } else {
1607                    None
1608                };
1609
1610                // Wire up non-divergent arms to join
1611                for (exit_block, body_result, diverged) in arm_results {
1612                    if !diverged {
1613                        let args: Vec<CfgValue> = if let Some(val) = body_result.value {
1614                            if result_param.is_some() {
1615                                vec![val]
1616                            } else {
1617                                vec![]
1618                            }
1619                        } else {
1620                            vec![]
1621                        };
1622                        let (args_start, args_len) = self.cfg.push_extra(args);
1623                        self.cfg.set_terminator(
1624                            exit_block,
1625                            Terminator::Goto {
1626                                target: join_block,
1627                                args_start,
1628                                args_len,
1629                            },
1630                        );
1631                    }
1632                }
1633
1634                self.current_block = join_block;
1635
1636                if let Some(param) = result_param {
1637                    self.cache(air_ref, param);
1638                }
1639
1640                ExprResult {
1641                    value: result_param,
1642                    continuation: Continuation::Continues,
1643                }
1644            }
1645
1646            AirInstData::Break => {
1647                // Emit drops for slots in scopes created inside the loop
1648                let loop_ctx = self.loop_stack.last().expect("break outside loop");
1649                let target_depth = loop_ctx.scope_depth;
1650                let exit_block = loop_ctx.exit;
1651                self.emit_drops_for_loop_exit(target_depth, span);
1652
1653                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1654                self.cfg.set_terminator(
1655                    self.current_block,
1656                    Terminator::Goto {
1657                        target: exit_block,
1658                        args_start,
1659                        args_len,
1660                    },
1661                );
1662
1663                ExprResult {
1664                    value: None,
1665                    continuation: Continuation::Diverged,
1666                }
1667            }
1668
1669            AirInstData::Continue => {
1670                // Emit drops for slots in scopes created inside the loop
1671                let loop_ctx = self.loop_stack.last().expect("continue outside loop");
1672                let target_depth = loop_ctx.scope_depth;
1673                let header_block = loop_ctx.header;
1674                self.emit_drops_for_loop_exit(target_depth, span);
1675
1676                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1677                self.cfg.set_terminator(
1678                    self.current_block,
1679                    Terminator::Goto {
1680                        target: header_block,
1681                        args_start,
1682                        args_len,
1683                    },
1684                );
1685
1686                ExprResult {
1687                    value: None,
1688                    continuation: Continuation::Diverged,
1689                }
1690            }
1691
1692            AirInstData::Ret(value) => {
1693                let val = match value {
1694                    Some(v) => {
1695                        let result = self.lower_inst(*v);
1696                        if matches!(result.continuation, Continuation::Diverged) {
1697                            // The return value expression itself diverged (e.g., a block
1698                            // containing an earlier return). The terminator was already set
1699                            // by the inner diverging expression, so just propagate divergence.
1700                            return Self::diverged();
1701                        }
1702
1703                        // The returned value transfers ownership to the caller.
1704                        // Remove it from scope/param tracking to prevent dropping it.
1705                        self.forget_consumed_value(*v);
1706
1707                        // result.value may be None for Unit-typed expressions - that's OK
1708                        result.value
1709                    }
1710                    None => None,
1711                };
1712
1713                // Emit drops for all live slots before returning
1714                self.emit_drops_for_all_scopes(span);
1715
1716                self.cfg
1717                    .set_terminator(self.current_block, Terminator::Return { value: val });
1718
1719                ExprResult {
1720                    value: None,
1721                    continuation: Continuation::Diverged,
1722                }
1723            }
1724
1725            AirInstData::ArrayInit {
1726                elems_start,
1727                elems_len,
1728            } => {
1729                let elems: Vec<AirRef> = self.air.get_air_refs(*elems_start, *elems_len).collect();
1730                let mut element_vals = Vec::new();
1731                for &elem in &elems {
1732                    let Some(val) = self.lower_value(elem) else {
1733                        return Self::diverged();
1734                    };
1735                    element_vals.push(val);
1736                }
1737
1738                // Forget consumed values to prevent double-drop
1739                for &elem in &elems {
1740                    self.forget_consumed_value(elem);
1741                }
1742
1743                // Store elements in extra array
1744                let (elements_start, elements_len) = self.cfg.push_extra(element_vals);
1745                let value = self.emit(
1746                    CfgInstData::ArrayInit {
1747                        elements_start,
1748                        elements_len,
1749                    },
1750                    ty,
1751                    span,
1752                );
1753                self.cache(air_ref, value);
1754                ExprResult {
1755                    value: Some(value),
1756                    continuation: Continuation::Continues,
1757                }
1758            }
1759
1760            AirInstData::IndexGet {
1761                base,
1762                array_type,
1763                index,
1764            } => {
1765                // ADR-0030 Phase 3: Try to use PlaceRead for array indexing
1766                if let Some(value) = self.lower_place_read(air_ref, ty, span) {
1767                    self.cache(air_ref, value);
1768                    return ExprResult {
1769                        value: Some(value),
1770                        continuation: Continuation::Continues,
1771                    };
1772                }
1773
1774                // ADR-0030 Phase 6: Spill computed array to temp, then use PlaceRead
1775                // This handles cases like `get_array()[i]` where the base is a computed
1776                // value, not a local variable.
1777                // Note: Currently Gruel can't return arrays (see issue gruel-b79f), but this
1778                // handles the case for when that's fixed.
1779                let Some(base_val) = self.lower_value(*base) else {
1780                    return Self::diverged();
1781                };
1782                let Some(index_val) = self.lower_value(*index) else {
1783                    return Self::diverged();
1784                };
1785
1786                // Allocate a temporary slot for the array
1787                let temp_slot = self.cfg.alloc_temp_local();
1788
1789                // Emit StorageLive, Alloc to store the computed array
1790                self.emit(
1791                    CfgInstData::StorageLive { slot: temp_slot },
1792                    Type::UNIT,
1793                    span,
1794                );
1795                self.emit(
1796                    CfgInstData::Alloc {
1797                        slot: temp_slot,
1798                        init: base_val,
1799                    },
1800                    Type::UNIT,
1801                    span,
1802                );
1803
1804                // Create a PlaceRead from the temp slot with Index projection
1805                let place = self.cfg.make_place(
1806                    PlaceBase::Local(temp_slot),
1807                    std::iter::once(Projection::Index {
1808                        array_type: *array_type,
1809                        index: index_val,
1810                    }),
1811                );
1812                let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
1813
1814                // Emit StorageDead for the temp
1815                self.emit(
1816                    CfgInstData::StorageDead { slot: temp_slot },
1817                    Type::UNIT,
1818                    span,
1819                );
1820
1821                self.cache(air_ref, value);
1822                ExprResult {
1823                    value: Some(value),
1824                    continuation: Continuation::Continues,
1825                }
1826            }
1827
1828            AirInstData::IndexSet {
1829                slot,
1830                array_type,
1831                index,
1832                value,
1833            } => {
1834                let Some(index_val) = self.lower_value(*index) else {
1835                    return Self::diverged();
1836                };
1837                let Some(val) = self.lower_value(*value) else {
1838                    return Self::diverged();
1839                };
1840                self.emit(
1841                    CfgInstData::IndexSet {
1842                        slot: *slot,
1843                        array_type: *array_type,
1844                        index: index_val,
1845                        value: val,
1846                    },
1847                    Type::UNIT,
1848                    span,
1849                );
1850                ExprResult {
1851                    value: None,
1852                    continuation: Continuation::Continues,
1853                }
1854            }
1855
1856            AirInstData::ParamIndexSet {
1857                param_slot,
1858                array_type,
1859                index,
1860                value,
1861            } => {
1862                let Some(index_val) = self.lower_value(*index) else {
1863                    return Self::diverged();
1864                };
1865                let Some(val) = self.lower_value(*value) else {
1866                    return Self::diverged();
1867                };
1868                self.emit(
1869                    CfgInstData::ParamIndexSet {
1870                        param_slot: *param_slot,
1871                        array_type: *array_type,
1872                        index: index_val,
1873                        value: val,
1874                    },
1875                    Type::UNIT,
1876                    span,
1877                );
1878                ExprResult {
1879                    value: None,
1880                    continuation: Continuation::Continues,
1881                }
1882            }
1883
1884            // ADR-0030 Phase 8: Handle AIR place-based instructions
1885            AirInstData::PlaceRead { place } => {
1886                // Convert AIR place to CFG place
1887                let Some(cfg_place) = self.lower_air_place(*place) else {
1888                    return Self::diverged();
1889                };
1890                let value = self.emit(CfgInstData::PlaceRead { place: cfg_place }, ty, span);
1891                self.cache(air_ref, value);
1892                ExprResult {
1893                    value: Some(value),
1894                    continuation: Continuation::Continues,
1895                }
1896            }
1897
1898            AirInstData::PlaceWrite { place, value } => {
1899                // Lower the value first (RHS evaluated before drop of old value)
1900                let Some(val) = self.lower_value(*value) else {
1901                    return Self::diverged();
1902                };
1903                let value_ty = self.air.get(*value).ty;
1904                // Convert AIR place to CFG place
1905                let Some(cfg_place) = self.lower_air_place(*place) else {
1906                    return Self::diverged();
1907                };
1908                // Drop the old value at this place if the type has a destructor.
1909                // For PlaceWrite (field/index assignment), the base is always live
1910                // (you cannot write to a field of a moved value), so no liveness check needed.
1911                if self.type_needs_drop(value_ty) {
1912                    let old_val =
1913                        self.emit(CfgInstData::PlaceRead { place: cfg_place }, value_ty, span);
1914                    self.emit(CfgInstData::Drop { value: old_val }, Type::UNIT, span);
1915                }
1916                self.emit(
1917                    CfgInstData::PlaceWrite {
1918                        place: cfg_place,
1919                        value: val,
1920                    },
1921                    Type::UNIT,
1922                    span,
1923                );
1924                ExprResult {
1925                    value: None,
1926                    continuation: Continuation::Continues,
1927                }
1928            }
1929
1930            AirInstData::EnumVariant {
1931                enum_id,
1932                variant_index,
1933            } => {
1934                // Enum variants are just their discriminant value
1935                let value = self.emit(
1936                    CfgInstData::EnumVariant {
1937                        enum_id: *enum_id,
1938                        variant_index: *variant_index,
1939                    },
1940                    ty,
1941                    span,
1942                );
1943                self.cache(air_ref, value);
1944                ExprResult {
1945                    value: Some(value),
1946                    continuation: Continuation::Continues,
1947                }
1948            }
1949
1950            AirInstData::EnumCreate {
1951                enum_id,
1952                variant_index,
1953                fields_start,
1954                fields_len,
1955            } => {
1956                // Lower each field value
1957                let field_air_refs = self
1958                    .air
1959                    .get_air_refs(*fields_start, *fields_len)
1960                    .collect::<Vec<_>>();
1961                let mut field_vals = Vec::with_capacity(field_air_refs.len());
1962                for field_ref in &field_air_refs {
1963                    let Some(val) = self.lower_value(*field_ref) else {
1964                        return Self::diverged();
1965                    };
1966                    field_vals.push(val);
1967                }
1968
1969                // Forget consumed values: each payload field transfers
1970                // ownership into the enum variant. Without this, a non-Copy
1971                // payload (e.g. `Result::Ok(some_String)`) would be dropped
1972                // at scope exit *and* via the enum's drop, double-freeing.
1973                for field_ref in &field_air_refs {
1974                    self.forget_consumed_value(*field_ref);
1975                }
1976
1977                // Store field CfgValues in the extra array
1978                let (fields_start_cfg, fields_len_cfg) = self.cfg.push_extra(field_vals);
1979
1980                let value = self.emit(
1981                    CfgInstData::EnumCreate {
1982                        enum_id: *enum_id,
1983                        variant_index: *variant_index,
1984                        fields_start: fields_start_cfg,
1985                        fields_len: fields_len_cfg,
1986                    },
1987                    ty,
1988                    span,
1989                );
1990                self.cache(air_ref, value);
1991                ExprResult {
1992                    value: Some(value),
1993                    continuation: Continuation::Continues,
1994                }
1995            }
1996
1997            AirInstData::EnumPayloadGet {
1998                base,
1999                variant_index,
2000                field_index,
2001            } => {
2002                let Some(base_val) = self.lower_value(*base) else {
2003                    return Self::diverged();
2004                };
2005                // If the extracted payload owns drop responsibility, the source
2006                // enum's payload has been moved out and the enum should not
2007                // be dropped again (which would re-drop the same payload and
2008                // double-free heap-owned buffers like Vec(u8) or String).
2009                if self.type_needs_drop(ty) {
2010                    self.forget_consumed_value(*base);
2011                }
2012                let value = self.emit(
2013                    CfgInstData::EnumPayloadGet {
2014                        base: base_val,
2015                        variant_index: *variant_index,
2016                        field_index: *field_index,
2017                    },
2018                    ty,
2019                    span,
2020                );
2021                self.cache(air_ref, value);
2022                ExprResult {
2023                    value: Some(value),
2024                    continuation: Continuation::Continues,
2025                }
2026            }
2027
2028            AirInstData::IntCast { value, from_ty } => {
2029                let Some(val) = self.lower_value(*value) else {
2030                    return Self::diverged();
2031                };
2032                let result = self.emit(
2033                    CfgInstData::IntCast {
2034                        value: val,
2035                        from_ty: *from_ty,
2036                    },
2037                    ty,
2038                    span,
2039                );
2040                self.cache(air_ref, result);
2041                ExprResult {
2042                    value: Some(result),
2043                    continuation: Continuation::Continues,
2044                }
2045            }
2046
2047            AirInstData::FloatCast { value, from_ty } => {
2048                let Some(val) = self.lower_value(*value) else {
2049                    return Self::diverged();
2050                };
2051                let result = self.emit(
2052                    CfgInstData::FloatCast {
2053                        value: val,
2054                        from_ty: *from_ty,
2055                    },
2056                    ty,
2057                    span,
2058                );
2059                self.cache(air_ref, result);
2060                ExprResult {
2061                    value: Some(result),
2062                    continuation: Continuation::Continues,
2063                }
2064            }
2065
2066            AirInstData::IntToFloat { value, from_ty } => {
2067                let Some(val) = self.lower_value(*value) else {
2068                    return Self::diverged();
2069                };
2070                let result = self.emit(
2071                    CfgInstData::IntToFloat {
2072                        value: val,
2073                        from_ty: *from_ty,
2074                    },
2075                    ty,
2076                    span,
2077                );
2078                self.cache(air_ref, result);
2079                ExprResult {
2080                    value: Some(result),
2081                    continuation: Continuation::Continues,
2082                }
2083            }
2084
2085            AirInstData::FloatToInt { value, from_ty } => {
2086                let Some(val) = self.lower_value(*value) else {
2087                    return Self::diverged();
2088                };
2089                let result = self.emit(
2090                    CfgInstData::FloatToInt {
2091                        value: val,
2092                        from_ty: *from_ty,
2093                    },
2094                    ty,
2095                    span,
2096                );
2097                self.cache(air_ref, result);
2098                ExprResult {
2099                    value: Some(result),
2100                    continuation: Continuation::Continues,
2101                }
2102            }
2103
2104            AirInstData::Drop { value } => {
2105                // Lower the value to drop
2106                let Some(val) = self.lower_value(*value) else {
2107                    return Self::diverged();
2108                };
2109                let val_ty = self.air.get(*value).ty;
2110
2111                // Only emit a Drop instruction if the type needs drop.
2112                // For trivially droppable types, this is a no-op.
2113                // We use self.type_needs_drop() which has access to struct/array
2114                // definitions to recursively check if fields need drop.
2115                if self.type_needs_drop(val_ty) {
2116                    self.emit(CfgInstData::Drop { value: val }, Type::UNIT, span);
2117                }
2118
2119                // Drop is a statement, produces no value
2120                ExprResult {
2121                    value: None,
2122                    continuation: Continuation::Continues,
2123                }
2124            }
2125
2126            AirInstData::StorageLive { slot } => {
2127                // Emit StorageLive to CFG
2128                self.emit(CfgInstData::StorageLive { slot: *slot }, Type::UNIT, span);
2129
2130                // Record this slot as live in the current scope for drop elaboration.
2131                // Also remember the depth so a later forget+reassignment in a
2132                // nested scope re-attaches the slot to the same scope rather
2133                // than dropping it at the inner-scope exit.
2134                let depth = self.scope_stack.len().saturating_sub(1);
2135                self.slot_origin_depth.insert(*slot, depth);
2136                if let Some(scope) = self.scope_stack.last_mut() {
2137                    scope.push(LiveSlot {
2138                        slot: *slot,
2139                        ty,
2140                        span,
2141                    });
2142                }
2143
2144                ExprResult {
2145                    value: None,
2146                    continuation: Continuation::Continues,
2147                }
2148            }
2149
2150            AirInstData::StorageDead { slot } => {
2151                // StorageDead in AIR is a hint; CFG builder emits these at scope exit
2152                // This case handles explicit StorageDead if any (currently unused)
2153                self.emit(CfgInstData::StorageDead { slot: *slot }, Type::UNIT, span);
2154                ExprResult {
2155                    value: None,
2156                    continuation: Continuation::Continues,
2157                }
2158            }
2159
2160            AirInstData::MethodCallDyn {
2161                interface_id,
2162                slot,
2163                recv,
2164                args_start,
2165                args_len,
2166            } => {
2167                // Lower the receiver and the additional args.
2168                let Some(recv_val) = self.lower_value(*recv) else {
2169                    return Self::diverged();
2170                };
2171
2172                let air_call_args: Vec<_> =
2173                    self.air.get_call_args(*args_start, *args_len).collect();
2174                let mut arg_vals = Vec::new();
2175                for arg in &air_call_args {
2176                    let Some(value) = self.lower_value(arg.value) else {
2177                        return Self::diverged();
2178                    };
2179                    arg_vals.push(CfgCallArg {
2180                        value,
2181                        mode: CfgArgMode::from(arg.mode),
2182                    });
2183                }
2184                for arg in &air_call_args {
2185                    if arg.mode == AirArgMode::Normal {
2186                        self.forget_consumed_value(arg.value);
2187                    }
2188                }
2189
2190                let (cfg_args_start, cfg_args_len) = self.cfg.push_call_args(arg_vals);
2191                let value = self.emit(
2192                    CfgInstData::MethodCallDyn {
2193                        interface_id: *interface_id,
2194                        slot: *slot,
2195                        recv: recv_val,
2196                        args_start: cfg_args_start,
2197                        args_len: cfg_args_len,
2198                    },
2199                    ty,
2200                    span,
2201                );
2202                self.cache(air_ref, value);
2203                ExprResult {
2204                    value: Some(value),
2205                    continuation: Continuation::Continues,
2206                }
2207            }
2208
2209            AirInstData::MakeInterfaceRef {
2210                value,
2211                struct_id,
2212                interface_id,
2213            } => {
2214                // ADR-0056 Phase 4d: lower the AIR coercion to a CFG
2215                // instruction; codegen materializes the fat-pointer struct.
2216                let Some(cfg_value) = self.lower_value(*value) else {
2217                    return Self::diverged();
2218                };
2219                let result = self.emit(
2220                    CfgInstData::MakeInterfaceRef {
2221                        value: cfg_value,
2222                        struct_id: *struct_id,
2223                        interface_id: *interface_id,
2224                    },
2225                    ty,
2226                    span,
2227                );
2228                self.cache(air_ref, result);
2229                ExprResult {
2230                    value: Some(result),
2231                    continuation: Continuation::Continues,
2232                }
2233            }
2234        }
2235    }
2236
2237    /// Emit an instruction in the current block.
2238    fn emit(&mut self, data: CfgInstData, ty: Type, span: gruel_util::Span) -> CfgValue {
2239        self.cfg
2240            .add_inst_to_block(self.current_block, CfgInst { data, ty, span })
2241    }
2242
2243    /// Cache a value for an AIR ref.
2244    fn cache(&mut self, air_ref: AirRef, value: CfgValue) {
2245        self.value_cache[air_ref.as_u32() as usize] = Some(value);
2246    }
2247
2248    /// Lower an instruction and return its value, or None if it diverged.
2249    /// This is a helper for use with the `?` operator when processing operands.
2250    /// If the operand diverged, the caller should propagate the divergence.
2251    fn lower_value(&mut self, air_ref: AirRef) -> Option<CfgValue> {
2252        let result = self.lower_inst(air_ref);
2253        if matches!(result.continuation, Continuation::Diverged) {
2254            None
2255        } else {
2256            result.value
2257        }
2258    }
2259
2260    /// Create a diverged ExprResult. Used when an operand diverges.
2261    fn diverged() -> ExprResult {
2262        ExprResult {
2263            value: None,
2264            continuation: Continuation::Diverged,
2265        }
2266    }
2267
2268    /// ADR-0051 Phase 4b part 2: emit the test for `pattern` against the
2269    /// scrutinee spilled into `scr_slot`, following `projection` to reach
2270    /// the current focused value. Terminates `self.current_block` with a
2271    /// `Goto(matched)` for an unconditional match or a `Branch` for a
2272    /// conditional one. `unmatched` is used for the false side of
2273    /// conditional branches. `scr_ty` is the type of the (projected)
2274    /// value currently in focus.
2275    fn emit_pattern_test(
2276        &mut self,
2277        scrut: Scrutinee<'_>,
2278        pattern: &AirPattern,
2279        targets: BranchTargets,
2280        span: gruel_util::Span,
2281    ) {
2282        let Scrutinee {
2283            slot: scr_slot,
2284            ty: scr_ty,
2285            projection,
2286        } = scrut;
2287        let BranchTargets { matched, unmatched } = targets;
2288        match pattern {
2289            AirPattern::Wildcard => {
2290                // Unconditional match.
2291                let (a_s, a_l) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
2292                self.cfg.set_terminator(
2293                    self.current_block,
2294                    Terminator::Goto {
2295                        target: matched,
2296                        args_start: a_s,
2297                        args_len: a_l,
2298                    },
2299                );
2300            }
2301            AirPattern::Int(n) => {
2302                let val = self.read_projected(scr_slot, scr_ty, projection, span);
2303                let lit = self.emit(CfgInstData::Const(*n as u64), scr_ty, span);
2304                let cond = self.emit(CfgInstData::Bin(BinOp::Eq, val, lit), Type::BOOL, span);
2305                self.branch_to(cond, matched, unmatched, span);
2306            }
2307            AirPattern::Bool(b) => {
2308                let val = self.read_projected(scr_slot, scr_ty, projection, span);
2309                let lit = self.emit(CfgInstData::BoolConst(*b), Type::BOOL, span);
2310                let cond = self.emit(CfgInstData::Bin(BinOp::Eq, val, lit), Type::BOOL, span);
2311                self.branch_to(cond, matched, unmatched, span);
2312            }
2313            AirPattern::EnumVariant { variant_index, .. }
2314            | AirPattern::EnumUnitVariant { variant_index, .. } => {
2315                // Unit-style enum arms dispatch purely on the discriminant.
2316                let disc_ty = self.enum_discriminant_type(scr_ty);
2317                let val = self.read_projected(scr_slot, scr_ty, projection, span);
2318                let disc = self.emit(CfgInstData::GetDiscriminant { base: val }, disc_ty, span);
2319                let lit = self.emit(CfgInstData::Const(*variant_index as u64), disc_ty, span);
2320                let cond = self.emit(CfgInstData::Bin(BinOp::Eq, disc, lit), Type::BOOL, span);
2321                self.branch_to(cond, matched, unmatched, span);
2322            }
2323            AirPattern::EnumDataVariant {
2324                enum_id: _,
2325                variant_index,
2326                fields,
2327            } => {
2328                // ADR-0052: if every field is a trivial leaf (Wildcard / bare
2329                // Bind), sema's `emit_recursive_pattern_bindings` already
2330                // handled binding setup in the arm body — CFG only needs the
2331                // discriminant check. Otherwise, after the discriminant
2332                // matches, recursively dispatch on each non-trivial field by
2333                // projecting the enum payload.
2334                let all_trivial = fields.iter().all(is_trivial_pattern);
2335                let disc_ty = self.enum_discriminant_type(scr_ty);
2336                let val = self.read_projected(scr_slot, scr_ty, projection, span);
2337                let disc = self.emit(CfgInstData::GetDiscriminant { base: val }, disc_ty, span);
2338                let lit = self.emit(CfgInstData::Const(*variant_index as u64), disc_ty, span);
2339                let cond = self.emit(CfgInstData::Bin(BinOp::Eq, disc, lit), Type::BOOL, span);
2340                if all_trivial {
2341                    self.branch_to(cond, matched, unmatched, span);
2342                } else {
2343                    let checked = self.cfg.new_block();
2344                    self.branch_to(cond, checked, unmatched, span);
2345                    self.current_block = checked;
2346                    self.emit_enum_field_tests(
2347                        scrut,
2348                        *variant_index,
2349                        fields.iter().enumerate().map(|(i, p)| (i as u32, p)),
2350                        targets,
2351                        span,
2352                    );
2353                }
2354            }
2355            AirPattern::EnumStructVariant {
2356                enum_id: _,
2357                variant_index,
2358                fields,
2359            } => {
2360                let all_trivial = fields.iter().all(|(_, p)| is_trivial_pattern(p));
2361                let disc_ty = self.enum_discriminant_type(scr_ty);
2362                let val = self.read_projected(scr_slot, scr_ty, projection, span);
2363                let disc = self.emit(CfgInstData::GetDiscriminant { base: val }, disc_ty, span);
2364                let lit = self.emit(CfgInstData::Const(*variant_index as u64), disc_ty, span);
2365                let cond = self.emit(CfgInstData::Bin(BinOp::Eq, disc, lit), Type::BOOL, span);
2366                if all_trivial {
2367                    self.branch_to(cond, matched, unmatched, span);
2368                } else {
2369                    let checked = self.cfg.new_block();
2370                    self.branch_to(cond, checked, unmatched, span);
2371                    self.current_block = checked;
2372                    self.emit_enum_field_tests(
2373                        scrut,
2374                        *variant_index,
2375                        fields.iter().map(|(idx, p)| (*idx, p)),
2376                        targets,
2377                        span,
2378                    );
2379                }
2380            }
2381            AirPattern::Tuple { elems } => {
2382                // For each element, recurse with an extended projection.
2383                // Intermediate blocks chain the successful element tests;
2384                // a failed test at any element branches straight to
2385                // `unmatched` without testing the rest.
2386                let struct_id = scr_ty
2387                    .as_struct()
2388                    .expect("AirPattern::Tuple scrutinee must be struct-shaped");
2389                let struct_def = self.type_pool.struct_def(struct_id);
2390                let field_tys: Vec<Type> = struct_def.fields.iter().map(|f| f.ty).collect();
2391                for (i, elem) in elems.iter().enumerate() {
2392                    let is_last = i + 1 == elems.len();
2393                    let next = if is_last {
2394                        matched
2395                    } else {
2396                        self.cfg.new_block()
2397                    };
2398                    let mut sub_proj: Vec<Projection> = projection.to_vec();
2399                    sub_proj.push(Projection::Field {
2400                        struct_id,
2401                        field_index: i as u32,
2402                    });
2403                    let field_ty = field_tys
2404                        .get(i)
2405                        .copied()
2406                        .expect("tuple pattern arity mismatches scrutinee");
2407                    let sub_scrut = Scrutinee {
2408                        slot: scr_slot,
2409                        ty: field_ty,
2410                        projection: &sub_proj,
2411                    };
2412                    let sub_targets = BranchTargets {
2413                        matched: next,
2414                        unmatched,
2415                    };
2416                    self.emit_pattern_test(sub_scrut, elem, sub_targets, span);
2417                    if !is_last {
2418                        self.current_block = next;
2419                    }
2420                }
2421            }
2422            AirPattern::Struct { struct_id, fields } => {
2423                for (i, (field_index, sub_pat)) in fields.iter().enumerate() {
2424                    let is_last = i + 1 == fields.len();
2425                    let next = if is_last {
2426                        matched
2427                    } else {
2428                        self.cfg.new_block()
2429                    };
2430                    let mut sub_proj: Vec<Projection> = projection.to_vec();
2431                    sub_proj.push(Projection::Field {
2432                        struct_id: *struct_id,
2433                        field_index: *field_index,
2434                    });
2435                    let field_ty = self
2436                        .type_pool
2437                        .struct_def(*struct_id)
2438                        .fields
2439                        .get(*field_index as usize)
2440                        .map(|f| f.ty)
2441                        .expect("struct pattern field index out of range");
2442                    let sub_scrut = Scrutinee {
2443                        slot: scr_slot,
2444                        ty: field_ty,
2445                        projection: &sub_proj,
2446                    };
2447                    let sub_targets = BranchTargets {
2448                        matched: next,
2449                        unmatched,
2450                    };
2451                    self.emit_pattern_test(sub_scrut, sub_pat, sub_targets, span);
2452                    if !is_last {
2453                        self.current_block = next;
2454                    }
2455                }
2456            }
2457            AirPattern::Bind { inner, .. } => {
2458                // Bind leaves are purely informational for CFG today — the
2459                // sema extraction path still emits `StorageLive` + field
2460                // extraction into the arm body for DataVariant /
2461                // StructVariant arms, and top-level irrefutable Ident arms
2462                // never hit this function (their scope lives in the arm
2463                // body via sema / astgen). Phase 4c will move binding
2464                // introduction into CFG so we can delete sema's inline
2465                // extraction and unify both paths; for now, treat Bind as
2466                // a transparent wrapper around its inner pattern.
2467                match inner {
2468                    Some(inner_pat) => {
2469                        self.emit_pattern_test(scrut, inner_pat, targets, span);
2470                    }
2471                    None => {
2472                        // `x` bare binding acts like `_` for dispatch.
2473                        let (a_s, a_l) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
2474                        self.cfg.set_terminator(
2475                            self.current_block,
2476                            Terminator::Goto {
2477                                target: matched,
2478                                args_start: a_s,
2479                                args_len: a_l,
2480                            },
2481                        );
2482                    }
2483                }
2484            }
2485        }
2486    }
2487
2488    /// Read the scrutinee value at `scr_slot` with the given projection,
2489    /// producing a `CfgValue` of type `ty`.
2490    fn read_projected(
2491        &mut self,
2492        scr_slot: u32,
2493        ty: Type,
2494        projection: &[Projection],
2495        span: gruel_util::Span,
2496    ) -> CfgValue {
2497        let place = self
2498            .cfg
2499            .make_place(PlaceBase::Local(scr_slot), projection.iter().copied());
2500        self.emit(CfgInstData::PlaceRead { place }, ty, span)
2501    }
2502
2503    /// ADR-0052 Phase 1: after a discriminant check passes, emit the
2504    /// per-field tests for a nested enum arm. Each field's sub-pattern
2505    /// is dispatched against the value extracted via `EnumPayloadGet`,
2506    /// spilled into a fresh temp slot so the existing projection-based
2507    /// helpers apply. On any mismatch the chain jumps to `unmatched`;
2508    /// the final field's matched branch lands in `matched`.
2509    fn emit_enum_field_tests<'p, I>(
2510        &mut self,
2511        scrut: Scrutinee<'_>,
2512        variant_index: u32,
2513        fields: I,
2514        targets: BranchTargets,
2515        span: gruel_util::Span,
2516    ) where
2517        I: IntoIterator<Item = (u32, &'p AirPattern)>,
2518    {
2519        let Scrutinee {
2520            slot: scr_slot,
2521            ty: scr_ty,
2522            projection,
2523        } = scrut;
2524        let BranchTargets { matched, unmatched } = targets;
2525        let enum_id = scr_ty
2526            .as_enum()
2527            .expect("enum-variant pattern requires an enum scrutinee");
2528        let variant_def = self.type_pool.enum_def(enum_id).variants[variant_index as usize].clone();
2529
2530        // Filter to non-trivial fields; trivial leaves are handled by sema's
2531        // existing binding-setup walker in the arm body.
2532        let non_trivial: Vec<(u32, &AirPattern)> = fields
2533            .into_iter()
2534            .filter(|(_, p)| !is_trivial_pattern(p))
2535            .collect();
2536        if non_trivial.is_empty() {
2537            let (a_s, a_l) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
2538            self.cfg.set_terminator(
2539                self.current_block,
2540                Terminator::Goto {
2541                    target: matched,
2542                    args_start: a_s,
2543                    args_len: a_l,
2544                },
2545            );
2546            return;
2547        }
2548
2549        // The base enum value lives in the spilled scrutinee slot; read it
2550        // once per chain so EnumPayloadGet sees the original composite.
2551        let base_val = self.read_projected(scr_slot, scr_ty, projection, span);
2552
2553        for (i, (field_index, sub_pat)) in non_trivial.iter().enumerate() {
2554            let is_last = i + 1 == non_trivial.len();
2555            let next = if is_last {
2556                matched
2557            } else {
2558                self.cfg.new_block()
2559            };
2560            let field_ty = variant_def
2561                .fields
2562                .get(*field_index as usize)
2563                .copied()
2564                .expect("enum field index out of range for variant");
2565            let field_val = self.emit(
2566                CfgInstData::EnumPayloadGet {
2567                    base: base_val,
2568                    variant_index,
2569                    field_index: *field_index,
2570                },
2571                field_ty,
2572                span,
2573            );
2574            let field_slot = self.cfg.alloc_temp_local();
2575            self.emit(
2576                CfgInstData::StorageLive { slot: field_slot },
2577                Type::UNIT,
2578                span,
2579            );
2580            self.emit(
2581                CfgInstData::Alloc {
2582                    slot: field_slot,
2583                    init: field_val,
2584                },
2585                Type::UNIT,
2586                span,
2587            );
2588            let sub_scrut = Scrutinee {
2589                slot: field_slot,
2590                ty: field_ty,
2591                projection: &[],
2592            };
2593            let sub_targets = BranchTargets {
2594                matched: next,
2595                unmatched,
2596            };
2597            self.emit_pattern_test(sub_scrut, sub_pat, sub_targets, span);
2598            if !is_last {
2599                self.current_block = next;
2600            }
2601        }
2602    }
2603
2604    /// ADR-0052: return the integer type used by this enum's
2605    /// discriminant. Falls back to `Type::I32` for non-enum scrutinees,
2606    /// which lets the caller treat the GetDiscriminant-then-Const-Eq
2607    /// shape uniformly at the cost of a sanity check.
2608    fn enum_discriminant_type(&self, scr_ty: Type) -> Type {
2609        if let Some(enum_id) = scr_ty.as_enum() {
2610            self.type_pool.enum_def(enum_id).discriminant_type()
2611        } else {
2612            Type::I32
2613        }
2614    }
2615
2616    /// Set the current block's terminator to a two-way conditional branch
2617    /// with empty block arguments on both sides. Helper for
2618    /// `emit_pattern_test`.
2619    fn branch_to(
2620        &mut self,
2621        cond: CfgValue,
2622        then_block: BlockId,
2623        else_block: BlockId,
2624        _span: gruel_util::Span,
2625    ) {
2626        let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
2627        let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty::<CfgValue>());
2628        self.cfg.set_terminator(
2629            self.current_block,
2630            Terminator::Branch {
2631                cond,
2632                then_block,
2633                then_args_start,
2634                then_args_len,
2635                else_block,
2636                else_args_start,
2637                else_args_len,
2638            },
2639        );
2640    }
2641
2642    /// ADR-0051 Phase 4b part 2: shared arm-body lowering + join wiring.
2643    /// Called by both the flat-switch and cascading-dispatch paths of the
2644    /// `AirInstData::Match` handler after arm selection has been wired.
2645    fn lower_match_arm_bodies_and_join(
2646        &mut self,
2647        arms: &[(AirPattern, AirRef)],
2648        arm_blocks: &[BlockId],
2649        join_block: BlockId,
2650        result_type: Type,
2651        air_ref: AirRef,
2652        _span: gruel_util::Span,
2653    ) -> ExprResult {
2654        let mut all_diverged = true;
2655        let mut arm_results = Vec::new();
2656
2657        for (i, (_, body)) in arms.iter().enumerate() {
2658            self.current_block = arm_blocks[i];
2659            let body_result = self.lower_inst(*body);
2660            let exit_block = self.current_block;
2661            let diverged = matches!(body_result.continuation, Continuation::Diverged);
2662            if !diverged {
2663                all_diverged = false;
2664            }
2665            arm_results.push((exit_block, body_result, diverged));
2666        }
2667
2668        if all_diverged {
2669            self.cfg.set_terminator(join_block, Terminator::Unreachable);
2670            return ExprResult {
2671                value: None,
2672                continuation: Continuation::Diverged,
2673            };
2674        }
2675
2676        let result_param = if result_type != Type::UNIT && result_type != Type::NEVER {
2677            Some(self.cfg.add_block_param(join_block, result_type))
2678        } else {
2679            None
2680        };
2681
2682        for (exit_block, body_result, diverged) in arm_results {
2683            if !diverged {
2684                let args: Vec<CfgValue> = if let Some(val) = body_result.value {
2685                    if result_param.is_some() {
2686                        vec![val]
2687                    } else {
2688                        vec![]
2689                    }
2690                } else {
2691                    vec![]
2692                };
2693                let (args_start, args_len) = self.cfg.push_extra(args);
2694                self.cfg.set_terminator(
2695                    exit_block,
2696                    Terminator::Goto {
2697                        target: join_block,
2698                        args_start,
2699                        args_len,
2700                    },
2701                );
2702            }
2703        }
2704
2705        self.current_block = join_block;
2706        if let Some(param) = result_param {
2707            self.cache(air_ref, param);
2708        }
2709
2710        ExprResult {
2711            value: result_param,
2712            continuation: Continuation::Continues,
2713        }
2714    }
2715
2716    /// Remove a local slot from all scope tracking to prevent it from being dropped at scope exit.
2717    ///
2718    /// Called when a non-Copy value is moved out of a local slot (e.g., into a struct field).
2719    /// Without this, the scope-exit drop elaboration would drop the original slot after the
2720    /// containing composite (struct/array) has already been dropped, causing a double-free.
2721    fn forget_local_slot(&mut self, slot: u32) {
2722        for scope in self.scope_stack.iter_mut() {
2723            scope.retain(|ls| ls.slot != slot);
2724        }
2725    }
2726
2727    /// Re-add a local slot to its original scope after it was previously
2728    /// forgotten. This handles the case where a variable is moved
2729    /// (`forget_local_slot`) and then reassigned — the new value needs to
2730    /// be tracked for drop, but only at the scope exit where the slot
2731    /// originally became live (not at any inner scope's exit).
2732    fn re_add_local_slot(&mut self, slot: u32, ty: Type, span: gruel_util::Span) {
2733        // Only add if not already tracked (avoid double-tracking)
2734        let already_tracked = self
2735            .scope_stack
2736            .iter()
2737            .any(|scope| scope.iter().any(|ls| ls.slot == slot));
2738        if already_tracked {
2739            return;
2740        }
2741        let target_depth = self
2742            .slot_origin_depth
2743            .get(&slot)
2744            .copied()
2745            .unwrap_or_else(|| self.scope_stack.len().saturating_sub(1));
2746        // Clamp in case the original scope has already been popped (a
2747        // reassignment outside the slot's original scope is a sema bug,
2748        // but we don't want to panic here).
2749        let depth = target_depth.min(self.scope_stack.len().saturating_sub(1));
2750        if let Some(scope) = self.scope_stack.get_mut(depth) {
2751            scope.push(LiveSlot { slot, ty, span });
2752        }
2753    }
2754
2755    /// Remove a parameter from live_params to prevent it from being dropped at function exit.
2756    ///
2757    /// Called when a non-copy parameter is consumed (passed to another function, moved into a
2758    /// struct, etc.). Without this, the function-exit drop elaboration would drop the parameter
2759    /// after ownership has already been transferred, causing a double-free.
2760    fn forget_param(&mut self, param_slot: u32) {
2761        self.live_params.retain(|lp| lp.param_slot != param_slot);
2762    }
2763
2764    /// Check if a type needs to be dropped (has a destructor).
2765    fn type_needs_drop(&self, ty: Type) -> bool {
2766        crate::drop_names::type_needs_drop(ty, self.type_pool)
2767    }
2768
2769    /// Forget any local slots or params that are consumed by a value being moved.
2770    /// Checks if `air_ref` is a Load (local) or Param instruction, and if its type
2771    /// needs drop, removes it from scope/param tracking to prevent double-free.
2772    fn forget_consumed_value(&mut self, air_ref: AirRef) {
2773        let inst = self.air.get(air_ref);
2774        match inst.data {
2775            AirInstData::Load { slot } => {
2776                if self.type_needs_drop(inst.ty) {
2777                    self.forget_local_slot(slot);
2778                }
2779            }
2780            AirInstData::Param { index } => {
2781                if self.type_needs_drop(inst.ty) {
2782                    self.forget_param(index);
2783                }
2784            }
2785            _ => {}
2786        }
2787    }
2788
2789    /// Emit drops for all live slots in all scopes, plus live params (for return).
2790    /// Drops are emitted in reverse order (LIFO) across all scopes, then params in reverse order.
2791    fn emit_drops_for_all_scopes(&mut self, span: gruel_util::Span) {
2792        // Collect all live slots in reverse order across all scopes
2793        let all_slots: Vec<LiveSlot> = self
2794            .scope_stack
2795            .iter()
2796            .rev()
2797            .flat_map(|scope| scope.iter().rev().cloned())
2798            .collect();
2799
2800        for live_slot in all_slots {
2801            self.emit_drop_for_slot(&live_slot, span);
2802        }
2803
2804        // Drop live params in reverse order (last param first)
2805        let params: Vec<LiveParam> = self.live_params.iter().rev().cloned().collect();
2806        for live_param in params {
2807            self.emit_drop_for_param(&live_param, span);
2808        }
2809    }
2810
2811    /// Emit drops for slots in scopes created inside the current loop (for break/continue).
2812    /// Only drops slots from the current scope depth down to (but not including) `target_depth`.
2813    /// This ensures that slots declared outside the loop are NOT dropped.
2814    fn emit_drops_for_loop_exit(&mut self, target_depth: usize, span: gruel_util::Span) {
2815        // Collect slots from scopes created inside the loop (depth >= target_depth)
2816        // in reverse order (LIFO)
2817        let loop_slots: Vec<LiveSlot> = self
2818            .scope_stack
2819            .iter()
2820            .skip(target_depth)
2821            .rev()
2822            .flat_map(|scope| scope.iter().rev().cloned())
2823            .collect();
2824
2825        for live_slot in loop_slots {
2826            self.emit_drop_for_slot(&live_slot, span);
2827        }
2828    }
2829
2830    /// Emit Drop and StorageDead for a single local slot.
2831    fn emit_drop_for_slot(&mut self, live_slot: &LiveSlot, span: gruel_util::Span) {
2832        // Emit Drop if the type needs it
2833        if self.type_needs_drop(live_slot.ty) {
2834            let slot_val = self.emit(
2835                CfgInstData::Load {
2836                    slot: live_slot.slot,
2837                },
2838                live_slot.ty,
2839                span,
2840            );
2841            self.emit(CfgInstData::Drop { value: slot_val }, Type::UNIT, span);
2842        }
2843        self.emit(
2844            CfgInstData::StorageDead {
2845                slot: live_slot.slot,
2846            },
2847            Type::UNIT,
2848            span,
2849        );
2850    }
2851
2852    /// Emit Drop for a function parameter.
2853    /// Unlike locals, params don't use StorageLive/StorageDead — they are live for the
2854    /// entire function. We just need to load and drop the value.
2855    fn emit_drop_for_param(&mut self, live_param: &LiveParam, span: gruel_util::Span) {
2856        let param_val = self.emit(
2857            CfgInstData::Param {
2858                index: live_param.param_slot,
2859            },
2860            live_param.ty,
2861            span,
2862        );
2863        self.emit(CfgInstData::Drop { value: param_val }, Type::UNIT, span);
2864    }
2865
2866    // ============================================================================
2867    // Place Expression Tracing (ADR-0030)
2868    // ============================================================================
2869
2870    /// Try to trace an AIR expression back to a Place.
2871    ///
2872    /// Returns `Some((base, projections))` if the expression represents a place
2873    /// (lvalue) that can be read from or written to. Returns `None` if the
2874    /// expression is not a simple place (e.g., a function call result).
2875    ///
2876    /// This function traces chains like `arr[i][j].field` into a `PlaceBase` and
2877    /// a list of `Projection`s. The projections are returned in order from the
2878    /// base outward (e.g., for `arr[i].x`, the projections are `[Index(i), Field(x)]`).
2879    ///
2880    /// The returned CfgValue indices for Index projections are the already-lowered
2881    /// index values, which must be computed before calling this function.
2882    fn try_trace_place(&mut self, air_ref: AirRef) -> TracedPlace {
2883        let inst = self.air.get(air_ref);
2884
2885        match &inst.data {
2886            // Base case: Load from a local variable
2887            AirInstData::Load { slot } => Some((PlaceBase::Local(*slot), Vec::new())),
2888
2889            // Base case: Parameter reference
2890            AirInstData::Param { index } => Some((PlaceBase::Param(*index), Vec::new())),
2891
2892            // Recursive case: Array index
2893            AirInstData::IndexGet {
2894                base,
2895                array_type,
2896                index,
2897            } => {
2898                // Recursively trace the base
2899                let (base_place, mut projections) = self.try_trace_place(*base)?;
2900
2901                // Lower the index expression to get the CfgValue
2902                let index_val = self.lower_value(*index)?;
2903
2904                // Add the Index projection
2905                projections.push((
2906                    Projection::Index {
2907                        array_type: *array_type,
2908                        index: index_val,
2909                    },
2910                    Some(index_val),
2911                ));
2912
2913                Some((base_place, projections))
2914            }
2915
2916            // Recursive case: Field access
2917            AirInstData::FieldGet {
2918                base,
2919                struct_id,
2920                field_index,
2921            } => {
2922                // Recursively trace the base
2923                let (base_place, mut projections) = self.try_trace_place(*base)?;
2924
2925                // Add the Field projection
2926                projections.push((
2927                    Projection::Field {
2928                        struct_id: *struct_id,
2929                        field_index: *field_index,
2930                    },
2931                    None,
2932                ));
2933
2934                Some((base_place, projections))
2935            }
2936
2937            // Not a simple place expression
2938            _ => None,
2939        }
2940    }
2941
2942    /// Lower a place expression from AIR to a CFG PlaceRead instruction.
2943    ///
2944    /// This is called when we detect that an IndexGet or FieldGet chain can be
2945    /// represented as a single PlaceRead, avoiding redundant Load instructions.
2946    fn lower_place_read(
2947        &mut self,
2948        air_ref: AirRef,
2949        ty: Type,
2950        span: gruel_util::Span,
2951    ) -> Option<CfgValue> {
2952        // Try to trace the expression to a place
2953        let (base, projections) = self.try_trace_place(air_ref)?;
2954
2955        // Build the Place with all projections
2956        let proj_iter = projections.into_iter().map(|(proj, _)| proj);
2957        let place = self.cfg.make_place(base, proj_iter);
2958
2959        // Emit the PlaceRead instruction
2960        let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
2961
2962        Some(value)
2963    }
2964
2965    /// Lower an AIR place reference to a CFG Place.
2966    ///
2967    /// This converts AirPlaceRef -> AirPlace -> CFG Place, translating projections
2968    /// and lowering any index expressions to CFG values.
2969    ///
2970    /// ADR-0062 / ADR-0063: lower an AIR lvalue (`Load { slot }` or
2971    /// `PlaceRead { place }`) to a CFG `Place`. Used when constructing
2972    /// references / pointers to a place without first loading the value.
2973    fn lower_air_lvalue_place(&mut self, air_ref: AirRef) -> Option<Place> {
2974        let inst = self.air.get(air_ref);
2975        match &inst.data {
2976            AirInstData::Load { slot } => Some(Place::local(*slot)),
2977            AirInstData::PlaceRead { place } => self.lower_air_place(*place),
2978            // ADR-0076: `&c` / `&mut c` where `c` is a function parameter
2979            // (e.g. forwarding a `MutRef(T)` parameter onward). Param-mode
2980            // parameters carry an LLVM pointer, so the place is just
2981            // `Param(index)`.
2982            AirInstData::Param { index } => Some(Place::param(*index)),
2983            other => panic!(
2984                "lower_air_lvalue_place: expected Load, PlaceRead, or Param, got {:?}",
2985                other
2986            ),
2987        }
2988    }
2989
2990    /// ADR-0030 Phase 8: This is the bridge between AIR's PlaceRead/PlaceWrite
2991    /// and CFG's PlaceRead/PlaceWrite.
2992    fn lower_air_place(&mut self, place_ref: AirPlaceRef) -> Option<Place> {
2993        let air_place = self.air.get_place(place_ref);
2994
2995        // Convert the base
2996        let base = match air_place.base {
2997            AirPlaceBase::Local(slot) => PlaceBase::Local(slot),
2998            AirPlaceBase::Param(slot) => PlaceBase::Param(slot),
2999        };
3000
3001        // Convert projections, lowering any index expressions
3002        let air_projections = self.air.get_place_projections(air_place);
3003        let mut cfg_projections = Vec::with_capacity(air_projections.len());
3004
3005        for proj in air_projections {
3006            let cfg_proj = match proj {
3007                AirProjection::Field {
3008                    struct_id,
3009                    field_index,
3010                } => Projection::Field {
3011                    struct_id: *struct_id,
3012                    field_index: *field_index,
3013                },
3014                AirProjection::Index { array_type, index } => {
3015                    // Lower the index expression to a CFG value
3016                    let index_val = self.lower_value(*index)?;
3017                    Projection::Index {
3018                        array_type: *array_type,
3019                        index: index_val,
3020                    }
3021                }
3022            };
3023            cfg_projections.push(cfg_proj);
3024        }
3025
3026        // Create the CFG place
3027        let place = self.cfg.make_place(base, cfg_projections);
3028
3029        Some(place)
3030    }
3031}
3032
3033#[cfg(test)]
3034mod tests {
3035    use super::*;
3036    use gruel_air::Sema;
3037    use gruel_lexer::Lexer;
3038    use gruel_parser::Parser;
3039    use gruel_rir::AstGen;
3040    use gruel_util::PreviewFeatures;
3041
3042    fn build_cfg(source: &str) -> Cfg {
3043        let lexer = Lexer::new(source);
3044        let (tokens, interner) = lexer.tokenize().unwrap();
3045        let parser = Parser::new(tokens, interner);
3046        let (ast, interner) = parser.parse().unwrap();
3047
3048        let astgen = AstGen::new(&ast, &interner);
3049        let rir = astgen.generate();
3050
3051        let sema = Sema::new(&rir, &interner, PreviewFeatures::default());
3052        let output = sema.analyze_all().unwrap();
3053
3054        let func = &output.functions[0];
3055        CfgBuilder::build(func, &output.type_pool, &interner).cfg
3056    }
3057
3058    #[test]
3059    fn test_simple_return() {
3060        let cfg = build_cfg("fn main() -> i32 { 42 }");
3061
3062        assert_eq!(cfg.block_count(), 1);
3063        assert_eq!(cfg.fn_name(), "main");
3064
3065        let entry = cfg.get_block(cfg.entry);
3066        assert!(matches!(entry.terminator, Terminator::Return { .. }));
3067    }
3068
3069    #[test]
3070    fn test_if_else() {
3071        let cfg = build_cfg("fn main() -> i32 { if true { 1 } else { 2 } }");
3072
3073        // Should have: entry, then, else, join
3074        assert!(cfg.block_count() >= 3);
3075    }
3076
3077    #[test]
3078    fn test_while_loop() {
3079        let cfg = build_cfg("fn main() -> i32 { let mut x = 0; while x < 10 { x = x + 1; } x }");
3080
3081        // Should have: entry, header, body, exit, and possibly join blocks
3082        assert!(cfg.block_count() >= 3);
3083    }
3084
3085    #[test]
3086    fn test_short_circuit_and() {
3087        let cfg = build_cfg("fn main() -> i32 { if true && false { 1 } else { 0 } }");
3088
3089        // && creates extra blocks for short-circuit evaluation
3090        assert!(cfg.block_count() >= 3);
3091    }
3092
3093    #[test]
3094    fn test_diverging_in_if_condition() {
3095        // Test that a diverging expression (block with return) in an if condition
3096        // is handled correctly without panicking.
3097        let cfg = build_cfg("fn main() -> i32 { if { return 1; true } { 2 } else { 3 } }");
3098
3099        // Should have at least entry block
3100        assert!(cfg.block_count() >= 1);
3101        // The function should return from the block in the condition
3102        let entry = cfg.get_block(cfg.entry);
3103        assert!(matches!(entry.terminator, Terminator::Return { .. }));
3104    }
3105
3106    #[test]
3107    fn test_diverging_in_loop_body() {
3108        // Test that a return inside a loop body is handled correctly.
3109        let cfg = build_cfg("fn main() -> i32 { loop { return 42; } }");
3110
3111        // The function should return from within the loop
3112        assert!(cfg.block_count() >= 2);
3113    }
3114}