gruel_air/sema/context.rs
1//! Analysis context and helper types for semantic analysis.
2//!
3//! This module contains the supporting structures used during function body
4//! analysis, including local variable tracking, scope management, and move
5//! state tracking for affine types.
6
7use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
8
9use gruel_rir::RirParamMode;
10use gruel_util::CompileWarning;
11use gruel_util::Span;
12use lasso::Spur;
13
14use crate::scope::ScopedContext;
15use crate::types::{EnumId, StructId, Type};
16
17/// Information about a local variable.
18#[derive(Debug, Clone)]
19pub(crate) struct LocalVar {
20 /// Slot index for this variable
21 pub slot: u32,
22 /// Type of the variable
23 pub ty: Type,
24 /// Whether the variable is mutable
25 pub is_mut: bool,
26 /// Span of the variable declaration (for unused variable warnings)
27 pub span: Span,
28 /// Whether @allow(unused_variable) was applied to this binding
29 pub allow_unused: bool,
30}
31
32/// A path of field accesses from a root variable.
33/// For example, `s.a.b` is represented as [sym("a"), sym("b")] with root sym("s").
34pub(crate) type FieldPath = Vec<Spur>;
35
36/// Tracks move state for a variable, including partial (field-level) moves.
37#[derive(Debug, Clone, Default)]
38pub(crate) struct VariableMoveState {
39 /// If Some, the entire variable has been fully moved at this span.
40 pub full_move: Option<Span>,
41 /// Partial moves: maps field paths to the span where they were moved.
42 /// For example, if `s.a` was moved, this contains ([sym("a")], span).
43 pub partial_moves: HashMap<FieldPath, Span>,
44}
45
46impl VariableMoveState {
47 /// Mark a field path as moved.
48 pub fn mark_path_moved(&mut self, path: &[Spur], span: Span) {
49 if path.is_empty() {
50 // Moving the whole variable
51 self.full_move = Some(span);
52 // Clear partial moves since the whole thing is moved
53 self.partial_moves.clear();
54 } else {
55 // Partial move - only if not already fully moved
56 if self.full_move.is_none() {
57 self.partial_moves.insert(path.to_vec(), span);
58 }
59 }
60 }
61
62 /// Check if a field path is moved.
63 /// Returns Some(span) if the path (or any ancestor) is moved.
64 #[allow(dead_code)]
65 pub fn is_path_moved(&self, path: &[Spur]) -> Option<Span> {
66 // If fully moved, everything is moved
67 if let Some(span) = self.full_move {
68 return Some(span);
69 }
70
71 // Check if this exact path is moved
72 if let Some(span) = self.partial_moves.get(path) {
73 return Some(*span);
74 }
75
76 // Check if any prefix (ancestor) of this path is moved
77 // e.g., if s.a is moved, then s.a.b is also considered moved
78 for len in 1..path.len() {
79 if let Some(span) = self.partial_moves.get(&path[..len]) {
80 return Some(*span);
81 }
82 }
83
84 None
85 }
86
87 /// Check if the entire variable (including all fields) is fully valid to use.
88 /// Returns Some(span) if there's any move (full or partial) that would prevent use.
89 pub fn is_any_part_moved(&self) -> Option<Span> {
90 if let Some(span) = self.full_move {
91 return Some(span);
92 }
93 self.partial_moves.values().next().copied()
94 }
95
96 /// Check if the variable has any move state.
97 pub fn is_empty(&self) -> bool {
98 self.full_move.is_none() && self.partial_moves.is_empty()
99 }
100
101 /// Merge move states from two branches (union semantics).
102 /// A variable is considered moved after a branch if it was moved in EITHER branch.
103 /// This prevents use-after-move when a value might have been moved.
104 pub fn merge_union(branch1: &Self, branch2: &Self) -> Self {
105 // If either branch has a full move, the result is a full move
106 // (use the span from whichever branch has it, preferring branch1)
107 let full_move = branch1.full_move.or(branch2.full_move);
108
109 // A partial move is kept if it appears in EITHER branch
110 let mut partial_moves = branch1.partial_moves.clone();
111 for (path, span) in &branch2.partial_moves {
112 partial_moves.entry(path.clone()).or_insert(*span);
113 }
114
115 Self {
116 full_move,
117 partial_moves,
118 }
119 }
120}
121
122/// Information about a function parameter.
123#[derive(Debug, Clone)]
124pub(crate) struct ParamInfo {
125 /// Parameter name symbol
126 pub name: Spur,
127 /// Starting ABI slot for this parameter (0-based).
128 /// For scalar types, this is the single slot.
129 /// For struct types, this is the first field's slot.
130 pub abi_slot: u32,
131 /// Parameter type
132 pub ty: Type,
133 /// Parameter passing mode
134 pub mode: RirParamMode,
135}
136
137/// Context for analyzing instructions within a function.
138///
139/// Bundles together the mutable state that needs to be threaded through
140/// recursive `analyze_inst` calls.
141pub(crate) struct AnalysisContext<'a> {
142 /// Local variables in scope
143 pub locals: HashMap<Spur, LocalVar>,
144 /// Function parameters (immutable reference, shared across the function)
145 pub params: &'a [ParamInfo],
146 /// Next available slot for local variables
147 pub next_slot: u32,
148 /// How many loops we're nested inside (for break/continue validation)
149 pub loop_depth: u32,
150 /// If set, `break` is forbidden in the current innermost loop.
151 /// Contains the element type name for the error message.
152 /// Set when iterating over arrays with non-Copy element types, because
153 /// breaking would leave un-dropped elements.
154 pub forbid_break: Option<String>,
155 /// How many `checked` blocks we're nested inside (for unchecked operation validation)
156 pub checked_depth: u32,
157 /// Local variables that have been read (for unused variable detection)
158 pub used_locals: HashSet<Spur>,
159 /// Return type of the current function (for explicit return validation)
160 pub return_type: Type,
161 /// Scope stack for efficient scope management.
162 /// Each entry is a list of (symbol, old_value) pairs for variables added/shadowed in that scope.
163 /// When a scope is popped, we restore old values (for shadowed vars) or remove new vars.
164 pub scope_stack: Vec<Vec<(Spur, Option<LocalVar>)>>,
165 /// Resolved types from HM inference.
166 /// Maps RIR instruction refs to their resolved concrete types.
167 /// This is populated by running constraint generation and unification
168 /// before AIR emission.
169 pub resolved_types: &'a HashMap<InstRef, Type>,
170 /// Variables that have been moved (for affine type checking).
171 /// Maps variable symbol to move state (supports partial/field-level moves).
172 pub moved_vars: HashMap<Spur, VariableMoveState>,
173 /// Warnings collected during function analysis.
174 /// This is per-function to enable future parallel analysis.
175 pub warnings: Vec<CompileWarning>,
176 /// Local string table: maps string content to local index (for deduplication within function).
177 /// This is per-function to enable parallel analysis - strings are merged globally after.
178 pub local_string_table: HashMap<String, u32>,
179 /// Local string data indexed by local string table index.
180 /// After analysis, these are merged into the global string table with ID remapping.
181 pub local_strings: Vec<String>,
182 /// Local byte-blob data indexed by local bytes table index. Used by
183 /// `@embed_file`. After analysis these are merged into the global bytes
184 /// table with ID remapping in the AIR instructions.
185 pub local_bytes: Vec<Vec<u8>>,
186 /// Comptime type variables: maps variable symbols to their compile-time type values.
187 /// When a variable is bound to a comptime type (e.g., `let P = make_point()` where
188 /// `make_point() -> type`), this map stores the resolved type so it can be used
189 /// as a type annotation (e.g., `let p: P = ...`).
190 pub comptime_type_vars: HashMap<Spur, Type>,
191 /// Comptime value variables: maps variable symbols to their compile-time constant values.
192 /// When an anonymous struct method captures comptime parameters from the enclosing function
193 /// (e.g., `fn FixedBuffer(comptime N: i32)` creates a struct with methods that reference `N`),
194 /// this map stores the captured values so method bodies can resolve them.
195 pub comptime_value_vars: HashMap<Spur, ConstValue>,
196 /// Functions referenced during analysis of this function.
197 /// Used for lazy semantic analysis (Phase 3 of module system) to track
198 /// which functions need to be analyzed. Each entry is a function name symbol.
199 pub referenced_functions: HashSet<Spur>,
200 /// Methods referenced during analysis of this function.
201 /// Each entry is (struct_id, method_name) matching the key format in methods map.
202 pub referenced_methods: HashSet<(StructId, Spur)>,
203 /// ADR-0076: when set, the named binding is being read as part of a
204 /// borrowing call argument (`borrow x`, `inout x`, or the implicit
205 /// re-borrow of a `Ref(T)` / `MutRef(T)` parameter forwarded to a
206 /// `Ref(T)` / `MutRef(T)` callee param). The move-out check on
207 /// `Borrow`-mode parameter reads is suppressed for this binding —
208 /// passing a borrow on is not a move.
209 pub borrow_arg_skip_move: Option<Spur>,
210 /// ADR-0079 Phase 2b: maps a let-binding name to an in-progress
211 /// `@uninit(T)` construction. Subsequent `@field_set(name, …, …)`
212 /// calls record the field write here; `@finalize(name)` reads the
213 /// accumulated map, emits a `StructInit`, and removes the entry.
214 pub uninit_handles: HashMap<Spur, UninitHandle>,
215 /// ADR-0079 Phase 3: per-arm comptime binding for arms expanded
216 /// from a `comptime_unroll for` template. Keyed by the expanded
217 /// arm's index in the post-expansion arm list. The match-arm
218 /// body analyzer pushes the binding into `comptime_value_vars`
219 /// before recursing and pops afterwards.
220 pub unroll_arm_bindings: HashMap<usize, UnrollArmBinding>,
221}
222
223/// ADR-0079 Phase 3: comptime binding to install while analyzing
224/// the body of an arm expanded from `comptime_unroll for v in …`.
225#[derive(Debug, Clone)]
226pub struct UnrollArmBinding {
227 /// The loop variable name (e.g. `v`).
228 pub binding: Spur,
229 /// The variant value bound for this iteration (a `VariantInfo`-
230 /// shaped struct from `@type_info(Self).variants`).
231 pub value: ConstValue,
232}
233
234/// ADR-0079: state of an in-progress `@uninit(T)` construction.
235#[derive(Debug, Clone)]
236pub struct UninitHandle {
237 /// Struct being constructed (or, for variant-uninit, the enum's struct-shaped payload).
238 pub target: UninitTarget,
239 /// Field-name → analyzed value. Populated by `@field_set`.
240 pub fields: HashMap<Spur, crate::AirRef>,
241}
242
243/// What is being built up by an in-progress uninit handle.
244#[derive(Debug, Clone, Copy)]
245pub enum UninitTarget {
246 /// `@uninit(T)` for a plain struct.
247 Struct(StructId),
248 /// `@variant_uninit(T, tag)` for an enum variant.
249 EnumVariant { enum_id: EnumId, variant_idx: u32 },
250}
251
252// Import InstRef for use in resolved_types
253use gruel_rir::InstRef;
254
255impl ScopedContext for AnalysisContext<'_> {
256 type VarInfo = LocalVar;
257
258 fn locals_mut(&mut self) -> &mut HashMap<Spur, Self::VarInfo> {
259 &mut self.locals
260 }
261
262 fn scope_stack_mut(&mut self) -> &mut Vec<Vec<(Spur, Option<Self::VarInfo>)>> {
263 &mut self.scope_stack
264 }
265
266 /// Insert a local variable, tracking it in the current scope for later cleanup.
267 ///
268 /// This override also clears any moved state for the variable, which handles
269 /// shadowing: `let x = moved_val; let x = new_val;`
270 /// The new `x` is a fresh binding and shouldn't carry the old moved state.
271 fn insert_local(&mut self, symbol: Spur, var: LocalVar) {
272 let old_value = self.locals.insert(symbol, var);
273 // Track in the current scope (if any) for cleanup on pop
274 if let Some(current_scope) = self.scope_stack.last_mut() {
275 current_scope.push((symbol, old_value));
276 }
277 // When a variable is (re)declared, clear any moved state for it.
278 self.moved_vars.remove(&symbol);
279 }
280}
281
282impl AnalysisContext<'_> {
283 /// Merge move states from two branches.
284 ///
285 /// For if-else expressions, a variable is considered moved after the expression
286 /// if it was moved in EITHER branch (union semantics). This prevents use-after-move
287 /// when a value might have been moved in one branch:
288 ///
289 /// ```gruel
290 /// if cond { consume(x) } else { }
291 /// x // Error: x might have been moved in the then-branch
292 /// ```
293 ///
294 /// When one branch diverges (returns Never), only the other branch's moves matter:
295 /// - If then-branch diverges, else-branch's moves are used (then never returns)
296 /// - If else-branch diverges, then-branch's moves are used (else never returns)
297 /// - If both diverge, the whole if-else diverges and moves don't matter
298 pub fn merge_branch_moves(
299 &mut self,
300 then_moves: HashMap<Spur, VariableMoveState>,
301 else_moves: HashMap<Spur, VariableMoveState>,
302 then_diverges: bool,
303 else_diverges: bool,
304 ) {
305 // If then-branch diverges, use else-branch's moves
306 // If else-branch diverges, use then-branch's moves
307 // If both diverge, the whole expression diverges - doesn't matter what we do
308 // If neither diverges, merge the moves (union - moved in either = moved after)
309 match (then_diverges, else_diverges) {
310 (true, true) => {
311 // Both branches diverge - no need to merge, the code after
312 // the if-else is unreachable. Use then_moves arbitrarily.
313 self.moved_vars = then_moves;
314 }
315 (true, false) => {
316 // Then-branch diverges, else-branch continues.
317 // Use else-branch's moves (then never executes to completion).
318 self.moved_vars = else_moves;
319 }
320 (false, true) => {
321 // Else-branch diverges, then-branch continues.
322 // Use then-branch's moves (else never executes to completion).
323 self.moved_vars = then_moves;
324 }
325 (false, false) => {
326 // Neither diverges - merge the moves (union).
327 // A variable is moved after if-else if moved in EITHER branch.
328 let mut merged = HashMap::default();
329
330 // Include all moves from then-branch
331 for (symbol, then_state) in &then_moves {
332 if let Some(else_state) = else_moves.get(symbol) {
333 // Variable has state in both branches - merge them
334 let merged_state = VariableMoveState::merge_union(then_state, else_state);
335 if !merged_state.is_empty() {
336 merged.insert(*symbol, merged_state);
337 }
338 } else {
339 // Variable only moved in then-branch
340 if !then_state.is_empty() {
341 merged.insert(*symbol, then_state.clone());
342 }
343 }
344 }
345
346 // Include moves that only appear in else-branch
347 for (symbol, else_state) in &else_moves {
348 if !then_moves.contains_key(symbol) && !else_state.is_empty() {
349 merged.insert(*symbol, else_state.clone());
350 }
351 }
352
353 self.moved_vars = merged;
354 }
355 }
356 }
357
358 /// Add a string to the local string table, returning its local index.
359 ///
360 /// This deduplicates strings within a single function. After function analysis
361 /// completes, local strings are merged into the global string table with ID
362 /// remapping in the AIR instructions.
363 pub fn add_local_string(&mut self, content: String) -> u32 {
364 use std::collections::hash_map::Entry;
365 match self.local_string_table.entry(content) {
366 Entry::Occupied(e) => *e.get(),
367 Entry::Vacant(e) => {
368 let id = self.local_strings.len() as u32;
369 self.local_strings.push(e.key().clone());
370 e.insert(id);
371 id
372 }
373 }
374 }
375
376 /// Append a byte blob to the local bytes table, returning its local
377 /// index. Bytes are not deduplicated — each `@embed_file` call gets a
378 /// fresh entry, since users may legitimately embed two files with
379 /// identical contents from different paths.
380 pub fn add_local_bytes(&mut self, bytes: Vec<u8>) -> u32 {
381 let id = self.local_bytes.len() as u32;
382 self.local_bytes.push(bytes);
383 id
384 }
385}
386
387/// Result of analyzing an instruction: the AIR reference and its synthesized type.
388#[derive(Debug, Clone, Copy)]
389pub(crate) struct AnalysisResult {
390 /// Reference to the generated AIR instruction
391 pub air_ref: AirRef,
392 /// The synthesized type of this expression
393 pub ty: Type,
394}
395
396use crate::inst::AirRef;
397
398impl AnalysisResult {
399 #[must_use]
400 pub fn new(air_ref: AirRef, ty: Type) -> Self {
401 Self { air_ref, ty }
402 }
403}
404
405/// An item stored on the comptime heap.
406///
407/// The comptime heap stores composite values (structs, arrays) created during
408/// comptime evaluation. These are referenced by index (`u32`) from
409/// `ConstValue::Struct` and `ConstValue::Array` so that `ConstValue` can
410/// remain `Copy`.
411pub enum ComptimeHeapItem {
412 /// A comptime struct instance: the struct's `StructId` and its field values
413 /// in declaration order.
414 Struct {
415 struct_id: StructId,
416 fields: Vec<ConstValue>,
417 },
418 /// A comptime array instance: element values in order.
419 Array(Vec<ConstValue>),
420 /// Tuple enum variant fields (positional).
421 EnumData(Vec<ConstValue>),
422 /// Struct enum variant fields (in declaration order).
423 EnumStruct(Vec<ConstValue>),
424 /// A comptime string value.
425 String(String),
426}
427
428/// A value that can be computed at compile time.
429///
430/// This is used for constant expression evaluation, primarily for compile-time
431/// bounds checking. It can be extended for future `comptime` features.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
433pub enum ConstValue {
434 /// Integer value (signed to handle arithmetic correctly)
435 Integer(i64),
436 /// Boolean value
437 Bool(bool),
438 /// Type value - stores a concrete type for type parameters.
439 /// This is used when a `comptime T: type` parameter is instantiated
440 /// with a specific type like `i32` or `bool`.
441 Type(Type),
442 /// Unit value `()` — the result of statements (let bindings, assignments)
443 /// and expressions of unit type within comptime blocks.
444 Unit,
445 /// Index into `Sema::comptime_heap` for a comptime struct instance.
446 /// Preserves the `Copy` trait while supporting composite values.
447 Struct(u32),
448 /// Index into `Sema::comptime_heap` for a comptime array instance.
449 Array(u32),
450 /// Index into `Sema::comptime_heap` for a comptime string value.
451 ComptimeStr(u32),
452 /// Enum variant with no data (e.g., `Color::Red`).
453 /// Stores the enum id and variant index.
454 EnumVariant { enum_id: EnumId, variant_idx: u32 },
455 /// Enum variant with tuple data (e.g., `Option::Some(42)`).
456 /// Data fields are stored on the comptime heap.
457 EnumData {
458 enum_id: EnumId,
459 variant_idx: u32,
460 heap_idx: u32,
461 },
462 /// Enum variant with struct data (e.g., `Shape::Rect { w: 10, h: 20 }`).
463 /// Fields are stored on the comptime heap in declaration order.
464 EnumStruct {
465 enum_id: EnumId,
466 variant_idx: u32,
467 heap_idx: u32,
468 },
469 /// Internal control-flow signal: produced by `break` inside a comptime loop.
470 /// Never escapes `evaluate_comptime_block` — consumed by Loop/InfiniteLoop cases.
471 BreakSignal,
472 /// Internal control-flow signal: produced by `continue` inside a comptime loop.
473 /// Never escapes `evaluate_comptime_block` — consumed by Loop/InfiniteLoop cases.
474 ContinueSignal,
475 /// Internal control-flow signal: produced by `return` inside a comptime function.
476 /// Never escapes a comptime `Call` handler — the return value is stored in
477 /// `Sema::comptime_return_value` before this signal is returned.
478 ReturnSignal,
479}
480
481impl ConstValue {
482 /// Try to extract an integer value.
483 pub fn as_integer(self) -> Option<i64> {
484 match self {
485 ConstValue::Integer(n) => Some(n),
486 _ => None,
487 }
488 }
489
490 /// Try to extract a boolean value.
491 pub fn as_bool(self) -> Option<bool> {
492 match self {
493 ConstValue::Bool(b) => Some(b),
494 _ => None,
495 }
496 }
497}
498
499// ADR-0081: `StringReceiverStorage`, `BuiltinMethodContext`, and
500// `ReceiverInfo` retired alongside the `BUILTIN_TYPES` registry. Method
501// dispatch on the prelude `String` flows through the regular user-method
502// path now.
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use lasso::ThreadedRodeo;
508
509 // =========================================================================
510 // VariableMoveState tests
511 // =========================================================================
512
513 #[test]
514 fn variable_move_state_default_is_empty() {
515 let state = VariableMoveState::default();
516 assert!(state.full_move.is_none());
517 assert!(state.partial_moves.is_empty());
518 assert!(state.is_empty());
519 }
520
521 #[test]
522 fn variable_move_state_full_move() {
523 let mut state = VariableMoveState::default();
524 let span = Span::new(10, 20);
525 state.mark_path_moved(&[], span);
526
527 assert!(state.full_move.is_some());
528 assert_eq!(state.full_move.unwrap(), span);
529 assert!(state.partial_moves.is_empty()); // Full move clears partials
530 }
531
532 #[test]
533 fn variable_move_state_is_path_moved_after_full_move() {
534 let mut state = VariableMoveState::default();
535 let span = Span::new(10, 20);
536 state.mark_path_moved(&[], span);
537
538 // Any path should be considered moved after a full move
539 assert_eq!(state.is_path_moved(&[]), Some(span));
540
541 let interner = ThreadedRodeo::new();
542 let field_x = interner.get_or_intern("x");
543 assert_eq!(state.is_path_moved(&[field_x]), Some(span));
544 }
545
546 #[test]
547 fn variable_move_state_partial_move() {
548 let mut state = VariableMoveState::default();
549 let interner = ThreadedRodeo::new();
550 let field_x = interner.get_or_intern("x");
551 let span = Span::new(10, 20);
552
553 state.mark_path_moved(&[field_x], span);
554
555 assert!(state.full_move.is_none());
556 assert_eq!(state.partial_moves.len(), 1);
557 assert_eq!(state.is_path_moved(&[field_x]), Some(span));
558 }
559
560 #[test]
561 fn variable_move_state_partial_move_does_not_affect_root() {
562 let mut state = VariableMoveState::default();
563 let interner = ThreadedRodeo::new();
564 let field_x = interner.get_or_intern("x");
565 let span = Span::new(10, 20);
566
567 state.mark_path_moved(&[field_x], span);
568
569 // The root path should not be moved if only a field is moved
570 assert!(state.is_path_moved(&[]).is_none());
571 }
572
573 #[test]
574 fn variable_move_state_partial_move_affects_descendants() {
575 let mut state = VariableMoveState::default();
576 let interner = ThreadedRodeo::new();
577 let field_a = interner.get_or_intern("a");
578 let field_b = interner.get_or_intern("b");
579 let span = Span::new(10, 20);
580
581 // Move s.a
582 state.mark_path_moved(&[field_a], span);
583
584 // s.a.b should also be considered moved (parent is moved)
585 assert_eq!(state.is_path_moved(&[field_a, field_b]), Some(span));
586
587 // s.b should not be moved
588 assert!(state.is_path_moved(&[field_b]).is_none());
589 }
590
591 #[test]
592 fn variable_move_state_multiple_partial_moves() {
593 let mut state = VariableMoveState::default();
594 let interner = ThreadedRodeo::new();
595 let field_x = interner.get_or_intern("x");
596 let field_y = interner.get_or_intern("y");
597 let span1 = Span::new(10, 20);
598 let span2 = Span::new(30, 40);
599
600 state.mark_path_moved(&[field_x], span1);
601 state.mark_path_moved(&[field_y], span2);
602
603 assert!(state.full_move.is_none());
604 assert_eq!(state.partial_moves.len(), 2);
605 assert_eq!(state.is_path_moved(&[field_x]), Some(span1));
606 assert_eq!(state.is_path_moved(&[field_y]), Some(span2));
607 }
608
609 #[test]
610 fn variable_move_state_full_move_after_partial_clears_partials() {
611 let mut state = VariableMoveState::default();
612 let interner = ThreadedRodeo::new();
613 let field_x = interner.get_or_intern("x");
614 let span1 = Span::new(10, 20);
615 let span2 = Span::new(30, 40);
616
617 // First, partially move a field
618 state.mark_path_moved(&[field_x], span1);
619 assert_eq!(state.partial_moves.len(), 1);
620
621 // Then, fully move the variable
622 state.mark_path_moved(&[], span2);
623
624 // Full move should clear partial moves
625 assert!(state.full_move.is_some());
626 assert!(state.partial_moves.is_empty());
627 }
628
629 #[test]
630 fn variable_move_state_partial_after_full_is_ignored() {
631 let mut state = VariableMoveState::default();
632 let interner = ThreadedRodeo::new();
633 let field_x = interner.get_or_intern("x");
634 let span1 = Span::new(10, 20);
635 let span2 = Span::new(30, 40);
636
637 // First, fully move the variable
638 state.mark_path_moved(&[], span1);
639
640 // Then try to partially move a field
641 state.mark_path_moved(&[field_x], span2);
642
643 // Partial move should be ignored when already fully moved
644 assert_eq!(state.full_move, Some(span1));
645 assert!(state.partial_moves.is_empty());
646 }
647
648 #[test]
649 fn variable_move_state_is_any_part_moved() {
650 let mut state = VariableMoveState::default();
651 let interner = ThreadedRodeo::new();
652 let field_x = interner.get_or_intern("x");
653 let span1 = Span::new(10, 20);
654 let span2 = Span::new(30, 40);
655
656 // Initially nothing is moved
657 assert!(state.is_any_part_moved().is_none());
658
659 // After partial move
660 state.mark_path_moved(&[field_x], span1);
661 assert_eq!(state.is_any_part_moved(), Some(span1));
662
663 // After full move
664 let mut state2 = VariableMoveState::default();
665 state2.mark_path_moved(&[], span2);
666 assert_eq!(state2.is_any_part_moved(), Some(span2));
667 }
668
669 #[test]
670 fn variable_move_state_merge_union_both_empty() {
671 let state1 = VariableMoveState::default();
672 let state2 = VariableMoveState::default();
673
674 let merged = VariableMoveState::merge_union(&state1, &state2);
675
676 assert!(merged.is_empty());
677 }
678
679 #[test]
680 fn variable_move_state_merge_union_one_full_move() {
681 let mut state1 = VariableMoveState::default();
682 let state2 = VariableMoveState::default();
683 let span = Span::new(10, 20);
684
685 state1.mark_path_moved(&[], span);
686
687 let merged = VariableMoveState::merge_union(&state1, &state2);
688 assert_eq!(merged.full_move, Some(span));
689
690 // Test other order
691 let merged2 = VariableMoveState::merge_union(&state2, &state1);
692 assert_eq!(merged2.full_move, Some(span));
693 }
694
695 #[test]
696 fn variable_move_state_merge_union_both_full_moves_prefers_first() {
697 let mut state1 = VariableMoveState::default();
698 let mut state2 = VariableMoveState::default();
699 let span1 = Span::new(10, 20);
700 let span2 = Span::new(30, 40);
701
702 state1.mark_path_moved(&[], span1);
703 state2.mark_path_moved(&[], span2);
704
705 let merged = VariableMoveState::merge_union(&state1, &state2);
706 assert_eq!(merged.full_move, Some(span1)); // Prefers first
707 }
708
709 #[test]
710 fn variable_move_state_merge_union_partial_moves() {
711 let mut state1 = VariableMoveState::default();
712 let mut state2 = VariableMoveState::default();
713 let interner = ThreadedRodeo::new();
714 let field_x = interner.get_or_intern("x");
715 let field_y = interner.get_or_intern("y");
716 let span1 = Span::new(10, 20);
717 let span2 = Span::new(30, 40);
718
719 state1.mark_path_moved(&[field_x], span1);
720 state2.mark_path_moved(&[field_y], span2);
721
722 let merged = VariableMoveState::merge_union(&state1, &state2);
723
724 // Both partial moves should be present
725 assert_eq!(merged.partial_moves.len(), 2);
726 assert_eq!(merged.is_path_moved(&[field_x]), Some(span1));
727 assert_eq!(merged.is_path_moved(&[field_y]), Some(span2));
728 }
729
730 #[test]
731 fn variable_move_state_merge_union_same_partial_move_prefers_first() {
732 let mut state1 = VariableMoveState::default();
733 let mut state2 = VariableMoveState::default();
734 let interner = ThreadedRodeo::new();
735 let field_x = interner.get_or_intern("x");
736 let span1 = Span::new(10, 20);
737 let span2 = Span::new(30, 40);
738
739 state1.mark_path_moved(&[field_x], span1);
740 state2.mark_path_moved(&[field_x], span2);
741
742 let merged = VariableMoveState::merge_union(&state1, &state2);
743
744 // Should have the span from the first state
745 assert_eq!(merged.partial_moves.len(), 1);
746 assert_eq!(merged.is_path_moved(&[field_x]), Some(span1));
747 }
748
749 // =========================================================================
750 // ConstValue tests
751 // =========================================================================
752
753 #[test]
754 fn const_value_as_integer() {
755 let cv = ConstValue::Integer(42);
756 assert_eq!(cv.as_integer(), Some(42));
757 assert_eq!(cv.as_bool(), None);
758 }
759
760 #[test]
761 fn const_value_as_bool() {
762 let cv = ConstValue::Bool(true);
763 assert_eq!(cv.as_bool(), Some(true));
764 assert_eq!(cv.as_integer(), None);
765
766 let cv2 = ConstValue::Bool(false);
767 assert_eq!(cv2.as_bool(), Some(false));
768 }
769
770 #[test]
771 fn const_value_negative_integer() {
772 let cv = ConstValue::Integer(-100);
773 assert_eq!(cv.as_integer(), Some(-100));
774 }
775
776 #[test]
777 fn const_value_equality() {
778 assert_eq!(ConstValue::Integer(42), ConstValue::Integer(42));
779 assert_ne!(ConstValue::Integer(42), ConstValue::Integer(43));
780 assert_eq!(ConstValue::Bool(true), ConstValue::Bool(true));
781 assert_ne!(ConstValue::Bool(true), ConstValue::Bool(false));
782 assert_ne!(ConstValue::Integer(1), ConstValue::Bool(true));
783 }
784
785 #[test]
786 fn const_value_type_equality() {
787 assert_eq!(ConstValue::Type(Type::I32), ConstValue::Type(Type::I32));
788 assert_ne!(ConstValue::Type(Type::I32), ConstValue::Type(Type::I64));
789 assert_ne!(ConstValue::Type(Type::I32), ConstValue::Integer(32));
790 }
791
792 // =========================================================================
793 // AnalysisResult tests
794 // =========================================================================
795
796 #[test]
797 fn analysis_result_new() {
798 let air_ref = AirRef::from_raw(5);
799 let ty = Type::I32;
800
801 let result = AnalysisResult::new(air_ref, ty);
802
803 assert_eq!(result.air_ref.as_u32(), 5);
804 assert_eq!(result.ty, Type::I32);
805 }
806}