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