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