Skip to main content

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}