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}