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_error::{CompileWarning, WarningKind};
11
12use crate::CfgOutput;
13use crate::inst::{
14    BlockId, Cfg, CfgArgMode, CfgCallArg, CfgInst, CfgInstData, CfgValue, Place, PlaceBase,
15    Projection, Terminator,
16};
17
18/// A traced place: (base, list of (projection, optional index value)).
19type TracedPlace = Option<(PlaceBase, Vec<(Projection, Option<CfgValue>)>)>;
20
21/// Result of lowering an expression.
22struct ExprResult {
23    /// The value produced (if any - statements like Store don't produce values)
24    value: Option<CfgValue>,
25    /// Whether control flow continues after this expression
26    continuation: Continuation,
27}
28
29/// How control flow continues after an expression.
30enum Continuation {
31    /// Control continues normally (can add more instructions)
32    Continues,
33    /// Control flow diverged (return, break, continue) - no more instructions
34    Diverged,
35}
36
37/// Loop context for break/continue handling.
38struct LoopContext {
39    /// Block to jump to for continue (loop header)
40    header: BlockId,
41    /// Block to jump to for break (loop exit)
42    exit: BlockId,
43    /// The scope depth when entering the loop (before the loop body scope).
44    /// Used to know how many scopes to drop on break/continue.
45    /// For break/continue, we drop scopes from current down to (but not including)
46    /// this depth.
47    scope_depth: usize,
48}
49
50/// Information about a slot that became live in a scope.
51/// Used for drop elaboration.
52#[derive(Debug, Clone)]
53struct LiveSlot {
54    /// The slot number
55    slot: u32,
56    /// The type of value stored in the slot
57    ty: Type,
58    /// The span where the slot became live (for error reporting)
59    span: gruel_span::Span,
60}
61
62/// A live parameter that needs dropping at function exit.
63#[derive(Debug, Clone)]
64struct LiveParam {
65    /// The parameter slot index
66    param_slot: u32,
67    /// The type of the parameter
68    ty: Type,
69}
70
71/// Builder that converts AIR to CFG.
72pub struct CfgBuilder<'a> {
73    air: &'a Air,
74    cfg: Cfg,
75    /// Type intern pool for struct/enum/array lookups (Phase 2B ADR-0024)
76    type_pool: &'a TypeInternPool,
77    /// Current block we're building
78    current_block: BlockId,
79    /// Stack of loop contexts for nested loops
80    loop_stack: Vec<LoopContext>,
81    /// Cache: maps AIR refs to CFG values (for already-lowered instructions)
82    value_cache: Vec<Option<CfgValue>>,
83    /// Warnings collected during CFG construction (e.g., unreachable code)
84    warnings: Vec<CompileWarning>,
85    /// Stack of scopes for drop elaboration.
86    /// Each scope contains the slots that became live in that scope.
87    /// Used to emit StorageDead (and Drop if needed) at scope exit.
88    scope_stack: Vec<Vec<LiveSlot>>,
89    /// Parameters that are live and need dropping at function exit.
90    /// Non-inout parameters whose types need drop are added here at function entry.
91    /// When a parameter is consumed (passed to another function, moved into a struct, etc.),
92    /// it is removed from this list to prevent double-drop.
93    live_params: Vec<LiveParam>,
94}
95
96impl<'a> CfgBuilder<'a> {
97    /// Build a CFG from AIR, returning the CFG and any warnings.
98    ///
99    /// The `type_pool` provides struct/enum/array definitions needed for queries like
100    /// `type_needs_drop`.
101    pub fn build(func: &'a AnalyzedFunction, type_pool: &'a TypeInternPool) -> CfgOutput {
102        // Determine which by-value parameters need dropping at function exit.
103        // Inout/borrow parameters are not owned by the callee and must not be dropped.
104        // Destructors must NOT auto-drop their self parameter — the destructor IS the
105        // drop logic for that value.
106        let live_params: Vec<LiveParam> = if func.is_destructor {
107            Vec::new()
108        } else {
109            func.param_slot_types
110                .iter()
111                .enumerate()
112                .filter(|(i, ty)| {
113                    !func.param_modes.get(*i).copied().unwrap_or(false)
114                        && crate::drop_names::type_needs_drop(**ty, type_pool)
115                })
116                .map(|(i, ty)| LiveParam {
117                    param_slot: i as u32,
118                    ty: *ty,
119                })
120                .collect()
121        };
122
123        let mut builder = CfgBuilder {
124            air: &func.air,
125            cfg: Cfg::new(
126                func.air.return_type(),
127                func.num_locals,
128                func.num_param_slots,
129                func.name.to_string(),
130                func.param_modes.clone(),
131                func.param_slot_types.clone(),
132            ),
133            type_pool,
134            current_block: BlockId(0),
135            loop_stack: Vec::new(),
136            value_cache: vec![None; func.air.len()],
137            warnings: Vec::new(),
138            scope_stack: vec![Vec::new()], // Start with one scope for the function body
139            live_params,
140        };
141
142        // Create entry block
143        builder.current_block = builder.cfg.new_block();
144        builder.cfg.entry = builder.current_block;
145
146        // Find the root (should be Ret as last instruction)
147        if !func.air.is_empty() {
148            let root = AirRef::from_raw((func.air.len() - 1) as u32);
149            builder.lower_inst(root);
150        }
151
152        // Compute predecessor lists
153        builder.cfg.compute_predecessors();
154
155        CfgOutput {
156            cfg: builder.cfg,
157            warnings: builder.warnings,
158        }
159    }
160
161    /// Lower an AIR instruction, returning its result.
162    fn lower_inst(&mut self, air_ref: AirRef) -> ExprResult {
163        // Check cache first
164        if let Some(cached) = self.value_cache[air_ref.as_u32() as usize] {
165            return ExprResult {
166                value: Some(cached),
167                continuation: Continuation::Continues,
168            };
169        }
170
171        let inst = self.air.get(air_ref);
172        let span = inst.span;
173        let ty = inst.ty;
174
175        match &inst.data {
176            AirInstData::Const(v) => {
177                let value = self.emit(CfgInstData::Const(*v), ty, span);
178                self.cache(air_ref, value);
179                ExprResult {
180                    value: Some(value),
181                    continuation: Continuation::Continues,
182                }
183            }
184
185            AirInstData::FloatConst(bits) => {
186                let value = self.emit(CfgInstData::FloatConst(*bits), ty, span);
187                self.cache(air_ref, value);
188                ExprResult {
189                    value: Some(value),
190                    continuation: Continuation::Continues,
191                }
192            }
193
194            AirInstData::BoolConst(v) => {
195                let value = self.emit(CfgInstData::BoolConst(*v), ty, span);
196                self.cache(air_ref, value);
197                ExprResult {
198                    value: Some(value),
199                    continuation: Continuation::Continues,
200                }
201            }
202
203            AirInstData::StringConst(string_id) => {
204                let value = self.emit(CfgInstData::StringConst(*string_id), ty, span);
205                self.cache(air_ref, value);
206                ExprResult {
207                    value: Some(value),
208                    continuation: Continuation::Continues,
209                }
210            }
211
212            AirInstData::UnitConst => {
213                // Unit constants have no runtime representation.
214                // We emit a dummy const 0 with unit type for uniformity,
215                // but codegen will ignore values of unit type.
216                let value = self.emit(CfgInstData::Const(0), ty, span);
217                self.cache(air_ref, value);
218                ExprResult {
219                    value: Some(value),
220                    continuation: Continuation::Continues,
221                }
222            }
223
224            AirInstData::TypeConst(_) => {
225                // TypeConst instructions are compile-time-only. They can appear in the AIR
226                // in several valid scenarios:
227                // 1. As arguments to generic functions (substituted during specialization)
228                // 2. As the result of comptime type-returning functions (stored in comptime_type_vars)
229                //
230                // At CFG building time, any TypeConst that remains is simply a no-op -
231                // type values don't exist at runtime. We return Unit with no value to indicate
232                // this instruction doesn't produce runtime code.
233                ExprResult {
234                    value: None,
235                    continuation: Continuation::Continues,
236                }
237            }
238
239            AirInstData::CallGeneric { .. } => {
240                // CallGeneric instructions must be specialized (rewritten to Call)
241                // before CFG building. If we reach here, specialization didn't run.
242                //
243                // TODO(ICE): This should be converted to:
244                //   return Err(ice_error!("CallGeneric not specialized", phase: "cfg_builder"));
245                // But that requires refactoring build() to return CompileResult.
246                panic!(
247                    "CallGeneric instruction reached CFG building - this is a compiler bug. \
248                     CallGeneric must be specialized to regular Call before codegen."
249                );
250            }
251
252            AirInstData::Param { index } => {
253                let value = self.emit(CfgInstData::Param { index: *index }, ty, span);
254                self.cache(air_ref, value);
255                ExprResult {
256                    value: Some(value),
257                    continuation: Continuation::Continues,
258                }
259            }
260
261            AirInstData::Add(lhs, rhs) => {
262                let Some(lhs_val) = self.lower_value(*lhs) else {
263                    return Self::diverged();
264                };
265                let Some(rhs_val) = self.lower_value(*rhs) else {
266                    return Self::diverged();
267                };
268                let value = self.emit(CfgInstData::Add(lhs_val, rhs_val), ty, span);
269                self.cache(air_ref, value);
270                ExprResult {
271                    value: Some(value),
272                    continuation: Continuation::Continues,
273                }
274            }
275
276            AirInstData::Sub(lhs, rhs) => {
277                let Some(lhs_val) = self.lower_value(*lhs) else {
278                    return Self::diverged();
279                };
280                let Some(rhs_val) = self.lower_value(*rhs) else {
281                    return Self::diverged();
282                };
283                let value = self.emit(CfgInstData::Sub(lhs_val, rhs_val), ty, span);
284                self.cache(air_ref, value);
285                ExprResult {
286                    value: Some(value),
287                    continuation: Continuation::Continues,
288                }
289            }
290
291            AirInstData::Mul(lhs, rhs) => {
292                let Some(lhs_val) = self.lower_value(*lhs) else {
293                    return Self::diverged();
294                };
295                let Some(rhs_val) = self.lower_value(*rhs) else {
296                    return Self::diverged();
297                };
298                let value = self.emit(CfgInstData::Mul(lhs_val, rhs_val), ty, span);
299                self.cache(air_ref, value);
300                ExprResult {
301                    value: Some(value),
302                    continuation: Continuation::Continues,
303                }
304            }
305
306            AirInstData::Div(lhs, rhs) => {
307                let Some(lhs_val) = self.lower_value(*lhs) else {
308                    return Self::diverged();
309                };
310                let Some(rhs_val) = self.lower_value(*rhs) else {
311                    return Self::diverged();
312                };
313                let value = self.emit(CfgInstData::Div(lhs_val, rhs_val), ty, span);
314                self.cache(air_ref, value);
315                ExprResult {
316                    value: Some(value),
317                    continuation: Continuation::Continues,
318                }
319            }
320
321            AirInstData::Mod(lhs, rhs) => {
322                let Some(lhs_val) = self.lower_value(*lhs) else {
323                    return Self::diverged();
324                };
325                let Some(rhs_val) = self.lower_value(*rhs) else {
326                    return Self::diverged();
327                };
328                let value = self.emit(CfgInstData::Mod(lhs_val, rhs_val), ty, span);
329                self.cache(air_ref, value);
330                ExprResult {
331                    value: Some(value),
332                    continuation: Continuation::Continues,
333                }
334            }
335
336            AirInstData::Eq(lhs, rhs) => {
337                let Some(lhs_val) = self.lower_value(*lhs) else {
338                    return Self::diverged();
339                };
340                let Some(rhs_val) = self.lower_value(*rhs) else {
341                    return Self::diverged();
342                };
343                let value = self.emit(CfgInstData::Eq(lhs_val, rhs_val), ty, span);
344                self.cache(air_ref, value);
345                ExprResult {
346                    value: Some(value),
347                    continuation: Continuation::Continues,
348                }
349            }
350
351            AirInstData::Ne(lhs, rhs) => {
352                let Some(lhs_val) = self.lower_value(*lhs) else {
353                    return Self::diverged();
354                };
355                let Some(rhs_val) = self.lower_value(*rhs) else {
356                    return Self::diverged();
357                };
358                let value = self.emit(CfgInstData::Ne(lhs_val, rhs_val), ty, span);
359                self.cache(air_ref, value);
360                ExprResult {
361                    value: Some(value),
362                    continuation: Continuation::Continues,
363                }
364            }
365
366            AirInstData::Lt(lhs, rhs) => {
367                let Some(lhs_val) = self.lower_value(*lhs) else {
368                    return Self::diverged();
369                };
370                let Some(rhs_val) = self.lower_value(*rhs) else {
371                    return Self::diverged();
372                };
373                let value = self.emit(CfgInstData::Lt(lhs_val, rhs_val), ty, span);
374                self.cache(air_ref, value);
375                ExprResult {
376                    value: Some(value),
377                    continuation: Continuation::Continues,
378                }
379            }
380
381            AirInstData::Gt(lhs, rhs) => {
382                let Some(lhs_val) = self.lower_value(*lhs) else {
383                    return Self::diverged();
384                };
385                let Some(rhs_val) = self.lower_value(*rhs) else {
386                    return Self::diverged();
387                };
388                let value = self.emit(CfgInstData::Gt(lhs_val, rhs_val), ty, span);
389                self.cache(air_ref, value);
390                ExprResult {
391                    value: Some(value),
392                    continuation: Continuation::Continues,
393                }
394            }
395
396            AirInstData::Le(lhs, rhs) => {
397                let Some(lhs_val) = self.lower_value(*lhs) else {
398                    return Self::diverged();
399                };
400                let Some(rhs_val) = self.lower_value(*rhs) else {
401                    return Self::diverged();
402                };
403                let value = self.emit(CfgInstData::Le(lhs_val, rhs_val), ty, span);
404                self.cache(air_ref, value);
405                ExprResult {
406                    value: Some(value),
407                    continuation: Continuation::Continues,
408                }
409            }
410
411            AirInstData::Ge(lhs, rhs) => {
412                let Some(lhs_val) = self.lower_value(*lhs) else {
413                    return Self::diverged();
414                };
415                let Some(rhs_val) = self.lower_value(*rhs) else {
416                    return Self::diverged();
417                };
418                let value = self.emit(CfgInstData::Ge(lhs_val, rhs_val), ty, span);
419                self.cache(air_ref, value);
420                ExprResult {
421                    value: Some(value),
422                    continuation: Continuation::Continues,
423                }
424            }
425
426            AirInstData::And(lhs, rhs) => {
427                // Short-circuit: if lhs is false, result is false
428                // We need to create blocks for this
429                let Some(lhs_val) = self.lower_value(*lhs) else {
430                    return Self::diverged();
431                };
432
433                let rhs_block = self.cfg.new_block();
434                let join_block = self.cfg.new_block();
435
436                // Add block parameter for the result
437                let result_param = self.cfg.add_block_param(join_block, Type::BOOL);
438
439                // Branch: if lhs is false, go to join with false; else evaluate rhs
440                let false_val = self.emit(CfgInstData::BoolConst(false), Type::BOOL, span);
441                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
442                let (else_args_start, else_args_len) =
443                    self.cfg.push_extra(std::iter::once(false_val));
444                self.cfg.set_terminator(
445                    self.current_block,
446                    Terminator::Branch {
447                        cond: lhs_val,
448                        then_block: rhs_block,
449                        then_args_start,
450                        then_args_len,
451                        else_block: join_block,
452                        else_args_start,
453                        else_args_len,
454                    },
455                );
456
457                // In rhs_block, evaluate rhs and go to join
458                self.current_block = rhs_block;
459                let Some(rhs_val) = self.lower_value(*rhs) else {
460                    return Self::diverged();
461                };
462                let (args_start, args_len) = self.cfg.push_extra(std::iter::once(rhs_val));
463                self.cfg.set_terminator(
464                    self.current_block,
465                    Terminator::Goto {
466                        target: join_block,
467                        args_start,
468                        args_len,
469                    },
470                );
471
472                // Continue in join block
473                self.current_block = join_block;
474                self.cache(air_ref, result_param);
475                ExprResult {
476                    value: Some(result_param),
477                    continuation: Continuation::Continues,
478                }
479            }
480
481            AirInstData::Or(lhs, rhs) => {
482                // Short-circuit: if lhs is true, result is true
483                let Some(lhs_val) = self.lower_value(*lhs) else {
484                    return Self::diverged();
485                };
486
487                let rhs_block = self.cfg.new_block();
488                let join_block = self.cfg.new_block();
489
490                // Add block parameter for the result
491                let result_param = self.cfg.add_block_param(join_block, Type::BOOL);
492
493                // Branch: if lhs is true, go to join with true; else evaluate rhs
494                let true_val = self.emit(CfgInstData::BoolConst(true), Type::BOOL, span);
495                let (then_args_start, then_args_len) =
496                    self.cfg.push_extra(std::iter::once(true_val));
497                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
498                self.cfg.set_terminator(
499                    self.current_block,
500                    Terminator::Branch {
501                        cond: lhs_val,
502                        then_block: join_block,
503                        then_args_start,
504                        then_args_len,
505                        else_block: rhs_block,
506                        else_args_start,
507                        else_args_len,
508                    },
509                );
510
511                // In rhs_block, evaluate rhs and go to join
512                self.current_block = rhs_block;
513                let Some(rhs_val) = self.lower_value(*rhs) else {
514                    return Self::diverged();
515                };
516                let (args_start, args_len) = self.cfg.push_extra(std::iter::once(rhs_val));
517                self.cfg.set_terminator(
518                    self.current_block,
519                    Terminator::Goto {
520                        target: join_block,
521                        args_start,
522                        args_len,
523                    },
524                );
525
526                // Continue in join block
527                self.current_block = join_block;
528                self.cache(air_ref, result_param);
529                ExprResult {
530                    value: Some(result_param),
531                    continuation: Continuation::Continues,
532                }
533            }
534
535            AirInstData::Neg(operand) => {
536                let Some(op_val) = self.lower_value(*operand) else {
537                    return Self::diverged();
538                };
539                let value = self.emit(CfgInstData::Neg(op_val), ty, span);
540                self.cache(air_ref, value);
541                ExprResult {
542                    value: Some(value),
543                    continuation: Continuation::Continues,
544                }
545            }
546
547            AirInstData::Not(operand) => {
548                let Some(op_val) = self.lower_value(*operand) else {
549                    return Self::diverged();
550                };
551                let value = self.emit(CfgInstData::Not(op_val), ty, span);
552                self.cache(air_ref, value);
553                ExprResult {
554                    value: Some(value),
555                    continuation: Continuation::Continues,
556                }
557            }
558
559            AirInstData::BitNot(operand) => {
560                let Some(op_val) = self.lower_value(*operand) else {
561                    return Self::diverged();
562                };
563                let value = self.emit(CfgInstData::BitNot(op_val), ty, span);
564                self.cache(air_ref, value);
565                ExprResult {
566                    value: Some(value),
567                    continuation: Continuation::Continues,
568                }
569            }
570
571            AirInstData::BitAnd(lhs, rhs) => {
572                let Some(lhs_val) = self.lower_value(*lhs) else {
573                    return Self::diverged();
574                };
575                let Some(rhs_val) = self.lower_value(*rhs) else {
576                    return Self::diverged();
577                };
578                let value = self.emit(CfgInstData::BitAnd(lhs_val, rhs_val), ty, span);
579                self.cache(air_ref, value);
580                ExprResult {
581                    value: Some(value),
582                    continuation: Continuation::Continues,
583                }
584            }
585
586            AirInstData::BitOr(lhs, rhs) => {
587                let Some(lhs_val) = self.lower_value(*lhs) else {
588                    return Self::diverged();
589                };
590                let Some(rhs_val) = self.lower_value(*rhs) else {
591                    return Self::diverged();
592                };
593                let value = self.emit(CfgInstData::BitOr(lhs_val, rhs_val), ty, span);
594                self.cache(air_ref, value);
595                ExprResult {
596                    value: Some(value),
597                    continuation: Continuation::Continues,
598                }
599            }
600
601            AirInstData::BitXor(lhs, rhs) => {
602                let Some(lhs_val) = self.lower_value(*lhs) else {
603                    return Self::diverged();
604                };
605                let Some(rhs_val) = self.lower_value(*rhs) else {
606                    return Self::diverged();
607                };
608                let value = self.emit(CfgInstData::BitXor(lhs_val, rhs_val), ty, span);
609                self.cache(air_ref, value);
610                ExprResult {
611                    value: Some(value),
612                    continuation: Continuation::Continues,
613                }
614            }
615
616            AirInstData::Shl(lhs, rhs) => {
617                let Some(lhs_val) = self.lower_value(*lhs) else {
618                    return Self::diverged();
619                };
620                let Some(rhs_val) = self.lower_value(*rhs) else {
621                    return Self::diverged();
622                };
623                let value = self.emit(CfgInstData::Shl(lhs_val, rhs_val), ty, span);
624                self.cache(air_ref, value);
625                ExprResult {
626                    value: Some(value),
627                    continuation: Continuation::Continues,
628                }
629            }
630
631            AirInstData::Shr(lhs, rhs) => {
632                let Some(lhs_val) = self.lower_value(*lhs) else {
633                    return Self::diverged();
634                };
635                let Some(rhs_val) = self.lower_value(*rhs) else {
636                    return Self::diverged();
637                };
638                let value = self.emit(CfgInstData::Shr(lhs_val, rhs_val), ty, span);
639                self.cache(air_ref, value);
640                ExprResult {
641                    value: Some(value),
642                    continuation: Continuation::Continues,
643                }
644            }
645
646            AirInstData::Alloc { slot, init } => {
647                // If the init is a non-copy value from a local/param,
648                // remove it from scope tracking (ownership transfers to the new slot).
649                self.forget_consumed_value(*init);
650
651                let init_result = self.lower_inst(*init);
652                // If init produces a value, use it; otherwise use a dummy Unit value
653                let init_val = init_result
654                    .value
655                    .unwrap_or_else(|| self.emit(CfgInstData::Const(0), Type::UNIT, span));
656                self.emit(
657                    CfgInstData::Alloc {
658                        slot: *slot,
659                        init: init_val,
660                    },
661                    Type::UNIT,
662                    span,
663                );
664                ExprResult {
665                    value: None,
666                    continuation: Continuation::Continues,
667                }
668            }
669
670            AirInstData::Load { slot } => {
671                let value = self.emit(CfgInstData::Load { slot: *slot }, ty, span);
672                self.cache(air_ref, value);
673                ExprResult {
674                    value: Some(value),
675                    continuation: Continuation::Continues,
676                }
677            }
678
679            AirInstData::Store {
680                slot,
681                value,
682                had_live_value,
683            } => {
684                let Some(val) = self.lower_value(*value) else {
685                    return Self::diverged();
686                };
687                let value_ty = self.air.get(*value).ty;
688                // Drop the old value if it was live (not moved) and the type has a destructor.
689                if *had_live_value && self.type_needs_drop(value_ty) {
690                    let old_val = self.emit(CfgInstData::Load { slot: *slot }, value_ty, span);
691                    self.emit(CfgInstData::Drop { value: old_val }, Type::UNIT, span);
692                }
693                // If the old value was not live (reassignment after move), the slot was
694                // removed from scope tracking by forget_local_slot. Re-add it so the new
695                // value gets dropped at scope exit.
696                if !*had_live_value && self.type_needs_drop(value_ty) {
697                    self.re_add_local_slot(*slot, value_ty, span);
698                }
699                self.emit(
700                    CfgInstData::Store {
701                        slot: *slot,
702                        value: val,
703                    },
704                    Type::UNIT,
705                    span,
706                );
707                ExprResult {
708                    value: None,
709                    continuation: Continuation::Continues,
710                }
711            }
712
713            AirInstData::ParamStore { param_slot, value } => {
714                let Some(val) = self.lower_value(*value) else {
715                    return Self::diverged();
716                };
717                self.emit(
718                    CfgInstData::ParamStore {
719                        param_slot: *param_slot,
720                        value: val,
721                    },
722                    Type::UNIT,
723                    span,
724                );
725                ExprResult {
726                    value: None,
727                    continuation: Continuation::Continues,
728                }
729            }
730
731            AirInstData::Call {
732                name,
733                args_start,
734                args_len,
735            } => {
736                // Collect AIR call args for later use in forget logic
737                let air_call_args: Vec<_> =
738                    self.air.get_call_args(*args_start, *args_len).collect();
739
740                let mut arg_vals = Vec::new();
741                for arg in &air_call_args {
742                    let Some(value) = self.lower_value(arg.value) else {
743                        return Self::diverged();
744                    };
745                    arg_vals.push(CfgCallArg {
746                        value,
747                        mode: CfgArgMode::from(arg.mode),
748                    });
749                }
750
751                // Forget consumed values: normal (non-inout/borrow) args transfer ownership
752                // to the callee. Remove them from scope/param tracking to prevent double-drop.
753                for arg in &air_call_args {
754                    if arg.mode == AirArgMode::Normal {
755                        self.forget_consumed_value(arg.value);
756                    }
757                }
758
759                // Store args in extra array
760                let (args_start, args_len) = self.cfg.push_call_args(arg_vals);
761                let value = self.emit(
762                    CfgInstData::Call {
763                        name: *name,
764                        args_start,
765                        args_len,
766                    },
767                    ty,
768                    span,
769                );
770                self.cache(air_ref, value);
771                ExprResult {
772                    value: Some(value),
773                    continuation: Continuation::Continues,
774                }
775            }
776
777            AirInstData::Intrinsic {
778                name,
779                args_start,
780                args_len,
781            } => {
782                let mut arg_vals = Vec::new();
783                for arg in self.air.get_air_refs(*args_start, *args_len) {
784                    let Some(val) = self.lower_value(arg) else {
785                        return Self::diverged();
786                    };
787                    arg_vals.push(val);
788                }
789                // Store args in extra array
790                let (args_start, args_len) = self.cfg.push_extra(arg_vals);
791                let value = self.emit(
792                    CfgInstData::Intrinsic {
793                        name: *name,
794                        args_start,
795                        args_len,
796                    },
797                    ty,
798                    span,
799                );
800                self.cache(air_ref, value);
801                ExprResult {
802                    value: Some(value),
803                    continuation: Continuation::Continues,
804                }
805            }
806
807            AirInstData::StructInit {
808                struct_id,
809                fields_start,
810                fields_len,
811                source_order_start,
812            } => {
813                // Evaluate field initializers in SOURCE ORDER (spec 4.0:8)
814                // The source_order tells us which declaration-order index to evaluate at each step
815                let (fields, source_order) =
816                    self.air
817                        .get_struct_init(*fields_start, *fields_len, *source_order_start);
818                let fields: Vec<AirRef> = fields.collect();
819                let source_order: Vec<usize> = source_order.collect();
820
821                let mut lowered_fields: Vec<Option<CfgValue>> = vec![None; fields.len()];
822                for decl_idx in source_order {
823                    let Some(lowered) = self.lower_value(fields[decl_idx]) else {
824                        return Self::diverged();
825                    };
826                    lowered_fields[decl_idx] = Some(lowered);
827                }
828
829                // Forget consumed values to prevent double-drop at scope exit.
830                //
831                // When a non-Copy value (local or param) is moved into a struct field,
832                // the containing struct's drop glue handles freeing it.
833                // We must not also drop the original at scope exit, or we'd get a
834                // double-free.
835                for &field_air_ref in &fields {
836                    self.forget_consumed_value(field_air_ref);
837                }
838
839                // Collect in declaration order for storage layout
840                let field_vals: Vec<CfgValue> = lowered_fields
841                    .into_iter()
842                    .map(|opt: Option<CfgValue>| opt.expect("all fields should be lowered"))
843                    .collect();
844
845                // Store fields in extra array
846                let (fields_start, fields_len) = self.cfg.push_extra(field_vals);
847                let value = self.emit(
848                    CfgInstData::StructInit {
849                        struct_id: *struct_id,
850                        fields_start,
851                        fields_len,
852                    },
853                    ty,
854                    span,
855                );
856                self.cache(air_ref, value);
857                ExprResult {
858                    value: Some(value),
859                    continuation: Continuation::Continues,
860                }
861            }
862
863            AirInstData::FieldGet {
864                base,
865                struct_id,
866                field_index,
867            } => {
868                // ADR-0030 Phase 3: Try to use PlaceRead for field access
869                if let Some(value) = self.lower_place_read(air_ref, ty, span) {
870                    self.cache(air_ref, value);
871                    return ExprResult {
872                        value: Some(value),
873                        continuation: Continuation::Continues,
874                    };
875                }
876
877                // ADR-0030 Phase 6: Spill computed struct to temp, then use PlaceRead
878                // This handles cases like `get_struct().field` or `method().field`
879                // where the base is a computed value, not a local variable.
880                let Some(base_val) = self.lower_value(*base) else {
881                    return Self::diverged();
882                };
883
884                // Allocate a temporary slot for the struct
885                let temp_slot = self.cfg.alloc_temp_local();
886
887                // Emit StorageLive, Alloc to store the computed struct
888                self.emit(
889                    CfgInstData::StorageLive { slot: temp_slot },
890                    Type::UNIT,
891                    span,
892                );
893                self.emit(
894                    CfgInstData::Alloc {
895                        slot: temp_slot,
896                        init: base_val,
897                    },
898                    Type::UNIT,
899                    span,
900                );
901
902                // Create a PlaceRead from the temp slot with Field projection
903                let place = self.cfg.make_place(
904                    PlaceBase::Local(temp_slot),
905                    std::iter::once(Projection::Field {
906                        struct_id: *struct_id,
907                        field_index: *field_index,
908                    }),
909                );
910                let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
911
912                // Emit StorageDead for the temp
913                self.emit(
914                    CfgInstData::StorageDead { slot: temp_slot },
915                    Type::UNIT,
916                    span,
917                );
918
919                self.cache(air_ref, value);
920                ExprResult {
921                    value: Some(value),
922                    continuation: Continuation::Continues,
923                }
924            }
925
926            AirInstData::FieldSet {
927                slot,
928                struct_id,
929                field_index,
930                value,
931            } => {
932                let Some(val) = self.lower_value(*value) else {
933                    return Self::diverged();
934                };
935                self.emit(
936                    CfgInstData::FieldSet {
937                        slot: *slot,
938                        struct_id: *struct_id,
939                        field_index: *field_index,
940                        value: val,
941                    },
942                    Type::UNIT,
943                    span,
944                );
945                ExprResult {
946                    value: None,
947                    continuation: Continuation::Continues,
948                }
949            }
950
951            AirInstData::ParamFieldSet {
952                param_slot,
953                inner_offset,
954                struct_id,
955                field_index,
956                value,
957            } => {
958                let Some(val) = self.lower_value(*value) else {
959                    return Self::diverged();
960                };
961                self.emit(
962                    CfgInstData::ParamFieldSet {
963                        param_slot: *param_slot,
964                        inner_offset: *inner_offset,
965                        struct_id: *struct_id,
966                        field_index: *field_index,
967                        value: val,
968                    },
969                    Type::UNIT,
970                    span,
971                );
972                ExprResult {
973                    value: None,
974                    continuation: Continuation::Continues,
975                }
976            }
977
978            AirInstData::Block {
979                stmts_start,
980                stmts_len,
981                value,
982            } => {
983                // Collect statements into a Vec for iteration (needed for checking remaining)
984                let statements: Vec<AirRef> =
985                    self.air.get_air_refs(*stmts_start, *stmts_len).collect();
986
987                // Check if this is a "wrapper block" that only contains StorageLive statements.
988                // These are synthetic blocks created to pair StorageLive with Alloc, and they
989                // should NOT create a new scope for drop elaboration.
990                let is_storage_live_wrapper = statements.iter().all(|stmt| {
991                    matches!(self.air.get(*stmt).data, AirInstData::StorageLive { .. })
992                });
993
994                // Only push a scope if this is a real syntactic block (not a StorageLive wrapper)
995                if !is_storage_live_wrapper {
996                    self.scope_stack.push(Vec::new());
997                }
998
999                // Lower each statement.
1000                //
1001                // Design decision: When a statement diverges (break/continue/return), we only
1002                // warn about the *first* unreachable statement or value expression following it.
1003                // This matches Rust's behavior and avoids flooding the user with redundant
1004                // warnings for code like:
1005                //   break;
1006                //   x = 1;  // warn about this
1007                //   y = 2;  // don't warn about this (already covered by first warning)
1008                for (i, stmt) in statements.iter().enumerate() {
1009                    let result = self.lower_inst(*stmt);
1010                    if matches!(result.continuation, Continuation::Diverged) {
1011                        // Get the span of the diverging statement for the secondary label
1012                        let diverging_span = self.air.get(*stmt).span;
1013
1014                        // Check if there are remaining statements or a value expression
1015                        // that will never be executed
1016                        let remaining = &statements[i + 1..];
1017                        if !remaining.is_empty() {
1018                            // Warn about the first unreachable statement
1019                            let unreachable_stmt = remaining[0];
1020                            let unreachable_span = self.air.get(unreachable_stmt).span;
1021                            self.warnings.push(
1022                                CompileWarning::new(WarningKind::UnreachableCode, unreachable_span)
1023                                    .with_label(
1024                                        "any code following this expression is unreachable",
1025                                        diverging_span,
1026                                    )
1027                                    .with_note(
1028                                        "this warning occurs because the preceding expression \
1029                                         diverges (e.g., returns, breaks, or continues)",
1030                                    ),
1031                            );
1032                        } else {
1033                            // The final value expression is unreachable.
1034                            // However, don't warn about synthetic unit values (created by parser
1035                            // when a block has no trailing expression). These have zero-length
1036                            // spans pointing at the closing brace.
1037                            let value_span = self.air.get(*value).span;
1038                            let is_synthetic = value_span.start == value_span.end;
1039                            if !is_synthetic {
1040                                self.warnings.push(
1041                                    CompileWarning::new(WarningKind::UnreachableCode, value_span)
1042                                        .with_label(
1043                                            "any code following this expression is unreachable",
1044                                            diverging_span,
1045                                        )
1046                                        .with_note(
1047                                            "this warning occurs because the preceding expression \
1048                                             diverges (e.g., returns, breaks, or continues)",
1049                                        ),
1050                                );
1051                            }
1052                        }
1053                        // Note: drops were already emitted by the diverging statement
1054                        // (break/continue/return handle their own drops)
1055                        return ExprResult {
1056                            value: None,
1057                            continuation: Continuation::Diverged,
1058                        };
1059                    }
1060                }
1061
1062                // Lower the final value
1063                let result = self.lower_inst(*value);
1064
1065                // Pop scope and emit StorageDead (with Drop if needed) in reverse order.
1066                // BUT: if the value diverged (break/continue/return), the diverging
1067                // instruction already emitted drops for all scopes via emit_drops_for_all_scopes,
1068                // so we must NOT emit duplicate StorageDead here.
1069                if !is_storage_live_wrapper && let Some(scope_slots) = self.scope_stack.pop() {
1070                    // Only emit scope cleanup if the value didn't diverge
1071                    if !matches!(result.continuation, Continuation::Diverged) {
1072                        for live_slot in scope_slots.into_iter().rev() {
1073                            // Emit Drop for types that need cleanup (e.g., heap-allocated String)
1074                            if self.type_needs_drop(live_slot.ty) {
1075                                let slot_val = self.emit(
1076                                    CfgInstData::Load {
1077                                        slot: live_slot.slot,
1078                                    },
1079                                    live_slot.ty,
1080                                    live_slot.span,
1081                                );
1082                                self.emit(
1083                                    CfgInstData::Drop { value: slot_val },
1084                                    Type::UNIT,
1085                                    live_slot.span,
1086                                );
1087                            }
1088                            self.emit(
1089                                CfgInstData::StorageDead {
1090                                    slot: live_slot.slot,
1091                                },
1092                                Type::UNIT,
1093                                live_slot.span,
1094                            );
1095                        }
1096                    }
1097                }
1098
1099                result
1100            }
1101
1102            AirInstData::Branch {
1103                cond,
1104                then_value,
1105                else_value,
1106            } => {
1107                let Some(cond_val) = self.lower_value(*cond) else {
1108                    return Self::diverged();
1109                };
1110
1111                let then_block = self.cfg.new_block();
1112                let else_block = self.cfg.new_block();
1113                let join_block = self.cfg.new_block();
1114
1115                // Get types for then/else
1116                let then_type = self.air.get(*then_value).ty;
1117                let else_type = else_value.map(|e| self.air.get(e).ty);
1118
1119                // Branch to then/else
1120                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
1121                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
1122                self.cfg.set_terminator(
1123                    self.current_block,
1124                    Terminator::Branch {
1125                        cond: cond_val,
1126                        then_block,
1127                        then_args_start,
1128                        then_args_len,
1129                        else_block,
1130                        else_args_start,
1131                        else_args_len,
1132                    },
1133                );
1134
1135                // Lower then branch
1136                self.current_block = then_block;
1137                let then_result = self.lower_inst(*then_value);
1138                let then_exit_block = self.current_block;
1139                let then_diverged = matches!(then_result.continuation, Continuation::Diverged);
1140
1141                // Lower else branch
1142                self.current_block = else_block;
1143                let else_result = if let Some(else_val) = else_value {
1144                    self.lower_inst(*else_val)
1145                } else {
1146                    // No else - emit unit
1147                    let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1148                    ExprResult {
1149                        value: Some(unit_val),
1150                        continuation: Continuation::Continues,
1151                    }
1152                };
1153                let else_exit_block = self.current_block;
1154                let else_diverged = matches!(else_result.continuation, Continuation::Diverged);
1155
1156                // If both branches diverge, mark join block as unreachable and diverge
1157                if then_diverged && else_diverged {
1158                    self.cfg.set_terminator(join_block, Terminator::Unreachable);
1159                    return ExprResult {
1160                        value: None,
1161                        continuation: Continuation::Diverged,
1162                    };
1163                }
1164
1165                // Determine result type
1166                let result_type = if then_type.is_never() {
1167                    else_type.unwrap_or(Type::UNIT)
1168                } else {
1169                    then_type
1170                };
1171
1172                // Add block parameter for result (if we have a value type)
1173                let result_param = if result_type != Type::UNIT && result_type != Type::NEVER {
1174                    Some(self.cfg.add_block_param(join_block, result_type))
1175                } else {
1176                    None
1177                };
1178
1179                // Wire up non-divergent branches to join
1180                if !then_diverged {
1181                    let args: Vec<CfgValue> = if let Some(val) = then_result.value {
1182                        if result_param.is_some() {
1183                            vec![val]
1184                        } else {
1185                            vec![]
1186                        }
1187                    } else {
1188                        vec![]
1189                    };
1190                    let (args_start, args_len) = self.cfg.push_extra(args);
1191                    self.cfg.set_terminator(
1192                        then_exit_block,
1193                        Terminator::Goto {
1194                            target: join_block,
1195                            args_start,
1196                            args_len,
1197                        },
1198                    );
1199                }
1200
1201                if !else_diverged {
1202                    let args: Vec<CfgValue> = if let Some(val) = else_result.value {
1203                        if result_param.is_some() {
1204                            vec![val]
1205                        } else {
1206                            vec![]
1207                        }
1208                    } else {
1209                        vec![]
1210                    };
1211                    let (args_start, args_len) = self.cfg.push_extra(args);
1212                    self.cfg.set_terminator(
1213                        else_exit_block,
1214                        Terminator::Goto {
1215                            target: join_block,
1216                            args_start,
1217                            args_len,
1218                        },
1219                    );
1220                }
1221
1222                self.current_block = join_block;
1223
1224                if let Some(param) = result_param {
1225                    self.cache(air_ref, param);
1226                }
1227
1228                ExprResult {
1229                    value: result_param,
1230                    continuation: Continuation::Continues,
1231                }
1232            }
1233
1234            AirInstData::Loop { cond, body } => {
1235                let header_block = self.cfg.new_block();
1236                let body_block = self.cfg.new_block();
1237                let exit_block = self.cfg.new_block();
1238
1239                // Jump to header
1240                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1241                self.cfg.set_terminator(
1242                    self.current_block,
1243                    Terminator::Goto {
1244                        target: header_block,
1245                        args_start,
1246                        args_len,
1247                    },
1248                );
1249
1250                // Push loop context with current scope depth.
1251                // The scope depth is captured BEFORE the loop body is lowered,
1252                // so break/continue will drop all slots in scopes created INSIDE the loop.
1253                self.loop_stack.push(LoopContext {
1254                    header: header_block,
1255                    exit: exit_block,
1256                    scope_depth: self.scope_stack.len(),
1257                });
1258
1259                // Lower condition in header
1260                self.current_block = header_block;
1261                let Some(cond_val) = self.lower_value(*cond) else {
1262                    return Self::diverged();
1263                };
1264
1265                // Branch: if true go to body, if false exit
1266                let (then_args_start, then_args_len) = self.cfg.push_extra(std::iter::empty());
1267                let (else_args_start, else_args_len) = self.cfg.push_extra(std::iter::empty());
1268                self.cfg.set_terminator(
1269                    self.current_block,
1270                    Terminator::Branch {
1271                        cond: cond_val,
1272                        then_block: body_block,
1273                        then_args_start,
1274                        then_args_len,
1275                        else_block: exit_block,
1276                        else_args_start,
1277                        else_args_len,
1278                    },
1279                );
1280
1281                // Lower body
1282                self.current_block = body_block;
1283                let body_result = self.lower_inst(*body);
1284
1285                // After body, go back to header (unless diverged)
1286                if !matches!(body_result.continuation, Continuation::Diverged) {
1287                    let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1288                    self.cfg.set_terminator(
1289                        self.current_block,
1290                        Terminator::Goto {
1291                            target: header_block,
1292                            args_start,
1293                            args_len,
1294                        },
1295                    );
1296                }
1297
1298                self.loop_stack.pop();
1299
1300                // Continue after loop
1301                self.current_block = exit_block;
1302
1303                // Loops produce a unit value (for use in unit-returning functions)
1304                let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1305                ExprResult {
1306                    value: Some(unit_val),
1307                    continuation: Continuation::Continues,
1308                }
1309            }
1310
1311            AirInstData::InfiniteLoop { body } => {
1312                // Infinite loop: loop { body }
1313                //
1314                // Structure (2 blocks, not 3):
1315                //   body_block: execute body, then goto body_block
1316                //   exit_block: only reachable via break
1317                //
1318                // Unlike while loops, there's no condition check, so we don't need
1319                // a separate header block. The body_block serves as both the loop
1320                // entry point and the continue target.
1321                let body_block = self.cfg.new_block();
1322                let exit_block = self.cfg.new_block();
1323
1324                // Jump to body
1325                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1326                self.cfg.set_terminator(
1327                    self.current_block,
1328                    Terminator::Goto {
1329                        target: body_block,
1330                        args_start,
1331                        args_len,
1332                    },
1333                );
1334
1335                // Push loop context (body_block is the continue target).
1336                // The scope depth is captured BEFORE the loop body is lowered,
1337                // so break/continue will drop all slots in scopes created INSIDE the loop.
1338                self.loop_stack.push(LoopContext {
1339                    header: body_block,
1340                    exit: exit_block,
1341                    scope_depth: self.scope_stack.len(),
1342                });
1343
1344                // Lower body
1345                self.current_block = body_block;
1346                let body_result = self.lower_inst(*body);
1347
1348                // After body, go back to start (unless diverged via return/break/continue)
1349                if !matches!(body_result.continuation, Continuation::Diverged) {
1350                    let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1351                    self.cfg.set_terminator(
1352                        self.current_block,
1353                        Terminator::Goto {
1354                            target: body_block,
1355                            args_start,
1356                            args_len,
1357                        },
1358                    );
1359                }
1360
1361                self.loop_stack.pop();
1362
1363                // Continue after loop (only reachable via break).
1364                // Set Unreachable as the initial terminator. If there's code after the loop
1365                // (which requires a break to be reachable), the subsequent Ret instruction
1366                // will overwrite this with the correct Return terminator. If there's no break,
1367                // the block is truly unreachable and Unreachable is correct.
1368                self.current_block = exit_block;
1369                self.cfg
1370                    .set_terminator(self.current_block, Terminator::Unreachable);
1371
1372                // Infinite loops have Never type, but if we reach exit_block via break,
1373                // we need a dummy unit value for the loop expression result.
1374                let unit_val = self.emit(CfgInstData::Const(0), Type::UNIT, span);
1375                ExprResult {
1376                    value: Some(unit_val),
1377                    continuation: Continuation::Continues,
1378                }
1379            }
1380
1381            AirInstData::Match {
1382                scrutinee,
1383                arms_start,
1384                arms_len,
1385            } => {
1386                // Lower the scrutinee
1387                let Some(scrutinee_val) = self.lower_value(*scrutinee) else {
1388                    return Self::diverged();
1389                };
1390
1391                // Collect arms into a Vec for iteration
1392                let arms: Vec<(AirPattern, AirRef)> =
1393                    self.air.get_match_arms(*arms_start, *arms_len).collect();
1394
1395                // Create blocks for each arm and a join block
1396                let arm_blocks: Vec<_> = arms.iter().map(|_| self.cfg.new_block()).collect();
1397                let join_block = self.cfg.new_block();
1398
1399                // Get result type (from first non-Never arm)
1400                let result_type = arms
1401                    .iter()
1402                    .map(|(_, body)| self.air.get(*body).ty)
1403                    .find(|ty| !ty.is_never())
1404                    .unwrap_or(Type::NEVER);
1405
1406                // Create the switch terminator
1407                // Build cases: for each arm, check pattern and jump to corresponding block
1408                let mut switch_cases = Vec::new();
1409                let mut default_block = None;
1410
1411                for (i, (pattern, _)) in arms.iter().enumerate() {
1412                    match pattern {
1413                        AirPattern::Wildcard => {
1414                            default_block = Some(arm_blocks[i]);
1415                            // Wildcard matches everything - any patterns after this are unreachable
1416                            break;
1417                        }
1418                        AirPattern::Int(n) => {
1419                            switch_cases.push((*n, arm_blocks[i]));
1420                        }
1421                        AirPattern::Bool(b) => {
1422                            // Booleans are 0 or 1
1423                            let val = if *b { 1 } else { 0 };
1424                            switch_cases.push((val, arm_blocks[i]));
1425                        }
1426                        AirPattern::EnumVariant { variant_index, .. } => {
1427                            // Enum variants are matched by their discriminant (variant index)
1428                            switch_cases.push((*variant_index as i64, arm_blocks[i]));
1429                        }
1430                    }
1431                }
1432
1433                // If no explicit wildcard, use the last arm as default
1434                // This handles exhaustive matches like `true => ..., false => ...`
1435                // where semantics verified exhaustiveness but we need a default for codegen
1436                let default = default_block.unwrap_or_else(|| {
1437                    // Pop the last case to use as default
1438                    let (_, last_block) = switch_cases
1439                        .pop()
1440                        .expect("match must have at least one arm");
1441                    last_block
1442                });
1443
1444                // Set the switch terminator on current block
1445                let (cases_start, cases_len) = self.cfg.push_switch_cases(switch_cases);
1446                self.cfg.set_terminator(
1447                    self.current_block,
1448                    Terminator::Switch {
1449                        scrutinee: scrutinee_val,
1450                        cases_start,
1451                        cases_len,
1452                        default,
1453                    },
1454                );
1455
1456                // Lower each arm and wire to join block
1457                let mut all_diverged = true;
1458                let mut arm_results = Vec::new();
1459
1460                for (i, (_, body)) in arms.iter().enumerate() {
1461                    self.current_block = arm_blocks[i];
1462                    let body_result = self.lower_inst(*body);
1463                    let exit_block = self.current_block;
1464                    let diverged = matches!(body_result.continuation, Continuation::Diverged);
1465
1466                    if !diverged {
1467                        all_diverged = false;
1468                    }
1469
1470                    arm_results.push((exit_block, body_result, diverged));
1471                }
1472
1473                // If all arms diverge, mark join block unreachable
1474                if all_diverged {
1475                    self.cfg.set_terminator(join_block, Terminator::Unreachable);
1476                    return ExprResult {
1477                        value: None,
1478                        continuation: Continuation::Diverged,
1479                    };
1480                }
1481
1482                // Add block parameter for result (if we have a value type)
1483                let result_param = if result_type != Type::UNIT && result_type != Type::NEVER {
1484                    Some(self.cfg.add_block_param(join_block, result_type))
1485                } else {
1486                    None
1487                };
1488
1489                // Wire up non-divergent arms to join
1490                for (exit_block, body_result, diverged) in arm_results {
1491                    if !diverged {
1492                        let args: Vec<CfgValue> = if let Some(val) = body_result.value {
1493                            if result_param.is_some() {
1494                                vec![val]
1495                            } else {
1496                                vec![]
1497                            }
1498                        } else {
1499                            vec![]
1500                        };
1501                        let (args_start, args_len) = self.cfg.push_extra(args);
1502                        self.cfg.set_terminator(
1503                            exit_block,
1504                            Terminator::Goto {
1505                                target: join_block,
1506                                args_start,
1507                                args_len,
1508                            },
1509                        );
1510                    }
1511                }
1512
1513                self.current_block = join_block;
1514
1515                if let Some(param) = result_param {
1516                    self.cache(air_ref, param);
1517                }
1518
1519                ExprResult {
1520                    value: result_param,
1521                    continuation: Continuation::Continues,
1522                }
1523            }
1524
1525            AirInstData::Break => {
1526                // Emit drops for slots in scopes created inside the loop
1527                let loop_ctx = self.loop_stack.last().expect("break outside loop");
1528                let target_depth = loop_ctx.scope_depth;
1529                let exit_block = loop_ctx.exit;
1530                self.emit_drops_for_loop_exit(target_depth, span);
1531
1532                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1533                self.cfg.set_terminator(
1534                    self.current_block,
1535                    Terminator::Goto {
1536                        target: exit_block,
1537                        args_start,
1538                        args_len,
1539                    },
1540                );
1541
1542                ExprResult {
1543                    value: None,
1544                    continuation: Continuation::Diverged,
1545                }
1546            }
1547
1548            AirInstData::Continue => {
1549                // Emit drops for slots in scopes created inside the loop
1550                let loop_ctx = self.loop_stack.last().expect("continue outside loop");
1551                let target_depth = loop_ctx.scope_depth;
1552                let header_block = loop_ctx.header;
1553                self.emit_drops_for_loop_exit(target_depth, span);
1554
1555                let (args_start, args_len) = self.cfg.push_extra(std::iter::empty());
1556                self.cfg.set_terminator(
1557                    self.current_block,
1558                    Terminator::Goto {
1559                        target: header_block,
1560                        args_start,
1561                        args_len,
1562                    },
1563                );
1564
1565                ExprResult {
1566                    value: None,
1567                    continuation: Continuation::Diverged,
1568                }
1569            }
1570
1571            AirInstData::Ret(value) => {
1572                let val = match value {
1573                    Some(v) => {
1574                        let result = self.lower_inst(*v);
1575                        if matches!(result.continuation, Continuation::Diverged) {
1576                            // The return value expression itself diverged (e.g., a block
1577                            // containing an earlier return). The terminator was already set
1578                            // by the inner diverging expression, so just propagate divergence.
1579                            return Self::diverged();
1580                        }
1581
1582                        // The returned value transfers ownership to the caller.
1583                        // Remove it from scope/param tracking to prevent dropping it.
1584                        self.forget_consumed_value(*v);
1585
1586                        // result.value may be None for Unit-typed expressions - that's OK
1587                        result.value
1588                    }
1589                    None => None,
1590                };
1591
1592                // Emit drops for all live slots before returning
1593                self.emit_drops_for_all_scopes(span);
1594
1595                self.cfg
1596                    .set_terminator(self.current_block, Terminator::Return { value: val });
1597
1598                ExprResult {
1599                    value: None,
1600                    continuation: Continuation::Diverged,
1601                }
1602            }
1603
1604            AirInstData::ArrayInit {
1605                elems_start,
1606                elems_len,
1607            } => {
1608                let elems: Vec<AirRef> = self.air.get_air_refs(*elems_start, *elems_len).collect();
1609                let mut element_vals = Vec::new();
1610                for &elem in &elems {
1611                    let Some(val) = self.lower_value(elem) else {
1612                        return Self::diverged();
1613                    };
1614                    element_vals.push(val);
1615                }
1616
1617                // Forget consumed values to prevent double-drop
1618                for &elem in &elems {
1619                    self.forget_consumed_value(elem);
1620                }
1621
1622                // Store elements in extra array
1623                let (elements_start, elements_len) = self.cfg.push_extra(element_vals);
1624                let value = self.emit(
1625                    CfgInstData::ArrayInit {
1626                        elements_start,
1627                        elements_len,
1628                    },
1629                    ty,
1630                    span,
1631                );
1632                self.cache(air_ref, value);
1633                ExprResult {
1634                    value: Some(value),
1635                    continuation: Continuation::Continues,
1636                }
1637            }
1638
1639            AirInstData::IndexGet {
1640                base,
1641                array_type,
1642                index,
1643            } => {
1644                // ADR-0030 Phase 3: Try to use PlaceRead for array indexing
1645                if let Some(value) = self.lower_place_read(air_ref, ty, span) {
1646                    self.cache(air_ref, value);
1647                    return ExprResult {
1648                        value: Some(value),
1649                        continuation: Continuation::Continues,
1650                    };
1651                }
1652
1653                // ADR-0030 Phase 6: Spill computed array to temp, then use PlaceRead
1654                // This handles cases like `get_array()[i]` where the base is a computed
1655                // value, not a local variable.
1656                // Note: Currently Gruel can't return arrays (see issue gruel-b79f), but this
1657                // handles the case for when that's fixed.
1658                let Some(base_val) = self.lower_value(*base) else {
1659                    return Self::diverged();
1660                };
1661                let Some(index_val) = self.lower_value(*index) else {
1662                    return Self::diverged();
1663                };
1664
1665                // Allocate a temporary slot for the array
1666                let temp_slot = self.cfg.alloc_temp_local();
1667
1668                // Emit StorageLive, Alloc to store the computed array
1669                self.emit(
1670                    CfgInstData::StorageLive { slot: temp_slot },
1671                    Type::UNIT,
1672                    span,
1673                );
1674                self.emit(
1675                    CfgInstData::Alloc {
1676                        slot: temp_slot,
1677                        init: base_val,
1678                    },
1679                    Type::UNIT,
1680                    span,
1681                );
1682
1683                // Create a PlaceRead from the temp slot with Index projection
1684                let place = self.cfg.make_place(
1685                    PlaceBase::Local(temp_slot),
1686                    std::iter::once(Projection::Index {
1687                        array_type: *array_type,
1688                        index: index_val,
1689                    }),
1690                );
1691                let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
1692
1693                // Emit StorageDead for the temp
1694                self.emit(
1695                    CfgInstData::StorageDead { slot: temp_slot },
1696                    Type::UNIT,
1697                    span,
1698                );
1699
1700                self.cache(air_ref, value);
1701                ExprResult {
1702                    value: Some(value),
1703                    continuation: Continuation::Continues,
1704                }
1705            }
1706
1707            AirInstData::IndexSet {
1708                slot,
1709                array_type,
1710                index,
1711                value,
1712            } => {
1713                let Some(index_val) = self.lower_value(*index) else {
1714                    return Self::diverged();
1715                };
1716                let Some(val) = self.lower_value(*value) else {
1717                    return Self::diverged();
1718                };
1719                self.emit(
1720                    CfgInstData::IndexSet {
1721                        slot: *slot,
1722                        array_type: *array_type,
1723                        index: index_val,
1724                        value: val,
1725                    },
1726                    Type::UNIT,
1727                    span,
1728                );
1729                ExprResult {
1730                    value: None,
1731                    continuation: Continuation::Continues,
1732                }
1733            }
1734
1735            AirInstData::ParamIndexSet {
1736                param_slot,
1737                array_type,
1738                index,
1739                value,
1740            } => {
1741                let Some(index_val) = self.lower_value(*index) else {
1742                    return Self::diverged();
1743                };
1744                let Some(val) = self.lower_value(*value) else {
1745                    return Self::diverged();
1746                };
1747                self.emit(
1748                    CfgInstData::ParamIndexSet {
1749                        param_slot: *param_slot,
1750                        array_type: *array_type,
1751                        index: index_val,
1752                        value: val,
1753                    },
1754                    Type::UNIT,
1755                    span,
1756                );
1757                ExprResult {
1758                    value: None,
1759                    continuation: Continuation::Continues,
1760                }
1761            }
1762
1763            // ADR-0030 Phase 8: Handle AIR place-based instructions
1764            AirInstData::PlaceRead { place } => {
1765                // Convert AIR place to CFG place
1766                let Some(cfg_place) = self.lower_air_place(*place) else {
1767                    return Self::diverged();
1768                };
1769                let value = self.emit(CfgInstData::PlaceRead { place: cfg_place }, ty, span);
1770                self.cache(air_ref, value);
1771                ExprResult {
1772                    value: Some(value),
1773                    continuation: Continuation::Continues,
1774                }
1775            }
1776
1777            AirInstData::PlaceWrite { place, value } => {
1778                // Lower the value first (RHS evaluated before drop of old value)
1779                let Some(val) = self.lower_value(*value) else {
1780                    return Self::diverged();
1781                };
1782                let value_ty = self.air.get(*value).ty;
1783                // Convert AIR place to CFG place
1784                let Some(cfg_place) = self.lower_air_place(*place) else {
1785                    return Self::diverged();
1786                };
1787                // Drop the old value at this place if the type has a destructor.
1788                // For PlaceWrite (field/index assignment), the base is always live
1789                // (you cannot write to a field of a moved value), so no liveness check needed.
1790                if self.type_needs_drop(value_ty) {
1791                    let old_val =
1792                        self.emit(CfgInstData::PlaceRead { place: cfg_place }, value_ty, span);
1793                    self.emit(CfgInstData::Drop { value: old_val }, Type::UNIT, span);
1794                }
1795                self.emit(
1796                    CfgInstData::PlaceWrite {
1797                        place: cfg_place,
1798                        value: val,
1799                    },
1800                    Type::UNIT,
1801                    span,
1802                );
1803                ExprResult {
1804                    value: None,
1805                    continuation: Continuation::Continues,
1806                }
1807            }
1808
1809            AirInstData::EnumVariant {
1810                enum_id,
1811                variant_index,
1812            } => {
1813                // Enum variants are just their discriminant value
1814                let value = self.emit(
1815                    CfgInstData::EnumVariant {
1816                        enum_id: *enum_id,
1817                        variant_index: *variant_index,
1818                    },
1819                    ty,
1820                    span,
1821                );
1822                self.cache(air_ref, value);
1823                ExprResult {
1824                    value: Some(value),
1825                    continuation: Continuation::Continues,
1826                }
1827            }
1828
1829            AirInstData::EnumCreate {
1830                enum_id,
1831                variant_index,
1832                fields_start,
1833                fields_len,
1834            } => {
1835                // Lower each field value
1836                let field_air_refs = self
1837                    .air
1838                    .get_air_refs(*fields_start, *fields_len)
1839                    .collect::<Vec<_>>();
1840                let mut field_vals = Vec::with_capacity(field_air_refs.len());
1841                for field_ref in field_air_refs {
1842                    let Some(val) = self.lower_value(field_ref) else {
1843                        return Self::diverged();
1844                    };
1845                    field_vals.push(val);
1846                }
1847
1848                // Store field CfgValues in the extra array
1849                let (fields_start_cfg, fields_len_cfg) = self.cfg.push_extra(field_vals);
1850
1851                let value = self.emit(
1852                    CfgInstData::EnumCreate {
1853                        enum_id: *enum_id,
1854                        variant_index: *variant_index,
1855                        fields_start: fields_start_cfg,
1856                        fields_len: fields_len_cfg,
1857                    },
1858                    ty,
1859                    span,
1860                );
1861                self.cache(air_ref, value);
1862                ExprResult {
1863                    value: Some(value),
1864                    continuation: Continuation::Continues,
1865                }
1866            }
1867
1868            AirInstData::EnumPayloadGet {
1869                base,
1870                variant_index,
1871                field_index,
1872            } => {
1873                let Some(base_val) = self.lower_value(*base) else {
1874                    return Self::diverged();
1875                };
1876                let value = self.emit(
1877                    CfgInstData::EnumPayloadGet {
1878                        base: base_val,
1879                        variant_index: *variant_index,
1880                        field_index: *field_index,
1881                    },
1882                    ty,
1883                    span,
1884                );
1885                self.cache(air_ref, value);
1886                ExprResult {
1887                    value: Some(value),
1888                    continuation: Continuation::Continues,
1889                }
1890            }
1891
1892            AirInstData::IntCast { value, from_ty } => {
1893                let Some(val) = self.lower_value(*value) else {
1894                    return Self::diverged();
1895                };
1896                let result = self.emit(
1897                    CfgInstData::IntCast {
1898                        value: val,
1899                        from_ty: *from_ty,
1900                    },
1901                    ty,
1902                    span,
1903                );
1904                self.cache(air_ref, result);
1905                ExprResult {
1906                    value: Some(result),
1907                    continuation: Continuation::Continues,
1908                }
1909            }
1910
1911            AirInstData::FloatCast { value, from_ty } => {
1912                let Some(val) = self.lower_value(*value) else {
1913                    return Self::diverged();
1914                };
1915                let result = self.emit(
1916                    CfgInstData::FloatCast {
1917                        value: val,
1918                        from_ty: *from_ty,
1919                    },
1920                    ty,
1921                    span,
1922                );
1923                self.cache(air_ref, result);
1924                ExprResult {
1925                    value: Some(result),
1926                    continuation: Continuation::Continues,
1927                }
1928            }
1929
1930            AirInstData::IntToFloat { value, from_ty } => {
1931                let Some(val) = self.lower_value(*value) else {
1932                    return Self::diverged();
1933                };
1934                let result = self.emit(
1935                    CfgInstData::IntToFloat {
1936                        value: val,
1937                        from_ty: *from_ty,
1938                    },
1939                    ty,
1940                    span,
1941                );
1942                self.cache(air_ref, result);
1943                ExprResult {
1944                    value: Some(result),
1945                    continuation: Continuation::Continues,
1946                }
1947            }
1948
1949            AirInstData::FloatToInt { value, from_ty } => {
1950                let Some(val) = self.lower_value(*value) else {
1951                    return Self::diverged();
1952                };
1953                let result = self.emit(
1954                    CfgInstData::FloatToInt {
1955                        value: val,
1956                        from_ty: *from_ty,
1957                    },
1958                    ty,
1959                    span,
1960                );
1961                self.cache(air_ref, result);
1962                ExprResult {
1963                    value: Some(result),
1964                    continuation: Continuation::Continues,
1965                }
1966            }
1967
1968            AirInstData::Drop { value } => {
1969                // Lower the value to drop
1970                let Some(val) = self.lower_value(*value) else {
1971                    return Self::diverged();
1972                };
1973                let val_ty = self.air.get(*value).ty;
1974
1975                // Only emit a Drop instruction if the type needs drop.
1976                // For trivially droppable types, this is a no-op.
1977                // We use self.type_needs_drop() which has access to struct/array
1978                // definitions to recursively check if fields need drop.
1979                if self.type_needs_drop(val_ty) {
1980                    self.emit(CfgInstData::Drop { value: val }, Type::UNIT, span);
1981                }
1982
1983                // Drop is a statement, produces no value
1984                ExprResult {
1985                    value: None,
1986                    continuation: Continuation::Continues,
1987                }
1988            }
1989
1990            AirInstData::StorageLive { slot } => {
1991                // Emit StorageLive to CFG
1992                self.emit(CfgInstData::StorageLive { slot: *slot }, Type::UNIT, span);
1993
1994                // Record this slot as live in the current scope for drop elaboration
1995                if let Some(scope) = self.scope_stack.last_mut() {
1996                    scope.push(LiveSlot {
1997                        slot: *slot,
1998                        ty,
1999                        span,
2000                    });
2001                }
2002
2003                ExprResult {
2004                    value: None,
2005                    continuation: Continuation::Continues,
2006                }
2007            }
2008
2009            AirInstData::StorageDead { slot } => {
2010                // StorageDead in AIR is a hint; CFG builder emits these at scope exit
2011                // This case handles explicit StorageDead if any (currently unused)
2012                self.emit(CfgInstData::StorageDead { slot: *slot }, Type::UNIT, span);
2013                ExprResult {
2014                    value: None,
2015                    continuation: Continuation::Continues,
2016                }
2017            }
2018        }
2019    }
2020
2021    /// Emit an instruction in the current block.
2022    fn emit(&mut self, data: CfgInstData, ty: Type, span: gruel_span::Span) -> CfgValue {
2023        self.cfg
2024            .add_inst_to_block(self.current_block, CfgInst { data, ty, span })
2025    }
2026
2027    /// Cache a value for an AIR ref.
2028    fn cache(&mut self, air_ref: AirRef, value: CfgValue) {
2029        self.value_cache[air_ref.as_u32() as usize] = Some(value);
2030    }
2031
2032    /// Lower an instruction and return its value, or None if it diverged.
2033    /// This is a helper for use with the `?` operator when processing operands.
2034    /// If the operand diverged, the caller should propagate the divergence.
2035    fn lower_value(&mut self, air_ref: AirRef) -> Option<CfgValue> {
2036        let result = self.lower_inst(air_ref);
2037        if matches!(result.continuation, Continuation::Diverged) {
2038            None
2039        } else {
2040            result.value
2041        }
2042    }
2043
2044    /// Create a diverged ExprResult. Used when an operand diverges.
2045    fn diverged() -> ExprResult {
2046        ExprResult {
2047            value: None,
2048            continuation: Continuation::Diverged,
2049        }
2050    }
2051
2052    /// Remove a local slot from all scope tracking to prevent it from being dropped at scope exit.
2053    ///
2054    /// Called when a non-Copy value is moved out of a local slot (e.g., into a struct field).
2055    /// Without this, the scope-exit drop elaboration would drop the original slot after the
2056    /// containing composite (struct/array) has already been dropped, causing a double-free.
2057    fn forget_local_slot(&mut self, slot: u32) {
2058        for scope in self.scope_stack.iter_mut() {
2059            scope.retain(|ls| ls.slot != slot);
2060        }
2061    }
2062
2063    /// Re-add a local slot to the current scope after it was previously forgotten.
2064    /// This handles the case where a variable is moved (forget_local_slot), then
2065    /// reassigned — the new value needs to be tracked for drop at scope exit.
2066    fn re_add_local_slot(&mut self, slot: u32, ty: Type, span: gruel_span::Span) {
2067        // Only add if not already tracked (avoid double-tracking)
2068        let already_tracked = self
2069            .scope_stack
2070            .iter()
2071            .any(|scope| scope.iter().any(|ls| ls.slot == slot));
2072        if !already_tracked && let Some(scope) = self.scope_stack.last_mut() {
2073            scope.push(LiveSlot { slot, ty, span });
2074        }
2075    }
2076
2077    /// Remove a parameter from live_params to prevent it from being dropped at function exit.
2078    ///
2079    /// Called when a non-copy parameter is consumed (passed to another function, moved into a
2080    /// struct, etc.). Without this, the function-exit drop elaboration would drop the parameter
2081    /// after ownership has already been transferred, causing a double-free.
2082    fn forget_param(&mut self, param_slot: u32) {
2083        self.live_params.retain(|lp| lp.param_slot != param_slot);
2084    }
2085
2086    /// Check if a type needs to be dropped (has a destructor).
2087    fn type_needs_drop(&self, ty: Type) -> bool {
2088        crate::drop_names::type_needs_drop(ty, self.type_pool)
2089    }
2090
2091    /// Forget any local slots or params that are consumed by a value being moved.
2092    /// Checks if `air_ref` is a Load (local) or Param instruction, and if its type
2093    /// needs drop, removes it from scope/param tracking to prevent double-free.
2094    fn forget_consumed_value(&mut self, air_ref: AirRef) {
2095        let inst = self.air.get(air_ref);
2096        match inst.data {
2097            AirInstData::Load { slot } => {
2098                if self.type_needs_drop(inst.ty) {
2099                    self.forget_local_slot(slot);
2100                }
2101            }
2102            AirInstData::Param { index } => {
2103                if self.type_needs_drop(inst.ty) {
2104                    self.forget_param(index);
2105                }
2106            }
2107            _ => {}
2108        }
2109    }
2110
2111    /// Emit drops for all live slots in all scopes, plus live params (for return).
2112    /// Drops are emitted in reverse order (LIFO) across all scopes, then params in reverse order.
2113    fn emit_drops_for_all_scopes(&mut self, span: gruel_span::Span) {
2114        // Collect all live slots in reverse order across all scopes
2115        let all_slots: Vec<LiveSlot> = self
2116            .scope_stack
2117            .iter()
2118            .rev()
2119            .flat_map(|scope| scope.iter().rev().cloned())
2120            .collect();
2121
2122        for live_slot in all_slots {
2123            self.emit_drop_for_slot(&live_slot, span);
2124        }
2125
2126        // Drop live params in reverse order (last param first)
2127        let params: Vec<LiveParam> = self.live_params.iter().rev().cloned().collect();
2128        for live_param in params {
2129            self.emit_drop_for_param(&live_param, span);
2130        }
2131    }
2132
2133    /// Emit drops for slots in scopes created inside the current loop (for break/continue).
2134    /// Only drops slots from the current scope depth down to (but not including) `target_depth`.
2135    /// This ensures that slots declared outside the loop are NOT dropped.
2136    fn emit_drops_for_loop_exit(&mut self, target_depth: usize, span: gruel_span::Span) {
2137        // Collect slots from scopes created inside the loop (depth >= target_depth)
2138        // in reverse order (LIFO)
2139        let loop_slots: Vec<LiveSlot> = self
2140            .scope_stack
2141            .iter()
2142            .skip(target_depth)
2143            .rev()
2144            .flat_map(|scope| scope.iter().rev().cloned())
2145            .collect();
2146
2147        for live_slot in loop_slots {
2148            self.emit_drop_for_slot(&live_slot, span);
2149        }
2150    }
2151
2152    /// Emit Drop and StorageDead for a single local slot.
2153    fn emit_drop_for_slot(&mut self, live_slot: &LiveSlot, span: gruel_span::Span) {
2154        // Emit Drop if the type needs it
2155        if self.type_needs_drop(live_slot.ty) {
2156            let slot_val = self.emit(
2157                CfgInstData::Load {
2158                    slot: live_slot.slot,
2159                },
2160                live_slot.ty,
2161                span,
2162            );
2163            self.emit(CfgInstData::Drop { value: slot_val }, Type::UNIT, span);
2164        }
2165        self.emit(
2166            CfgInstData::StorageDead {
2167                slot: live_slot.slot,
2168            },
2169            Type::UNIT,
2170            span,
2171        );
2172    }
2173
2174    /// Emit Drop for a function parameter.
2175    /// Unlike locals, params don't use StorageLive/StorageDead — they are live for the
2176    /// entire function. We just need to load and drop the value.
2177    fn emit_drop_for_param(&mut self, live_param: &LiveParam, span: gruel_span::Span) {
2178        let param_val = self.emit(
2179            CfgInstData::Param {
2180                index: live_param.param_slot,
2181            },
2182            live_param.ty,
2183            span,
2184        );
2185        self.emit(CfgInstData::Drop { value: param_val }, Type::UNIT, span);
2186    }
2187
2188    // ============================================================================
2189    // Place Expression Tracing (ADR-0030)
2190    // ============================================================================
2191
2192    /// Try to trace an AIR expression back to a Place.
2193    ///
2194    /// Returns `Some((base, projections))` if the expression represents a place
2195    /// (lvalue) that can be read from or written to. Returns `None` if the
2196    /// expression is not a simple place (e.g., a function call result).
2197    ///
2198    /// This function traces chains like `arr[i][j].field` into a `PlaceBase` and
2199    /// a list of `Projection`s. The projections are returned in order from the
2200    /// base outward (e.g., for `arr[i].x`, the projections are `[Index(i), Field(x)]`).
2201    ///
2202    /// The returned CfgValue indices for Index projections are the already-lowered
2203    /// index values, which must be computed before calling this function.
2204    fn try_trace_place(&mut self, air_ref: AirRef) -> TracedPlace {
2205        let inst = self.air.get(air_ref);
2206
2207        match &inst.data {
2208            // Base case: Load from a local variable
2209            AirInstData::Load { slot } => Some((PlaceBase::Local(*slot), Vec::new())),
2210
2211            // Base case: Parameter reference
2212            AirInstData::Param { index } => Some((PlaceBase::Param(*index), Vec::new())),
2213
2214            // Recursive case: Array index
2215            AirInstData::IndexGet {
2216                base,
2217                array_type,
2218                index,
2219            } => {
2220                // Recursively trace the base
2221                let (base_place, mut projections) = self.try_trace_place(*base)?;
2222
2223                // Lower the index expression to get the CfgValue
2224                let index_val = self.lower_value(*index)?;
2225
2226                // Add the Index projection
2227                projections.push((
2228                    Projection::Index {
2229                        array_type: *array_type,
2230                        index: index_val,
2231                    },
2232                    Some(index_val),
2233                ));
2234
2235                Some((base_place, projections))
2236            }
2237
2238            // Recursive case: Field access
2239            AirInstData::FieldGet {
2240                base,
2241                struct_id,
2242                field_index,
2243            } => {
2244                // Recursively trace the base
2245                let (base_place, mut projections) = self.try_trace_place(*base)?;
2246
2247                // Add the Field projection
2248                projections.push((
2249                    Projection::Field {
2250                        struct_id: *struct_id,
2251                        field_index: *field_index,
2252                    },
2253                    None,
2254                ));
2255
2256                Some((base_place, projections))
2257            }
2258
2259            // Not a simple place expression
2260            _ => None,
2261        }
2262    }
2263
2264    /// Lower a place expression from AIR to a CFG PlaceRead instruction.
2265    ///
2266    /// This is called when we detect that an IndexGet or FieldGet chain can be
2267    /// represented as a single PlaceRead, avoiding redundant Load instructions.
2268    fn lower_place_read(
2269        &mut self,
2270        air_ref: AirRef,
2271        ty: Type,
2272        span: gruel_span::Span,
2273    ) -> Option<CfgValue> {
2274        // Try to trace the expression to a place
2275        let (base, projections) = self.try_trace_place(air_ref)?;
2276
2277        // Build the Place with all projections
2278        let proj_iter = projections.into_iter().map(|(proj, _)| proj);
2279        let place = self.cfg.make_place(base, proj_iter);
2280
2281        // Emit the PlaceRead instruction
2282        let value = self.emit(CfgInstData::PlaceRead { place }, ty, span);
2283
2284        Some(value)
2285    }
2286
2287    /// Lower an AIR place reference to a CFG Place.
2288    ///
2289    /// This converts AirPlaceRef -> AirPlace -> CFG Place, translating projections
2290    /// and lowering any index expressions to CFG values.
2291    ///
2292    /// ADR-0030 Phase 8: This is the bridge between AIR's PlaceRead/PlaceWrite
2293    /// and CFG's PlaceRead/PlaceWrite.
2294    fn lower_air_place(&mut self, place_ref: AirPlaceRef) -> Option<Place> {
2295        let air_place = self.air.get_place(place_ref);
2296
2297        // Convert the base
2298        let base = match air_place.base {
2299            AirPlaceBase::Local(slot) => PlaceBase::Local(slot),
2300            AirPlaceBase::Param(slot) => PlaceBase::Param(slot),
2301        };
2302
2303        // Convert projections, lowering any index expressions
2304        let air_projections = self.air.get_place_projections(air_place);
2305        let mut cfg_projections = Vec::with_capacity(air_projections.len());
2306
2307        for proj in air_projections {
2308            let cfg_proj = match proj {
2309                AirProjection::Field {
2310                    struct_id,
2311                    field_index,
2312                } => Projection::Field {
2313                    struct_id: *struct_id,
2314                    field_index: *field_index,
2315                },
2316                AirProjection::Index { array_type, index } => {
2317                    // Lower the index expression to a CFG value
2318                    let index_val = self.lower_value(*index)?;
2319                    Projection::Index {
2320                        array_type: *array_type,
2321                        index: index_val,
2322                    }
2323                }
2324            };
2325            cfg_projections.push(cfg_proj);
2326        }
2327
2328        // Create the CFG place
2329        let place = self.cfg.make_place(base, cfg_projections);
2330
2331        Some(place)
2332    }
2333}
2334
2335#[cfg(test)]
2336mod tests {
2337    use super::*;
2338    use gruel_air::Sema;
2339    use gruel_error::PreviewFeatures;
2340    use gruel_lexer::Lexer;
2341    use gruel_parser::Parser;
2342    use gruel_rir::AstGen;
2343
2344    fn build_cfg(source: &str) -> Cfg {
2345        let lexer = Lexer::new(source);
2346        let (tokens, interner) = lexer.tokenize().unwrap();
2347        let parser = Parser::new(tokens, interner);
2348        let (ast, interner) = parser.parse().unwrap();
2349
2350        let astgen = AstGen::new(&ast, &interner);
2351        let rir = astgen.generate();
2352
2353        let sema = Sema::new(&rir, &interner, PreviewFeatures::new());
2354        let output = sema.analyze_all().unwrap();
2355
2356        let func = &output.functions[0];
2357        CfgBuilder::build(func, &output.type_pool).cfg
2358    }
2359
2360    #[test]
2361    fn test_simple_return() {
2362        let cfg = build_cfg("fn main() -> i32 { 42 }");
2363
2364        assert_eq!(cfg.block_count(), 1);
2365        assert_eq!(cfg.fn_name(), "main");
2366
2367        let entry = cfg.get_block(cfg.entry);
2368        assert!(matches!(entry.terminator, Terminator::Return { .. }));
2369    }
2370
2371    #[test]
2372    fn test_if_else() {
2373        let cfg = build_cfg("fn main() -> i32 { if true { 1 } else { 2 } }");
2374
2375        // Should have: entry, then, else, join
2376        assert!(cfg.block_count() >= 3);
2377    }
2378
2379    #[test]
2380    fn test_while_loop() {
2381        let cfg = build_cfg("fn main() -> i32 { let mut x = 0; while x < 10 { x = x + 1; } x }");
2382
2383        // Should have: entry, header, body, exit, and possibly join blocks
2384        assert!(cfg.block_count() >= 3);
2385    }
2386
2387    #[test]
2388    fn test_short_circuit_and() {
2389        let cfg = build_cfg("fn main() -> i32 { if true && false { 1 } else { 0 } }");
2390
2391        // && creates extra blocks for short-circuit evaluation
2392        assert!(cfg.block_count() >= 3);
2393    }
2394
2395    #[test]
2396    fn test_diverging_in_if_condition() {
2397        // Test that a diverging expression (block with return) in an if condition
2398        // is handled correctly without panicking.
2399        let cfg = build_cfg("fn main() -> i32 { if { return 1; true } { 2 } else { 3 } }");
2400
2401        // Should have at least entry block
2402        assert!(cfg.block_count() >= 1);
2403        // The function should return from the block in the condition
2404        let entry = cfg.get_block(cfg.entry);
2405        assert!(matches!(entry.terminator, Terminator::Return { .. }));
2406    }
2407
2408    #[test]
2409    fn test_diverging_in_loop_body() {
2410        // Test that a return inside a loop body is handled correctly.
2411        let cfg = build_cfg("fn main() -> i32 { loop { return 42; } }");
2412
2413        // The function should return from within the loop
2414        assert!(cfg.block_count() >= 2);
2415    }
2416}